• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

JuliaLang / julia / #37527

pending completion
#37527

push

local

web-flow
make `IRShow.method_name` inferrable (#49607)

18 of 18 new or added lines in 3 files covered. (100.0%)

68710 of 81829 relevant lines covered (83.97%)

33068903.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.41
/base/shell.jl
1
# This file is a part of Julia. License is MIT: https://julialang.org/license
2

3
## shell-like command parsing ##
4

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

7
# strips the end but respects the space when the string ends with "\\ "
8
function rstrip_shell(s::AbstractString)
534✔
9
    c_old = nothing
×
10
    for (i, c) in Iterators.reverse(pairs(s))
1,007✔
11
        i::Int; c::AbstractChar
×
12
        ((c == '\\') && c_old == ' ') && return SubString(s, 1, i+1)
484✔
13
        isspace(c) || return SubString(s, 1, i)
955✔
14
        c_old = c
×
15
    end
18✔
16
    SubString(s, 1, 0)
61✔
17
end
18

19
function shell_parse(str::AbstractString, interpolate::Bool=true;
1,561✔
20
                     special::AbstractString="", filename="none")
21
    s = SubString(str, firstindex(str))
534✔
22
    s = rstrip_shell(lstrip(s))
534✔
23

24
    # N.B.: This is used by REPLCompletions
25
    last_parse = 0:-1
×
26
    isempty(s) && return interpolate ? (Expr(:tuple,:()),last_parse) : ([],last_parse)
534✔
27

28
    in_single_quotes = false
×
29
    in_double_quotes = false
×
30

31
    args = []
473✔
32
    arg = []
473✔
33
    i = firstindex(s)
×
34
    st = Iterators.Stateful(pairs(s))
946✔
35

36
    function push_nonempty!(list, x)
991✔
37
        if !isa(x,AbstractString) || !isempty(x)
1,915✔
38
            push!(list, x)
1,296✔
39
        end
40
        return nothing
2,431✔
41
    end
42
    function consume_upto!(list, s, i, j)
1,913✔
43
        push_nonempty!(list, s[i:prevind(s, j)::Int])
1,440✔
44
        something(peek(st), lastindex(s)::Int+1 => '\0').first::Int
1,440✔
45
    end
46
    function append_2to1!(list, innerlist)
1,670✔
47
        if isempty(innerlist); push!(innerlist, ""); end
1,197✔
48
        push!(list, copy(innerlist))
1,197✔
49
        empty!(innerlist)
1,197✔
50
    end
51

52
    C = eltype(str)
×
53
    P = Pair{Int,C}
×
54
    for (j, c) in st
946✔
55
        j, c = j::Int, c::C
×
56
        if !in_single_quotes && !in_double_quotes && isspace(c)
14,120✔
57
            i = consume_upto!(arg, s, i, j)
724✔
58
            append_2to1!(args, arg)
724✔
59
            while !isempty(st)
724✔
60
                # We've made sure above that we don't end in whitespace,
61
                # so updating `i` here is ok
62
                (i, c) = peek(st)::P
724✔
63
                isspace(c) || break
1,448✔
64
                popfirst!(st)
×
65
            end
724✔
66
        elseif interpolate && !in_single_quotes && c == '$'
8,384✔
67
            i = consume_upto!(arg, s, i, j)
518✔
68
            isempty(st) && error("\$ right before end of command")
518✔
69
            stpos, c = popfirst!(st)::P
1,024✔
70
            isspace(c) && error("space not allowed right after \$")
1,036✔
71
            if startswith(SubString(s, stpos), "var\"")
1,004✔
72
                # Disallow var"#" syntax in cmd interpolations.
73
                # TODO: Allow only identifiers after the $ for consistency with
74
                # string interpolation syntax (see #3150)
75
                ex, j = :var, stpos+3
1✔
76
            else
77
                # use parseatom instead of parse to respect filename (#28188)
78
                ex, j = Meta.parseatom(s, stpos, filename=filename)
517✔
79
            end
80
            last_parse = (stpos:prevind(s, j)) .+ s.offset
518✔
81
            push_nonempty!(arg, ex)
518✔
82
            s = SubString(s, j)
518✔
83
            Iterators.reset!(st, pairs(s))
518✔
84
            i = firstindex(s)
518✔
85
        else
86
            if !in_double_quotes && c == '\''
7,866✔
87
                in_single_quotes = !in_single_quotes
66✔
88
                i = consume_upto!(arg, s, i, j)
66✔
89
            elseif !in_single_quotes && c == '"'
7,800✔
90
                in_double_quotes = !in_double_quotes
96✔
91
                i = consume_upto!(arg, s, i, j)
96✔
92
            elseif !in_single_quotes && c == '\\'
7,704✔
93
                if !isempty(st) && (peek(st)::P)[2] in ('\n', '\r')
100✔
94
                    i = consume_upto!(arg, s, i, j) + 1
11✔
95
                    if popfirst!(st)[2] == '\r' && (peek(st)::P)[2] == '\n'
11✔
96
                        i += 1
1✔
97
                        popfirst!(st)
2✔
98
                    end
99
                    while !isempty(st) && (peek(st)::P)[2] in (' ', '\t')
39✔
100
                        i = nextind(str, i)
6✔
101
                        _ = popfirst!(st)
12✔
102
                    end
17✔
103
                elseif in_double_quotes
29✔
104
                    isempty(st) && error("unterminated double quote")
22✔
105
                    k, c′ = peek(st)::P
22✔
106
                    if c′ == '"' || c′ == '$' || c′ == '\\'
40✔
107
                        i = consume_upto!(arg, s, i, j)
18✔
108
                        _ = popfirst!(st)
18✔
109
                    end
110
                else
111
                    isempty(st) && error("dangling backslash")
7✔
112
                    i = consume_upto!(arg, s, i, j)
7✔
113
                    _ = popfirst!(st)
47✔
114
                end
115
            elseif !in_single_quotes && !in_double_quotes && c in special
7,664✔
116
                error("parsing command `$str`: special characters \"$special\" must be quoted in commands")
×
117
            end
118
        end
119
    end
17,743✔
120

121
    if in_single_quotes; error("unterminated single quote"); end
473✔
122
    if in_double_quotes; error("unterminated double quote"); end
473✔
123

124
    push_nonempty!(arg, s[i:end])
473✔
125
    append_2to1!(args, arg)
473✔
126

127
    interpolate || return args, last_parse
514✔
128

129
    # construct an expression
130
    ex = Expr(:tuple)
432✔
131
    for arg in args
432✔
132
        push!(ex.args, Expr(:tuple, arg...))
1,109✔
133
    end
1,541✔
134
    return ex, last_parse
432✔
135
end
136

137
function shell_split(s::AbstractString)
41✔
138
    parsed = shell_parse(s, false)[1]
41✔
139
    args = String[]
41✔
140
    for arg in parsed
41✔
141
        push!(args, string(arg...))
88✔
142
    end
129✔
143
    args
41✔
144
end
145

146
function print_shell_word(io::IO, word::AbstractString, special::AbstractString = "")
96✔
147
    has_single = false
22✔
148
    has_special = false
22✔
149
    for c in word
190✔
150
        if isspace(c) || c=='\\' || c=='\'' || c=='"' || c=='$' || c in special
3,954✔
151
            has_special = true
6✔
152
            if c == '\''
14✔
153
                has_single = true
1✔
154
            end
155
        end
156
    end
3,862✔
157
    if isempty(word)
96✔
158
        print(io, "''")
2✔
159
    elseif !has_special
94✔
160
        print(io, word)
83✔
161
    elseif !has_single
11✔
162
        print(io, '\'', word, '\'')
9✔
163
    else
164
        print(io, '"')
2✔
165
        for c in word
4✔
166
            if c == '"' || c == '$'
12✔
167
                print(io, '\\')
×
168
            end
169
            print(io, c)
6✔
170
        end
10✔
171
        print(io, '"')
2✔
172
    end
173
    nothing
96✔
174
end
175

176
function print_shell_escaped(io::IO, cmd::AbstractString, args::AbstractString...;
6✔
177
                             special::AbstractString="")
178
    print_shell_word(io, cmd, special)
3✔
179
    for arg in args
3✔
180
        print(io, ' ')
19✔
181
        print_shell_word(io, arg, special)
19✔
182
    end
19✔
183
end
184
print_shell_escaped(io::IO; special::String="") = nothing
×
185

186
"""
187
    shell_escape(args::Union{Cmd,AbstractString...}; special::AbstractString="")
188

189
The unexported `shell_escape` function is the inverse of the unexported `shell_split` function:
190
it takes a string or command object and escapes any special characters in such a way that calling
191
`shell_split` on it would give back the array of words in the original command. The `special`
192
keyword argument controls what characters in addition to whitespace, backslashes, quotes and
193
dollar signs are considered to be special (default: none).
194

195
# Examples
196
```jldoctest
197
julia> Base.shell_escape("cat", "/foo/bar baz", "&&", "echo", "done")
198
"cat '/foo/bar baz' && echo done"
199

200
julia> Base.shell_escape("echo", "this", "&&", "that")
201
"echo this && that"
202
```
203
"""
204
shell_escape(args::AbstractString...; special::AbstractString="") =
6✔
205
    sprint((io, args...) -> print_shell_escaped(io, args..., special=special), args...)
3✔
206

207

208
function print_shell_escaped_posixly(io::IO, args::AbstractString...)
2✔
209
    first = true
2✔
210
    for arg in args
2✔
211
        first || print(io, ' ')
14✔
212
        # avoid printing quotes around simple enough strings
213
        # that any (reasonable) shell will definitely never consider them to be special
214
        have_single::Bool = false
14✔
215
        have_double::Bool = false
14✔
216
        function isword(c::AbstractChar)
53✔
217
            if '0' <= c <= '9' || 'a' <= c <= 'z' || 'A' <= c <= 'Z'
78✔
218
                # word characters
219
            elseif c == '_' || c == '/' || c == '+' || c == '-'
14✔
220
                # other common characters
221
            elseif c == '\''
10✔
222
                have_single = true
1✔
223
            elseif c == '"'
9✔
224
                have_double && return false # switch to single quoting
3✔
225
                have_double = true
2✔
226
            elseif !first && c == '='
6✔
227
                # equals is special if it is first (e.g. `env=val ./cmd`)
228
            else
229
                # anything else
230
                return false
5✔
231
            end
232
            return true
33✔
233
        end
234
        if isempty(arg)
14✔
235
            print(io, "''")
1✔
236
        elseif all(isword, arg)
13✔
237
            have_single && (arg = replace(arg, '\'' => "\\'"))
7✔
238
            have_double && (arg = replace(arg, '"' => "\\\""))
7✔
239
            print(io, arg)
7✔
240
        else
241
            print(io, '\'', replace(arg, '\'' => "'\\''"), '\'')
6✔
242
        end
243
        first = false
14✔
244
    end
14✔
245
end
246

247
"""
248
    shell_escape_posixly(args::Union{Cmd,AbstractString...})
249

250
The unexported `shell_escape_posixly` function
251
takes a string or command object and escapes any special characters in such a way that
252
it is safe to pass it as an argument to a posix shell.
253

254
# Examples
255
```jldoctest
256
julia> Base.shell_escape_posixly("cat", "/foo/bar baz", "&&", "echo", "done")
257
"cat '/foo/bar baz' '&&' echo done"
258

259
julia> Base.shell_escape_posixly("echo", "this", "&&", "that")
260
"echo this '&&' that"
261
```
262
"""
263
shell_escape_posixly(args::AbstractString...) =
2✔
264
    sprint(print_shell_escaped_posixly, args...)
265

266
"""
267
    shell_escape_csh(args::Union{Cmd,AbstractString...})
268
    shell_escape_csh(io::IO, args::Union{Cmd,AbstractString...})
269

270
This function quotes any metacharacters in the string arguments such
271
that the string returned can be inserted into a command-line for
272
interpretation by the Unix C shell (csh, tcsh), where each string
273
argument will form one word.
274

275
In contrast to a POSIX shell, csh does not support the use of the
276
backslash as a general escape character in double-quoted strings.
277
Therefore, this function wraps strings that might contain
278
metacharacters in single quotes, except for parts that contain single
279
quotes, which it wraps in double quotes instead. It switches between
280
these types of quotes as needed. Linefeed characters are escaped with
281
a backslash.
282

283
This function should also work for a POSIX shell, except if the input
284
string contains a linefeed (`"\\n"`) character.
285

286
See also: [`shell_escape_posixly`](@ref)
287
"""
288
function shell_escape_csh(io::IO, args::AbstractString...)
×
289
    first = true
×
290
    for arg in args
×
291
        first || write(io, ' ')
×
292
        first = false
×
293
        i = 1
×
294
        while true
×
295
            for (r,e) = (r"^[A-Za-z0-9/\._-]+\z"sa => "",
×
296
                         r"^[^']*\z"sa => "'", r"^[^\$\`\"]*\z"sa => "\"",
×
297
                         r"^[^']+"sa  => "'", r"^[^\$\`\"]+"sa  => "\"")
×
298
                if ((m = match(r, SubString(arg, i))) !== nothing)
×
299
                    write(io, e)
×
300
                    write(io, replace(m.match, '\n' => "\\\n"))
×
301
                    write(io, e)
×
302
                    i += ncodeunits(m.match)
×
303
                    break
×
304
                end
305
            end
×
306
            i <= lastindex(arg) || break
×
307
        end
×
308
    end
×
309
end
310
shell_escape_csh(args::AbstractString...) =
×
311
    sprint(shell_escape_csh, args...;
312
           sizehint = sum(sizeof.(args)) + length(args) * 3)
313

314
"""
315
    shell_escape_wincmd(s::AbstractString)
316
    shell_escape_wincmd(io::IO, s::AbstractString)
317

318
The unexported `shell_escape_wincmd` function escapes Windows `cmd.exe` shell
319
meta characters. It escapes `()!^<>&|` by placing a `^` in front. An `@` is
320
only escaped at the start of the string. Pairs of `"` characters and the
321
strings they enclose are passed through unescaped. Any remaining `"` is escaped
322
with `^` to ensure that the number of unescaped `"` characters in the result
323
remains even.
324

325
Since `cmd.exe` substitutes variable references (like `%USER%`) _before_
326
processing the escape characters `^` and `"`, this function makes no attempt to
327
escape the percent sign (`%`), the presence of `%` in the input may cause
328
severe breakage, depending on where the result is used.
329

330
Input strings with ASCII control characters that cannot be escaped (NUL, CR,
331
LF) will cause an `ArgumentError` exception.
332

333
The result is safe to pass as an argument to a command call being processed by
334
`CMD.exe /S /C " ... "` (with surrounding double-quote pair) and will be
335
received verbatim by the target application if the input does not contain `%`
336
(else this function will fail with an ArgumentError). The presence of `%` in
337
the input string may result in command injection vulnerabilities and may
338
invalidate any claim of suitability of the output of this function for use as
339
an argument to cmd (due to the ordering described above), so use caution when
340
assembling a string from various sources.
341

342
This function may be useful in concert with the `windows_verbatim` flag to
343
[`Cmd`](@ref) when constructing process pipelines.
344

345
```julia
346
wincmd(c::String) =
347
   run(Cmd(Cmd(["cmd.exe", "/s /c \\" \$c \\""]);
348
           windows_verbatim=true))
349
wincmd_echo(s::String) =
350
   wincmd("echo " * Base.shell_escape_wincmd(s))
351
wincmd_echo("hello \$(ENV["USERNAME"]) & the \\"whole\\" world! (=^I^=)")
352
```
353

354
But take note that if the input string `s` contains a `%`, the argument list
355
and echo'ed text may get corrupted, resulting in arbitrary command execution.
356
The argument can alternatively be passed as an environment variable, which
357
avoids the problem with `%` and the need for the `windows_verbatim` flag:
358

359
```julia
360
cmdargs = Base.shell_escape_wincmd("Passing args with %cmdargs% works 100%!")
361
run(setenv(`cmd /C echo %cmdargs%`, "cmdargs" => cmdargs))
362
```
363

364
!!! warning
365
    The argument parsing done by CMD when calling batch files (either inside
366
    `.bat` files or as arguments to them) is not fully compatible with the
367
    output of this function. In particular, the processing of `%` is different.
368

369
!!! important
370
    Due to a peculiar behavior of the CMD parser/interpreter, each command
371
    after a literal `|` character (indicating a command pipeline) must have
372
    `shell_escape_wincmd` applied twice since it will be parsed twice by CMD.
373
    This implies ENV variables would also be expanded twice!
374
    For example:
375
    ```julia
376
    to_print = "All for 1 & 1 for all!"
377
    to_print_esc = Base.shell_escape_wincmd(Base.shell_escape_wincmd(to_print))
378
    run(Cmd(Cmd(["cmd", "/S /C \\" break | echo \$(to_print_esc) \\""]), windows_verbatim=true))
379
    ```
380

381
With an I/O stream parameter `io`, the result will be written there,
382
rather than returned as a string.
383

384
See also [`escape_microsoft_c_args`](@ref), [`shell_escape_posixly`](@ref).
385

386
# Example
387
```jldoctest
388
julia> Base.shell_escape_wincmd("a^\\"^o\\"^u\\"")
389
"a^^\\"^o\\"^^u^\\""
390
```
391
"""
392
function shell_escape_wincmd(io::IO, s::AbstractString)
13✔
393
    # https://stackoverflow.com/a/4095133/1990689
394
    occursin(r"[\r\n\0]"sa, s) &&
13✔
395
        throw(ArgumentError("control character unsupported by CMD.EXE"))
396
    i = 1
10✔
397
    len = ncodeunits(s)
10✔
398
    if len > 0 && s[1] == '@'
18✔
399
        write(io, '^')
1✔
400
    end
401
    while i <= len
197✔
402
        c = s[i]
372✔
403
        if c == '"' && (j = findnext('"', s, nextind(s,i))) !== nothing
187✔
404
            write(io, SubString(s,i,j))
18✔
405
            i = j
18✔
406
        else
407
            if c in ('"', '(', ')', '!', '^', '<', '>', '&', '|')
1,518✔
408
                write(io, '^', c)
33✔
409
            else
410
                write(io, c)
136✔
411
            end
412
        end
413
        i = nextind(s,i)
187✔
414
    end
197✔
415
end
416
shell_escape_wincmd(s::AbstractString) = sprint(shell_escape_wincmd, s;
13✔
417
                                                sizehint = 2*sizeof(s))
418

419
"""
420
    escape_microsoft_c_args(args::Union{Cmd,AbstractString...})
421
    escape_microsoft_c_args(io::IO, args::Union{Cmd,AbstractString...})
422

423
Convert a collection of string arguments into a string that can be
424
passed to many Windows command-line applications.
425

426
Microsoft Windows passes the entire command line as a single string to
427
the application (unlike POSIX systems, where the shell splits the
428
command line into a list of arguments). Many Windows API applications
429
(including julia.exe), use the conventions of the [Microsoft C/C++
430
runtime](https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments)
431
to split that command line into a list of strings.
432

433
This function implements an inverse for a parser compatible with these rules.
434
It joins command-line arguments to be passed to a Windows
435
C/C++/Julia application into a command line, escaping or quoting the
436
meta characters space, TAB, double quote and backslash where needed.
437

438
See also [`shell_escape_wincmd`](@ref), [`escape_raw_string`](@ref).
439
"""
440
function escape_microsoft_c_args(io::IO, args::AbstractString...)
22✔
441
    # http://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULES
442
    first = true
22✔
443
    for arg in args
22✔
444
        if first
41✔
445
            first = false
22✔
446
        else
447
            write(io, ' ')  # separator
19✔
448
        end
449
        if isempty(arg) || occursin(r"[ \t\"]"sa, arg)
80✔
450
            # Julia raw strings happen to use the same escaping convention
451
            # as the argv[] parser in Microsoft's C runtime library.
452
            write(io, '"')
23✔
453
            escape_raw_string(io, arg)
23✔
454
            write(io, '"')
23✔
455
        else
456
            write(io, arg)
18✔
457
        end
458
    end
41✔
459
end
460
escape_microsoft_c_args(args::AbstractString...) =
22✔
461
    sprint(escape_microsoft_c_args, args...;
462
           sizehint = (sum(sizeof.(args)) + 3*length(args)))
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc