diff --git a/base/Base.jl b/base/Base.jl index c39b08b13539b2..7247d770b02060 100644 --- a/base/Base.jl +++ b/base/Base.jl @@ -174,8 +174,8 @@ include("iobuffer.jl") include("intfuncs.jl") include("strings/strings.jl") include("parse.jl") -include("shell.jl") include("regex.jl") +include("shell.jl") include("show.jl") include("arrayshow.jl") include("methodshow.jl") diff --git a/base/client.jl b/base/client.jl index 8f5a89fbb86660..c2328f59d57cc5 100644 --- a/base/client.jl +++ b/base/client.jl @@ -31,8 +31,11 @@ stackframe_lineinfo_color() = repl_color("JULIA_STACKFRAME_LINEINFO_COLOR", :bol stackframe_function_color() = repl_color("JULIA_STACKFRAME_FUNCTION_COLOR", :bold) function repl_cmd(cmd, out) - shell = shell_split(get(ENV, "JULIA_SHELL", get(ENV, "SHELL", "/bin/sh"))) + shell = shell_split(get(ENV, "JULIA_SHELL", Sys.iswindows() ? "cmd" : get(ENV, "SHELL", "/bin/sh"))) shell_name = Base.basename(shell[1]) + if Sys.iswindows() + shell_name = lowercase(splitext(shell_name)[1]) # canonicalize for comparisons below + end # Immediately expand all arguments, so that typing e.g. ~/bin/foo works. cmd.exec .= expanduser.(cmd.exec) @@ -66,15 +69,28 @@ function repl_cmd(cmd, out) ENV["OLDPWD"] = new_oldpwd println(out, pwd()) else - @static if !Sys.iswindows() + local command::Cmd + if Sys.iswindows() + if shell_name == "" + command = cmd + elseif shell_name == "cmd" + command = Cmd(`$shell /c $(shell_escape_CMDly(shell_escape_winsomely(cmd)))`, windows_verbatim=true) + elseif shell_name in ("powershell", "pwsh") + command = Cmd(`$shell -Command $(shell_escape_PWSHly(shell_escape_winsomely(cmd)))`, windows_verbatim=true) + elseif shell_name == "busybox" + command = `$shell sh -c $(shell_escape_posixly(cmd))` + else + command = `$shell $cmd` + end + else if shell_name == "fish" shell_escape_cmd = "begin; $(shell_escape_posixly(cmd)); and true; end" else shell_escape_cmd = "($(shell_escape_posixly(cmd))) && true" end - cmd = `$shell -c $shell_escape_cmd` + command = `$shell -c $shell_escape_cmd` end - run(ignorestatus(cmd)) + run(ignorestatus(command)) end nothing end diff --git a/base/shell.jl b/base/shell.jl index 85243a3e405426..0875d6a0283aa8 100644 --- a/base/shell.jl +++ b/base/shell.jl @@ -312,3 +312,70 @@ julia> println(shell_escaped_winsomely("A B\\", "C")) """ shell_escape_winsomely(args::AbstractString...) = sprint(print_shell_escaped_winsomely, args..., sizehint=(sum(length, args)) + 3*length(args)) + +function print_shell_escaped_CMDly(io::IO, arg::AbstractString) + any(in("\r\n"), arg) && throw("Encountered unsupported character by CMD.") + # include " so to avoid toggling behavior of ^ + # the rule for ! is a bit esoteric, but doesn't hurt to add it + arg = replace(arg, r"[%!^<>&|]" => s"^\0") + print(io, arg) +end + +""" + shell_escape_CMDly(arg::AbstractString)::String + +The unexported `shell_escape_CMDly` function takes a string and escapes any special characters +in such a way that it is safe to pass it as an argument to some `CMD.exe`. This may be useful +in concert with the `windows_verbatim` flag to [`Cmd`](@ref) when constructing process +pipelines. + +See also [`shell_escape_BATCHly`](@ref) and [`shell_escape_PWSHly`](@ref). + +# Example +```jldoctest +julia> println(shell_escape_CMDly("\"A B\\\" & C")) +^"A B\\^" ^& C + +!important + Due to a peculiar behavior of the CMD, each command after a literal `|` character + (indicating a command pipeline) must have `shell_escape_CMDly` applied twice. For example: + ``` + to_print = "All for 1 & 1 for all!" + run(Cmd(Cmd(["cmd /c \"break | echo \$(shell_escape_CMDly(shell_escape_CMDly(to_print)))"]), windows_verbatim=true)) + ``` +""" +shell_escape_CMDly(arg::AbstractString) = sprint(print_shell_escaped_CMDly, arg) + +function print_shell_escaped_BATCHly(io::IO, arg::AbstractString) + # see https://www.robvanderwoude.com/variableexpansion.php + # the rule for ! is a bit esoteric, but doesn't hurt to add it + any(in("\r\n"), arg) && throw("Encountered unsupported character by CMD") + arg = replace(arg, r"[()!^\"<>&|]" => s"^\0") + arg = replace(arg, "%" => "%%") + print(io, arg) +end + +""" + shell_escape_BATCHly(arg::AbstractString)::String + +Like [`shell_escape_CMDly`](@ref), but appropriate for use inside batch (*.bat) files for use +with `CMD.exe`. + +See also [`shell_escape_PWSHly`](@ref). +""" +shell_escape_BATCHly(arg::AbstractString) = sprint(print_shell_escaped_BATCHly, arg) + + +function print_shell_escaped_PWSHly(io::IO, arg::AbstractString) + arg = replace(arg, r"`\$#;@" => s"`\0") + print(io, arg) +end + +""" + shell_escape_PWSHly(arg::AbstractString)::String + +Escapes special characters so they can be appropriately used with PowerShell. + +See also [`shell_escape_CMDly`](@ref). +""" +shell_escape_PWSHly(arg::AbstractString) = sprint(print_shell_escaped_PWSHly, arg) diff --git a/doc/src/manual/environment-variables.md b/doc/src/manual/environment-variables.md index 8976b5f40050c3..e9510426d59365 100644 --- a/doc/src/manual/environment-variables.md +++ b/doc/src/manual/environment-variables.md @@ -168,10 +168,9 @@ The absolute path of the shell with which Julia should execute external commands (via `Base.repl_cmd()`). Defaults to the environment variable `$SHELL`, and falls back to `/bin/sh` if `$SHELL` is unset. -!!! note - - On Windows, this environment variable is ignored, and external commands are - executed directly. +On Windows, `$JULIA_SHELL` can be set to `cmd`, `powershell`, `busybox` or `""`. +If set to `""` external commands are executed directly. Defaults to `cmd` if +`$JULIA_SHELL` is not set. ### `JULIA_EDITOR` diff --git a/stdlib/REPL/docs/src/index.md b/stdlib/REPL/docs/src/index.md index 323d0f13fbbe7c..4dd53ef83825b0 100644 --- a/stdlib/REPL/docs/src/index.md +++ b/stdlib/REPL/docs/src/index.md @@ -103,6 +103,7 @@ julia> ; # upon typing ;, the prompt changes (in place) to: shell> shell> echo hello hello ``` +See `JULIA_SHELL` in the Environment Variables section of the Julia manual. ### Search modes