Skip to content

Commit 2e1fb5e

Browse files
authored
Merge pull request #26102 from JuliaLang/kf/shell_parse
Get rid of raw iteration protocol use in cmd parsing
2 parents abd2133 + 90d0ff5 commit 2e1fb5e

File tree

5 files changed

+71
-66
lines changed

5 files changed

+71
-66
lines changed

base/iterators.jl

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,18 @@ mutable struct Stateful{T, VS}
10521052
end
10531053
end
10541054

1055+
function reset!(s::Stateful{T,VS}, itr::T) where {T,VS}
1056+
s.itr = itr
1057+
state = start(itr)
1058+
if done(itr, state)
1059+
s.nextvalstate = nothing
1060+
else
1061+
s.nextvalstate = next(itr, state)::VS
1062+
end
1063+
s.taken = 0
1064+
s
1065+
end
1066+
10551067
# Try to find an appropriate type for the (value, state tuple),
10561068
# by doing a recursive unrolling of the iteration protocol up to
10571069
# fixpoint.

base/shell.jl

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,18 @@
44

55
const shell_special = "#{}()[]<>|&*?~;"
66

7+
# strips the end but respects the space when the string ends with "\\ "
8+
function rstrip_shell(s::AbstractString)
9+
c_old = nothing
10+
for (i, c) in Iterators.reverse(pairs(s))
11+
((c == '\\') && c_old == ' ') && return SubString(s, 1, i+1)
12+
c in _default_delims || return SubString(s, 1, i)
13+
c_old = c
14+
end
15+
SubString(s, 1, 0)
16+
end
17+
18+
719
# needs to be factored out so depwarn only warns once
820
# when removed, also need to update shell_escape for a Cmd to pass shell_special
921
# and may want to use it in the test for #10120 (currently the implementation is essentially copied there)
@@ -12,24 +24,10 @@ const shell_special = "#{}()[]<>|&*?~;"
1224

1325
function shell_parse(str::AbstractString, interpolate::Bool=true;
1426
special::AbstractString="")
15-
s = lstrip(str)
16-
# strips the end but respects the space when the string ends with "\\ "
17-
r = reverse(s)
18-
i = start(r)
19-
c_old = nothing
20-
while !done(r,i)
21-
c, j = next(r,i)
22-
if c == '\\' && c_old == ' '
23-
i -= 1
24-
break
25-
elseif !(c in _default_delims)
26-
break
27-
end
28-
i = j
29-
c_old = c
30-
end
31-
s = s[1:end-i+1]
27+
s::SubString = SubString(str, firstindex(str))
28+
s = rstrip_shell(lstrip(s))
3229

30+
# N.B.: This is used by REPLCompletions
3331
last_parse = 0:-1
3432
isempty(s) && return interpolate ? (Expr(:tuple,:()),last_parse) : ([],last_parse)
3533

