1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
|
# This file is a part of Julia. License is MIT: https://julialang.org/license
## shell-like command parsing ##
const shell_special = "#{}()[]<>|&*?~;"
# strips the end but respects the space when the string ends with "\\ "
function rstrip_shell(s::AbstractString)
c_old = nothing
for (i, c) in Iterators.reverse(pairs(s))
((c == '\\') && c_old == ' ') && return SubString(s, 1, i+1)
isspace(c) || return SubString(s, 1, i)
c_old = c
end
SubString(s, 1, 0)
end
# needs to be factored out so depwarn only warns once
# when removed, also need to update shell_escape for a Cmd to pass shell_special
# and may want to use it in the test for #10120 (currently the implementation is essentially copied there)
@noinline warn_shell_special(special) =
depwarn("special characters \"$special\" should now be quoted in commands", :warn_shell_special)
function shell_parse(str::AbstractString, interpolate::Bool=true;
special::AbstractString="")
s::SubString = SubString(str, firstindex(str))
s = rstrip_shell(lstrip(s))
# N.B.: This is used by REPLCompletions
last_parse = 0:-1
isempty(s) && return interpolate ? (Expr(:tuple,:()),last_parse) : ([],last_parse)
in_single_quotes = false
in_double_quotes = false
args::Vector{Any} = []
arg::Vector{Any} = []
i = firstindex(s)
st = Iterators.Stateful(pairs(s))
function update_arg(x)
if !isa(x,AbstractString) || !isempty(x)
push!(arg, x)
end
end
function consume_upto(j)
update_arg(s[i:prevind(s, j)])
i = something(peek(st), (lastindex(s)+1,'\0'))[1]
end
function append_arg()
if isempty(arg); arg = Any["",]; end
push!(args, arg)
arg = []
end
for (j, c) in st
if !in_single_quotes && !in_double_quotes && isspace(c)
consume_upto(j)
append_arg()
while !isempty(st)
# We've made sure above that we don't end in whitespace,
# so updating `i` here is ok
(i, c) = peek(st)
isspace(c) || break
popfirst!(st)
end
elseif interpolate && !in_single_quotes && c == '$'
consume_upto(j)
isempty(st) && error("\$ right before end of command")
stpos, c = popfirst!(st)
isspace(c) && error("space not allowed right after \$")
ex, j = Meta.parse(s,stpos,greedy=false)
last_parse = (stpos:prevind(s, j)) .+ s.offset
update_arg(ex);
s = SubString(s, j)
Iterators.reset!(st, pairs(s))
i = firstindex(s)
else
if !in_double_quotes && c == '\''
in_single_quotes = !in_single_quotes
consume_upto(j)
elseif !in_single_quotes && c == '"'
in_double_quotes = !in_double_quotes
consume_upto(j)
elseif c == '\\'
if in_double_quotes
isempty(st) && error("unterminated double quote")
k, c′ = peek(st)
if c′ == '"' || c′ == '$' || c′ == '\\'
consume_upto(j)
_ = popfirst!(st)
end
elseif !in_single_quotes
isempty(st) && error("dangling backslash")
consume_upto(j)
_ = popfirst!(st)
end
elseif !in_single_quotes && !in_double_quotes && c in special
warn_shell_special(special) # noinline depwarn
end
end
end
if in_single_quotes; error("unterminated single quote"); end
if in_double_quotes; error("unterminated double quote"); end
update_arg(s[i:end])
append_arg()
interpolate || return args, last_parse
# construct an expression
ex = Expr(:tuple)
for arg in args
push!(ex.args, Expr(:tuple, arg...))
end
return ex, last_parse
end
function shell_split(s::AbstractString)
parsed = shell_parse(s, false)[1]
args = String[]
for arg in parsed
push!(args, string(arg...))
end
args
end
function print_shell_word(io::IO, word::AbstractString, special::AbstractString = "")
if isempty(word)
print(io, "''")
end
has_single = false
has_special = false
for c in word
if isspace(c) || c=='\\' || c=='\'' || c=='"' || c=='$' || c in special
has_special = true
if c == '\''
has_single = true
end
end
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, '"')
end
end
function print_shell_escaped(io::IO, cmd::AbstractString, args::AbstractString...;
special::AbstractString="")
print_shell_word(io, cmd, special)
for arg in args
print(io, ' ')
print_shell_word(io, arg, special)
end
end
print_shell_escaped(io::IO; special::String="") = nothing
"""
shell_escape(args::Union{Cmd,AbstractString...}; special::AbstractString="")
The unexported `shell_escape` function is the inverse of the unexported `shell_split` function:
it takes a string or command object and escapes any special characters in such a way that calling
`shell_split` on it would give back the array of words in the original command. The `special`
keyword argument controls what characters in addition to whitespace, backslashes, quotes and
dollar signs are considered to be special (default: none).
# Examples
```jldoctest
julia> Base.shell_escape("cat", "/foo/bar baz", "&&", "echo", "done")
"cat '/foo/bar baz' && echo done"
julia> Base.shell_escape("echo", "this", "&&", "that")
"echo this && that"
```
"""
shell_escape(args::AbstractString...; special::AbstractString="") =
sprint(io->print_shell_escaped(io, args..., special=special))
function print_shell_escaped_posixly(io::IO, args::AbstractString...)
first = true
for arg in args
first || print(io, ' ')
# avoid printing quotes around simple enough strings
# that any (reasonable) shell will definitely never consider them to be special
have_single = false
have_double = false
function isword(c::AbstractChar)
if '0' <= c <= '9' || 'a' <= c <= 'z' || 'A' <= c <= 'Z'
# word characters
elseif c == '_' || c == '/' || c == '+' || c == '-'
# other common characters
elseif c == '\''
have_single = true
elseif c == '"'
have_double && return false # switch to single quoting
have_double = true
elseif !first && c == '='
# equals is special if it is first (e.g. `env=val ./cmd`)
else
# anything else
return false
end
return true
end
if all(isword, arg)
have_single && (arg = replace(arg, '\'' => "\\'"))
have_double && (arg = replace(arg, '"' => "\\\""))
print(io, arg)
else
print(io, '\'', replace(arg, '\'' => "'\\''"), '\'')
end
first = false
end
end
"""
shell_escape_posixly(args::Union{Cmd,AbstractString...})
The unexported `shell_escape_posixly` function
takes a string or command object and escapes any special characters in such a way that
it is safe to pass it as an argument to a posix shell.
# Examples
```jldoctest
julia> Base.shell_escape_posixly("cat", "/foo/bar baz", "&&", "echo", "done")
"cat '/foo/bar baz' '&&' echo done"
julia> Base.shell_escape_posixly("echo", "this", "&&", "that")
"echo this '&&' that"
```
"""
shell_escape_posixly(args::AbstractString...) =
sprint(io->print_shell_escaped_posixly(io, args...))
|