Skip to content

Commit

Permalink
Correctly parse and quote shell commands
Browse files Browse the repository at this point in the history
  • Loading branch information
eschnett committed Sep 18, 2015
1 parent 64d0cc9 commit b1ee6a2
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 27 deletions.
74 changes: 48 additions & 26 deletions base/shell.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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...)
Expand Down
2 changes: 1 addition & 1 deletion base/sysimg.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions test/repl.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down

0 comments on commit b1ee6a2

Please sign in to comment.