@@ -38,73 +36,69 @@ function shell_parse(str::AbstractString, interpolate::Bool=true;
3836

3937
args::Vector{Any} = []
4038
arg::Vector{Any} = []
41-
i = start(s)
42-
j = i
39+
i = firstindex(s)
40+
st = Iterators.Stateful(pairs(s))
4341

4442
function update_arg(x)
4543
if !isa(x,AbstractString) || !isempty(x)
4644
push!(arg, x)
4745
end
4846
end
47+
function consume_upto(j)
48+
update_arg(s[i:prevind(s, j)])
49+
i = coalesce(peek(st), (lastindex(s)+1,'\0'))[1]
50+
end
4951
function append_arg()
5052
if isempty(arg); arg = Any["",]; end
5153
push!(args, arg)
5254
arg = []
5355
end
5456

55-
while !done(s,j)
56-
c, k = next(s,j)
57+
for (j, c) in st
5758
if !in_single_quotes && !in_double_quotes && isspace(c)
58-
update_arg(s[i:prevind(s, j)])
59+
consume_upto(j)
5960
append_arg()
60-
j = k
61-
while !done(s,j)
62-
c, k = next(s,j)
63-
if !isspace(c)
64-
i = j
65-
break
66-
end
67-
j = k
61+
while !isempty(st)
62+
# We've made sure above that we don't end in whitespace,
63+
# so updateing `i` here is ok
64+
(i, c) = peek(st)
65+
isspace(c) || break
66+
popfirst!(st)
6867
end
6968
elseif interpolate && !in_single_quotes && c == '$'
70-
update_arg(s[i:prevind(s, j)]); i = k; j = k
71-
if done(s,k)
72-
error("\$ right before end of command")
73-
end
74-
if isspace(s[k])
75-
error("space not allowed right after \$")
76-
end
77-
stpos = j
78-
ex, j = Meta.parse(s,j,greedy=false)
79-
last_parse = stpos:j
80-
update_arg(ex); i = j
69+
consume_upto(j)
70+
isempty(st) && error("\$ right before end of command")
71+
stpos, c = popfirst!(st)
72+
isspace(c) && error("space not allowed right after \$")
73+
ex, j = Meta.parse(s,stpos,greedy=false)
74+
last_parse = (stpos:prevind(s, j)) .+ s.offset
75+
update_arg(ex);
76+
s = SubString(s, j)
77+
Iterators.reset!(st, pairs(s))
78+
i = firstindex(s)
8179
else
8280
if !in_double_quotes && c == '\''
8381
in_single_quotes = !in_single_quotes
84-
update_arg(s[i:prevind(s, j)]); i = k
82+
consume_upto(j)
8583
elseif !in_single_quotes && c == '"'
8684
in_double_quotes = !in_double_quotes
87-
update_arg(s[i:prevind(s, j)]); i = k
85+
consume_upto(j)
8886
elseif c == '\\'
8987
if in_double_quotes
90-
if done(s,k)
91-
error("unterminated double quote")
92-
end
93-
if s[k] == '"' || s[k] == '$' || s[k] == '\\'
94-
update_arg(s[i:prevind(s, j)]); i = k
95-
c, k = next(s,k)
88+
isempty(st) && error("unterminated double quote")
89+
k, c′ = peek(st)
90+
if c′ == '"' || c′ == '$' || c′ == '\\'
91+
consume_upto(j)
92+
_ = popfirst!(st)
9693
end
9794
elseif !in_single_quotes
98-
if done(s,k)
99-
error("dangling backslash")
100-
end
101-
update_arg(s[i:prevind(s, j)]); i = k
102-
c, k = next(s,k)
95+
isempty(st) && error("dangling backslash")
96+
consume_upto(j)
97+
_ = popfirst!(st)
10398
end
10499
elseif !in_single_quotes && !in_double_quotes && c in special
105100
warn_shell_special(special) # noinline depwarn
106101
end
107-
j = k
108102
end
109103
end
110104

base/show.jl

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -766,15 +766,10 @@ const expr_parens = Dict(:tuple=>('(',')'), :vcat=>('[',']'),
766766
is_id_start_char(c::Char) = ccall(:jl_id_start_char, Cint, (UInt32,), c) != 0
767767
is_id_char(c::Char) = ccall(:jl_id_char, Cint, (UInt32,), c) != 0
768768
function isidentifier(s::AbstractString)
769-
i = start(s)
770-
done(s, i) && return false
771-
(c, i) = next(s, i)
769+
isempty(s) && return false
770+
c, rest = Iterators.peel(s)
772771
is_id_start_char(c) || return false
773-
while !done(s, i)
774-
(c, i) = next(s, i)
775-
is_id_char(c) || return false
776-
end
777-
return true
772+
return all(is_id_char, rest)
778773
end
779774
isidentifier(s::Symbol) = isidentifier(string(s))
780775

stdlib/REPL/src/REPLCompletions.jl

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,9 @@ function shell_completions(string, pos)
646646

647647
return complete_path(prefix, pos, use_envpath=use_envpath)
648648
elseif isexpr(arg, :incomplete) || isexpr(arg, :error)
649-
r = first(last_parse):prevind(last_parse, last(last_parse))
650-
partial = scs[r]
649+
partial = scs[last_parse]
651650
ret, range = completions(partial, lastindex(partial))
652-
range = range .+ (first(r) - 1)
651+
range = range .+ (first(last_parse) - 1)
653652
return ret, range, true
654653
end
655654
return String[], 0:-1, false

test/spawn.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,3 +527,8 @@ let p = spawn(`$sleepcmd 100`)
527527
# Should not throw if already dead
528528
kill(p)
529529
end
530+
531+
# Second argument of shell_parse
532+
let s = " \$abc "
533+
@test s[Base.shell_parse(s)[2]] == "abc"
534+
end

0 commit comments

Comments
 (0)