https://github.com/JuliaLang/julia
Tip revision: 5bf5f729dd61c5b30de0b95dcfdd59ee63b1f8c2 authored by Jarrett Revels on 02 November 2018, 15:43:51 UTC
clearer names for a few things
clearer names for a few things
Tip revision: 5bf5f72
shell.jl
# 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...))