194 lines
5.7 KiB
Julia
194 lines
5.7 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
## shell-like command parsing ##
|
|
|
|
const shell_special = "#{}()[]<>|&*?~;"
|
|
|
|
# needs to be factored out so depwarn only warns once
|
|
@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 = lstrip(str)
|
|
# strips the end but respects the space when the string ends with "\\ "
|
|
r = RevString(s)
|
|
i = start(r)
|
|
c_old = nothing
|
|
while !done(r,i)
|
|
c, j = next(r,i)
|
|
if c == '\\' && c_old == ' '
|
|
i -= 1
|
|
break
|
|
elseif !(c in _default_delims)
|
|
break
|
|
end
|
|
i = j
|
|
c_old = c
|
|
end
|
|
s = s[1:end-i+1]
|
|
|
|
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 = start(s)
|
|
j = i
|
|
|
|
function update_arg(x)
|
|
if !isa(x,AbstractString) || !isempty(x)
|
|
push!(arg, x)
|
|
end
|
|
end
|
|
function append_arg()
|
|
if isempty(arg); arg = Any["",]; end
|
|
push!(args, arg)
|
|
arg = []
|
|
end
|
|
|
|
while !done(s,j)
|
|
c, k = next(s,j)
|
|
if !in_single_quotes && !in_double_quotes && isspace(c)
|
|
update_arg(s[i:j-1])
|
|
append_arg()
|
|
j = k
|
|
while !done(s,j)
|
|
c, k = next(s,j)
|
|
if !isspace(c)
|
|
i = j
|
|
break
|
|
end
|
|
j = k
|
|
end
|
|
elseif interpolate && !in_single_quotes && c == '$'
|
|
update_arg(s[i:j-1]); i = k; j = k
|
|
if done(s,k)
|
|
error("\$ right before end of command")
|
|
end
|
|
if isspace(s[k])
|
|
error("space not allowed right after \$")
|
|
end
|
|
stpos = j
|
|
ex, j = parse(s,j,greedy=false)
|
|
last_parse = stpos:j
|
|
update_arg(ex); i = j
|
|
else
|
|
if !in_double_quotes && c == '\''
|
|
in_single_quotes = !in_single_quotes
|
|
update_arg(s[i:j-1]); i = k
|
|
elseif !in_single_quotes && c == '"'
|
|
in_double_quotes = !in_double_quotes
|
|
update_arg(s[i:j-1]); i = k
|
|
elseif c == '\\'
|
|
if in_double_quotes
|
|
if done(s,k)
|
|
error("unterminated double quote")
|
|
end
|
|
if s[k] == '"' || s[k] == '$' || s[k] == '\\'
|
|
update_arg(s[i:j-1]); i = k
|
|
c, k = next(s,k)
|
|
end
|
|
elseif !in_single_quotes
|
|
if done(s,k)
|
|
error("dangling backslash")
|
|
end
|
|
update_arg(s[i:j-1]); i = k
|
|
c, k = next(s,k)
|
|
end
|
|
elseif !in_single_quotes && !in_double_quotes && c in special
|
|
warn_shell_special(special) # noinline depwarn
|
|
end
|
|
j = k
|
|
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))
|