Skip to content

Commit 8692b2e

Browse files
Merge pull request #19786 from JuliaLang/sk/shellspecial
deprecate unescaped shell special chars in commands
2 parents 13b2254 + 8c660de commit 8692b2e

File tree

5 files changed

+45
-24
lines changed

5 files changed

+45
-24
lines changed

base/managers.jl

+4-4
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,13 @@ function launch_on_machine(manager::SSHManager, machine, cnt, params, launched,
179179

180180
# the default worker timeout
181181
tval = haskey(ENV, "JULIA_WORKER_TIMEOUT") ?
182-
`export JULIA_WORKER_TIMEOUT=$(ENV["JULIA_WORKER_TIMEOUT"]);` : ``
182+
`export JULIA_WORKER_TIMEOUT=$(ENV["JULIA_WORKER_TIMEOUT"])\;` : ``
183183

184184
# Julia process with passed in command line flag arguments
185-
cmd = `cd $dir && $tval $exename $exeflags`
185+
cmd = `cd $dir '&&' $tval $exename $exeflags`
186186

187187
# shell login (-l) with string command (-c) to launch julia process
188-
cmd = `sh -l -c $(shell_escape(cmd))`
188+
cmd = `sh -l -c $(shell_escape(cmd, special = ""))`
189189

190190
# remote launch with ssh with given ssh flags / host / port information
191191
# -T → disable pseudo-terminal allocation
@@ -195,7 +195,7 @@ function launch_on_machine(manager::SSHManager, machine, cnt, params, launched,
195195
# forwarded connections are causing collisions
196196
# -n → Redirects stdin from /dev/null (actually, prevents reading from stdin).
197197
# Used when running ssh in the background.
198-
cmd = `ssh -T -a -x -o ClearAllForwardings=yes -n $sshflags $host $(shell_escape(cmd))`
198+
cmd = `ssh -T -a -x -o ClearAllForwardings=yes -n $sshflags $host $(shell_escape(cmd, special = ""))`
199199

200200
# launch the remote Julia process
201201

base/process.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ end
9797
hash(x::AndCmds, h::UInt) = hash(x.a, hash(x.b, h))
9898
==(x::AndCmds, y::AndCmds) = x.a == y.a && x.b == y.b
9999

100-
shell_escape(cmd::Cmd) = shell_escape(cmd.exec...)
100+
shell_escape(cmd::Cmd; special::AbstractString=shell_special) =
101+
shell_escape(cmd.exec..., special=special)
101102

102103
function show(io::IO, cmd::Cmd)
103104
print_env = cmd.env !== nothing

base/shell.jl

+37-17
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
## shell-like command parsing ##
44

5-
function shell_parse(raw::AbstractString, interp::Bool)
6-
s = lstrip(raw)
7-
#Strips the end but respects the space when the string endswith "\\ "
5+
const shell_special = "#{}()[]<>|&*?~;"
6+
7+
function shell_parse(str::AbstractString, interpolate::Bool=true)
8+
s = lstrip(str)
9+
# strips the end but respects the space when the string ends with "\\ "
810
r = RevString(s)
911
i = start(r)
1012
c_old = nothing
@@ -22,7 +24,7 @@ function shell_parse(raw::AbstractString, interp::Bool)
2224
s = s[1:end-i+1]
2325

2426
last_parse = 0:-1
25-
isempty(s) && return interp ? (Expr(:tuple,:()),last_parse) : ([],last_parse)
27+
isempty(s) && return interpolate ? (Expr(:tuple,:()),last_parse) : ([],last_parse)
2628

2729
in_single_quotes = false
2830
in_double_quotes = false
@@ -57,7 +59,7 @@ function shell_parse(raw::AbstractString, interp::Bool)
5759
end
5860
j = k
5961
end
60-
elseif interp && !in_single_quotes && c == '$'
62+
elseif interpolate && !in_single_quotes && c == '$'
6163
update_arg(s[i:j-1]); i = k; j = k
6264
if done(s,k)
6365
error("\$ right before end of command")
@@ -92,6 +94,8 @@ function shell_parse(raw::AbstractString, interp::Bool)
9294
update_arg(s[i:j-1]); i = k
9395
c, k = next(s,k)
9496
end
97+
elseif !in_single_quotes && !in_double_quotes && c in shell_special
98+
depwarn("special characters \"$shell_special\" should now be quoted in commands", :shell_parse)
9599
end
96100
j = k
97101
end
@@ -103,18 +107,15 @@ function shell_parse(raw::AbstractString, interp::Bool)
103107
update_arg(s[i:end])
104108
append_arg()
105109

106-
if !interp
107-
return (args,last_parse)
108-
end
110+
interpolate || return args, last_parse
109111

110112
# construct an expression
111113
ex = Expr(:tuple)
112114
for arg in args
113115
push!(ex.args, Expr(:tuple, arg...))
114116
end
115-
(ex,last_parse)
117+
return ex, last_parse
116118
end
117-
shell_parse(s::AbstractString) = shell_parse(s,true)
118119

119120
function shell_split(s::AbstractString)
120121
parsed = shell_parse(s,false)[1]
@@ -125,14 +126,14 @@ function shell_split(s::AbstractString)
125126
args
126127
end
127128

128-
function print_shell_word(io::IO, word::AbstractString)
129+
function print_shell_word(io::IO, word::AbstractString, special::AbstractString = shell_special)
129130
if isempty(word)
130131
print(io, "''")
131132
end
132133
has_single = false
133134
has_special = false
134135
for c in word
135-
if isspace(c) || c=='\\' || c=='\'' || c=='"' || c=='$'
136+
if isspace(c) || c=='\\' || c=='\'' || c=='"' || c=='$' || c in special
136137
has_special = true
137138
if c == '\''
138139
has_single = true
@@ -155,13 +156,32 @@ function print_shell_word(io::IO, word::AbstractString)
155156
end
156157
end
157158

158-
function print_shell_escaped(io::IO, cmd::AbstractString, args::AbstractString...)
159-
print_shell_word(io, cmd)
159+
function print_shell_escaped(
160+
io::IO, cmd::AbstractString, args::AbstractString...;
161+
special::AbstractString=shell_special
162+
)
163+
print_shell_word(io, cmd, special)
160164
for arg in args
161165
print(io, ' ')
162-
print_shell_word(io, arg)
166+
print_shell_word(io, arg, special)
163167
end
164168
end
165-
print_shell_escaped(io::IO) = nothing
169+
print_shell_escaped(io::IO; special::String=shell_special) = nothing
170+
171+
"""
172+
shell_escape(args::Union{Cmd,AbstractString...}; special::AbstractString="$shell_special")
173+
174+
The unexported `shell_escape` function is the inverse of the unexported `shell_split` function:
175+
it takes a string or command object and escapes any special characters in such a way that calling
176+
`shell_split` on it would give back the array of words in the original command. The `special`
177+
keyword argument controls what characters in addition to whitespace, backslashes, quotes and
178+
dollar signs are considered to be special. Examples:
179+
180+
julia> Base.shell_escape("echo", "this", "&&", "that")
181+
"echo this '&&' that"
166182
167-
shell_escape(args::AbstractString...) = sprint(print_shell_escaped, args...)
183+
julia> Base.shell_escape("cat", "/foo/bar baz", "&&", "echo", "done", special="")
184+
"cat '/foo/bar baz' && echo done"
185+
"""
186+
shell_escape(args::AbstractString...; special::AbstractString=shell_special) =
187+
sprint(io->print_shell_escaped(io, args..., special=special))

test/replcompletions.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ for (T, arg) in [(String,"\")\""),(Char, "')'")]
289289
@test s[r] == "CompletionFoo.test2"
290290
end
291291

292-
s = "(1, CompletionFoo.test2(`)`,"
292+
s = "(1, CompletionFoo.test2(`')'`,"
293293
c, r, res = test_complete(s)
294294
@test c[1] == string(first(methods(Main.CompletionFoo.test2, Tuple{Cmd})))
295295
@test length(c) == 1

test/spawn.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ end
3232

3333
#### Examples used in the manual ####
3434

35-
@test readstring(`$echo hello | sort`) == "hello | sort\n"
35+
@test readstring(`$echo hello \| sort`) == "hello | sort\n"
3636
@test readstring(pipeline(`$echo hello`, sortcmd)) == "hello\n"
3737
@test length(spawn(pipeline(`$echo hello`, sortcmd)).processes) == 2
3838

0 commit comments

Comments
 (0)