diff --git a/base/REPL.jl b/base/REPL.jl index 1616ca8f85e24..4d4349b481407 100644 --- a/base/REPL.jl +++ b/base/REPL.jl @@ -727,11 +727,9 @@ function setup_interface(repl::LineEditREPL; hascolor = repl.hascolor, extra_rep (repl.envcolors ? Base.input_color : repl.input_color) : "", keymap_func_data = repl, complete = ShellCompletionProvider(repl), - # Transform "foo bar baz" into `foo bar baz` (shell quoting) - # and pass into Base.repl_cmd for processing (handles `ls` and `cd` - # special) + # Pass into Base.repl_cmd for processing (handles `cd` special) on_done = respond(repl, julia_prompt) do line - Expr(:call, :(Base.repl_cmd), macroexpand(Expr(:macrocall, symbol("@cmd"),line)), outstream(repl)) + Base.repl_cmd(line, outstream(repl)) end) ################################# Stage II ############################# diff --git a/base/client.jl b/base/client.jl index 1530242c9787d..5c8459d02d54c 100644 --- a/base/client.jl +++ b/base/client.jl @@ -39,7 +39,7 @@ exit(n) = ccall(:jl_exit, Void, (Int32,), n) exit() = exit(0) quit() = exit() -function repl_cmd(cmd, out) +function repl_cmd(line, out) shell = shell_split(get(ENV,"JULIA_SHELL",get(ENV,"SHELL","/bin/sh"))) # Note that we can't support the fish shell due to its lack of subshells # See this for details: https://github.com/JuliaLang/julia/issues/4918 @@ -49,21 +49,24 @@ function repl_cmd(cmd, out) shell = "/bin/sh" end - if isempty(cmd.exec) - throw(ArgumentError("no cmd to execute")) - elseif cmd.exec[1] == "cd" + cmds = split(line) + if isempty(cmds) + # The command consists of white space only; do nothing + elseif cmds[1] == "cd" new_oldpwd = pwd() - if length(cmd.exec) > 2 + if length(cmds) > 2 throw(ArgumentError("cd method only takes one argument")) - elseif length(cmd.exec) == 2 - dir = cmd.exec[2] + elseif length(cmds) == 2 + dir = cmds[2] if dir == "-" if !haskey(ENV, "OLDPWD") error("cd: OLDPWD not set") end cd(ENV["OLDPWD"]) else - cd(@windows? dir : readchomp(`$shell -c "echo $(shell_escape(dir))"`)) + cmd = "echo $dir" + unixcmd = `$shell -c $cmd` + cd(@windows? dir : readchomp(unixcmd)) end else cd() @@ -71,7 +74,16 @@ function repl_cmd(cmd, out) ENV["OLDPWD"] = new_oldpwd println(out, pwd()) else - run(ignorestatus(@windows? cmd : (isa(STDIN, TTY) ? `$shell -i -c "($(shell_escape(cmd))) && true"` : `$shell -c "($(shell_escape(cmd))) && true"`))) + # We use two shell levels: An outer one process the shell + # metacharacters (output redirection, command pipelines, + # etc.), and an inner one to redirect stdout, stderr etc. so + # that it doesn't interfere with the REPL's connection to the + # terminal. + parline = "($line)" + ttyflag = isa(STDIN, TTY) ? "-i" : "" + unixcmd = `$shell $ttyflag -c $parline` + wincmd = Cmd(Vector{ByteString}(shell_split(line))) + run(ignorestatus(@windows? wincmd : unixcmd)) end nothing end diff --git a/base/shell.jl b/base/shell.jl index cfd7eb26ee690..50c3b879362ae 100644 --- a/base/shell.jl +++ b/base/shell.jl @@ -81,7 +81,7 @@ function shell_parse(raw::AbstractString, interp::Bool) if done(s,k) error("unterminated double quote") end - if s[k] == '"' || s[k] == '$' + if s[k] == '"' || s[k] == '$' || s[k] == '\\' update_arg(s[i:j-1]); i = k c, k = next(s,k) end @@ -125,34 +125,56 @@ function shell_split(s::AbstractString) args end -function print_shell_word(io::IO, word::AbstractString) - if isempty(word) - print(io, "''") +"Quote by enclosing in single quotes" +function _quote_via_single_quote(word::AbstractString) + # Quote by enclosing in single quotes, and replace all single + # quotes with backslash-quote + quoted = "'" * replace(word, r"'", "'\\''") * "'" + # Remove empty leading or trailing quotes + quoted = replace(quoted, r"^''", "") + quoted = replace(quoted, r"'''$(?!\n)", "'") + if isempty(quoted) + quoted = "''" end - has_single = false - has_special = false - for c in word - if isspace(c) || c=='\\' || c=='\'' || c=='"' || c=='$' - has_special = true - if c == '\'' - has_single = true - end - end + quoted +end +"Quote by enclosing in double quotes" +function _quote_via_double_quote(word::AbstractString) + # Quote by enclosing in double quotes and escaping certain special + # characters with a backslash, and by escaping all exclamation + # marks with a backslash + quoted = "\"" * replace(word, r"(?=[$`\"\\])", "\\") * "\"" + quoted = replace(quoted, r"!", "\"\\!\"\"") + # Remove empty leading or trailing quotes + quoted = replace(quoted, r"^\"\"", "") + quoted = replace(quoted, r"!\"\"$(?!\n)", "!") + if isempty(quoted) + quoted = "''" end - if !has_special - print(io, word) - elseif !has_single - print(io, '\'', word, '\'') - else - print(io, '"') - for c in word - if c == '"' || c == '$' - print(io, '\\') - end - print(io, c) - end - print(io, '"') + quoted +end +"Quote by escaping with backslashes" +function _quote_via_backslash(word::AbstractString) + # Quote by escaping all non-alphanumeric characters with a + # backslash, and by enclosing all newlines with single quotes + quoted = replace(word, r"(?=[^-0-9a-zA-Z_./\n])", "\\") + quoted = replace(quoted, r"\n", "'\\n'") + if isempty(quoted) + quoted = "''" end + quoted +end +function _quote_shell_word(word::AbstractString) + # Return the shortest string + quotes = [_quote_via_single_quote(word), + _quote_via_double_quote(word), + _quote_via_backslash(word)] + shortest = findmin(map(length, quotes))[2] + return quotes[shortest] +end + +function print_shell_word(io::IO, word::AbstractString) + print(io, _quote_shell_word(word)) end function print_shell_escaped(io::IO, cmd::AbstractString, args::AbstractString...) diff --git a/base/sysimg.jl b/base/sysimg.jl index f352779f71bbb..97ae1d35c64db 100644 --- a/base/sysimg.jl +++ b/base/sysimg.jl @@ -91,8 +91,8 @@ include("iobuffer.jl") include("string.jl") include("unicode.jl") include("parse.jl") -include("shell.jl") include("regex.jl") +include("shell.jl") include("base64.jl") importall .Base64 diff --git a/test/repl.jl b/test/repl.jl index 51968f0e07d27..fafc1d93529c9 100644 --- a/test/repl.jl +++ b/test/repl.jl @@ -1,5 +1,17 @@ # This file is a part of Julia. License is MIT: http://julialang.org/license +# Shell escaping +strs = ByteString["", "asdf", "'", "\"", "\\", " ", "\$", + "a\"bc\"d", "'012'3", " a ", "\n", "\\n", "\t", + "echo hello", " \${LANG}", "\"println(ARGS)\""] +for level in 1:3 + cmd = Cmd(strs) + quoted = Base.shell_escape(cmd) + strs2 = Base.shell_split(quoted) + @test strs2 == strs + strs = ByteString[Base.shell_escape(str) for str in strs] +end + # REPL tests isdefined(:TestHelpers) || include(joinpath(dirname(@__FILE__), "TestHelpers.jl")) using TestHelpers diff --git a/test/spawn.jl b/test/spawn.jl index 5707e28a917f1..4819d4878650b 100644 --- a/test/spawn.jl +++ b/test/spawn.jl @@ -327,5 +327,5 @@ let cmd = AbstractString[] # Backticks should automatically quote where necessary cmd = ["foo bar", "baz"] - @test string(`$cmd`) == "`'foo bar' baz`" + @test string(`$cmd`) in ["`'foo bar' baz`", "`foo\\ bar baz`"] end