Skip to content

Commit eaebcac

Browse files
GregPlowmanmusm
authored andcommitted
REPL shell mode for Windows
REPL shell mode for Windows Documentation JULIA_SHELL environment variable Interacting with Julia docs - xref JULIA_SHELL Fix whitespace JULIA_SHELL doc Fix JULIA_SHELL cross-reference Address code review comments rebase fix
1 parent 2b5faef commit eaebcac

File tree

5 files changed

+73
-11
lines changed

5 files changed

+73
-11
lines changed

base/Base.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,8 @@ include("iobuffer.jl")
174174
include("intfuncs.jl")
175175
include("strings/strings.jl")
176176
include("parse.jl")
177-
include("shell.jl")
178177
include("regex.jl")
178+
include("shell.jl")
179179
include("show.jl")
180180
include("arrayshow.jl")
181181
include("methodshow.jl")

base/client.jl

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ stackframe_lineinfo_color() = repl_color("JULIA_STACKFRAME_LINEINFO_COLOR", :bol
3131
stackframe_function_color() = repl_color("JULIA_STACKFRAME_FUNCTION_COLOR", :bold)
3232

3333
function repl_cmd(cmd, out)
34-
shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh")))
34+
shell = get(ENV, "JULIA_SHELL", nothing)
35+
if shell === nothing || isempty(shell)
36+
shell = Sys.iswindows() ? "cmd" : get(ENV, "SHELL", "/bin/sh")
37+
end
38+
shell = shell_split(shell)
3539
shell_name = Base.basename(shell[1])
40+
Sys.iswindows() && (shell_name = lowercase(splitext(shell_name)[1])) # canonicalize for comparisons below
3641

3742
# Immediately expand all arguments, so that typing e.g. ~/bin/foo works.
3843
cmd.exec .= expanduser.(cmd.exec)
@@ -66,15 +71,26 @@ function repl_cmd(cmd, out)
6671
ENV["OLDPWD"] = new_oldpwd
6772
println(out, pwd())
6873
else
69-
@static if !Sys.iswindows()
74+
local command::Cmd
75+
if Sys.iswindows()
76+
if shell_name == "cmd"
77+
command = Cmd(`$shell /c $(shell_escape_CMDly(shell_escape_winsomely(cmd)))`, windows_verbatim=true)
78+
elseif shell_name in ("powershell", "pwsh")
79+
command = Cmd(`$shell -Command $(shell_escape_PWSHly(shell_escape_winsomely(cmd)))`, windows_verbatim=true)
80+
elseif shell_name == "busybox"
81+
command = `$shell sh -c $(shell_escape_posixly(cmd))`
82+
else
83+
command = `$shell $cmd`
84+
end
85+
else
7086
if shell_name == "fish"
7187
shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end"
7288
else
7389
shell_escape_cmd = "($(shell_escape_posixly(cmd))) && true"
7490
end
75-
cmd = `$shell -c $shell_escape_cmd`
91+
command = `$shell -c $shell_escape_cmd`
7692
end
77-
run(ignorestatus(cmd))
93+
run(ignorestatus(command))
7894
end
7995
nothing
8096
end

base/shell.jl

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ function print_shell_escaped_winsomely(io::IO, args::AbstractString...)
263263
# Quote any arg that contains a whitespace (' ' or '\t') or a double quote mark '"'.
264264
# It's also valid to quote an arg with just a whitespace,
265265
# but the following may be 'safer', and both implementations are valid anyways.
266-
quotes = any(c -> c in (' ', '\t', '"'), arg) || isempty(arg)
266+
quotes = any(c -> c in (' ', '\t', '"', '\r', '\n'), arg) || isempty(arg)
267267
quotes && write(io, '"')
268268
backslashes = 0
269269
for c in arg
@@ -292,7 +292,7 @@ end
292292

293293

294294
"""
295-
shell_escaped_winsomely(args::Union{Cmd,AbstractString...})::String
295+
shell_escaped_winsomely(args::Union{Cmd,AbstractString...}) -> String
296296
297297
Convert the collection of strings `args` into single string suitable for passing as the argument
298298
string for a Windows command line. Windows passes the entire command line as a single string to
@@ -312,3 +312,49 @@ julia> println(shell_escaped_winsomely("A B\\", "C"))
312312
"""
313313
shell_escape_winsomely(args::AbstractString...) =
314314
sprint(print_shell_escaped_winsomely, args..., sizehint=(sum(length, args)) + 3*length(args))
315+
316+
function print_shell_escaped_CMDly(io::IO, arg::AbstractString)
317+
any(c -> c in ('\r', '\n'), arg) && throw(ArgumentError("Encountered unsupported character by CMD."))
318+
# include " so to avoid toggling behavior of ^
319+
arg = replace(arg, r"[%!^\"<>&|]" => s"^\0")
320+
print(io, arg)
321+
end
322+
323+
"""
324+
shell_escape_CMDly(arg::AbstractString) -> String
325+
326+
The unexported `shell_escape_CMDly` function takes a string and escapes any special characters
327+
in such a way that it is safe to pass it as an argument to some `CMD.exe`. This may be useful
328+
in concert with the `windows_verbatim` flag to [`Cmd`](@ref) when constructing process
329+
pipelines.
330+
331+
See also [`shell_escape_PWSHly`](@ref).
332+
333+
# Example
334+
```jldoctest
335+
julia> println(shell_escape_CMDly("\"A B\\\" & C"))
336+
^"A B\\^" ^& C
337+
338+
!important
339+
Due to a peculiar behavior of the CMD, each command after a literal `|` character
340+
(indicating a command pipeline) must have `shell_escape_CMDly` applied twice. For example:
341+
```
342+
to_print = "All for 1 & 1 for all!"
343+
run(Cmd(Cmd(["cmd /c \"break | echo \$(shell_escape_CMDly(shell_escape_CMDly(to_print)))"]), windows_verbatim=true))
344+
```
345+
"""
346+
shell_escape_CMDly(arg::AbstractString) = sprint(print_shell_escaped_CMDly, arg)
347+
348+
function print_shell_escaped_PWSHly(io::IO, arg::AbstractString)
349+
arg = replace(arg, r"[\$#;]" => s"`\0")
350+
print(io, arg)
351+
end
352+
353+
"""
354+
shell_escape_PWSHly(arg::AbstractString) -> String
355+
356+
Escapes special characters so they can be appropriately used with PowerShell.
357+
358+
See also [`shell_escape_CMDly`](@ref).
359+
"""
360+
shell_escape_PWSHly(arg::AbstractString) = sprint(print_shell_escaped_PWSHly, arg)

doc/src/manual/environment-variables.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,9 @@ The absolute path of the shell with which Julia should execute external commands
168168
(via `Base.repl_cmd()`). Defaults to the environment variable `$SHELL`, and
169169
falls back to `/bin/sh` if `$SHELL` is unset.
170170

171-
!!! note
172-
173-
On Windows, this environment variable is ignored, and external commands are
174-
executed directly.
171+
On Windows, `$JULIA_SHELL` can be set to `cmd`, `powershell`, `busybox` or `""`.
172+
If set to `""` external commands are executed directly. Defaults to `cmd` if
173+
`$JULIA_SHELL` is not set.
175174

176175
### `JULIA_EDITOR`
177176

stdlib/REPL/docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ julia> ; # upon typing ;, the prompt changes (in place) to: shell>
103103
shell> echo hello
104104
hello
105105
```
106+
See `JULIA_SHELL` in the Environment Variables section of the Julia manual.
106107

107108
### Search modes
108109

0 commit comments

Comments
 (0)