Skip to content

Commit 7d428ea

Browse files
committed
document correct usage for shell_escape_wincmd
Note that most resources online are wrong, and even `cmd /c help cmd` prints the wrong list, so it is important to be clear here about the actual guarantees this function can afford. Refs #38352
1 parent 65382c7 commit 7d428ea

File tree

1 file changed

+62
-13
lines changed

1 file changed

+62
-13
lines changed

base/shell.jl

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -256,19 +256,68 @@ shell_escape_posixly(args::AbstractString...) =
256256
shell_escape_wincmd(s::AbstractString)
257257
shell_escape_wincmd(io::IO, s::AbstractString)
258258
259-
The unexported `shell_escape_wincmd` function escapes Windows
260-
`cmd.exe` shell meta characters. It escapes `()!^<>&|` by placing a
261-
`^` in front. An `@` is only escaped at the start of the string. Pairs
262-
of `"` characters and the strings they enclose are passed through
263-
unescaped. Any remaining `"` is escaped with `^` to ensure that the
264-
number of unescaped `"` characters in the result remains even.
265-
266-
Since `cmd.exe` substitutes variable references (like `%USER%`)
267-
_before_ processing the escape characters `^` and `"`, this function
268-
makes no attempt to escape the percent sign (`%`).
269-
270-
Input strings with ASCII control characters that cannot be escaped
271-
(NUL, CR, LF) will cause an `ArgumentError` exception.
259+
The unexported `shell_escape_wincmd` function escapes Windows `cmd.exe` shell
260+
meta characters. It escapes `()!^<>&|` by placing a `^` in front. An `@` is
261+
only escaped at the start of the string. Pairs of `"` characters and the
262+
strings they enclose are passed through unescaped. Any remaining `"` is escaped
263+
with `^` to ensure that the number of unescaped `"` characters in the result
264+
remains even.
265+
266+
Since `cmd.exe` substitutes variable references (like `%USER%`) _before_
267+
processing the escape characters `^` and `"`, this function makes no attempt to
268+
escape the percent sign (`%`), the presence of `%` in the input may cause
269+
severe breakage, depending on where the result is used.
270+
271+
Input strings with ASCII control characters that cannot be escaped (NUL, CR,
272+
LF) will cause an `ArgumentError` exception.
273+
274+
The result is safe to pass as an argument to a command call being processed by
275+
`CMD.exe /S /C " ... "` (with surrounding double-quote pair) and will be
276+
received verbatim by the target application if the input does not contain `%`
277+
(else this function will fail with an ArgumentError). The presence of `%` in
278+
the input string may result in command injection vulnerabilities and may
279+
invalidate any claim of suitability of the output of this function for use as
280+
an argument to cmd (due to the ordering described above), so use caution when
281+
assembling a string from various sources.
282+
283+
This function may be useful in concert with the `windows_verbatim` flag to
284+
[`Cmd`](@ref) when constructing process pipelines.
285+
286+
```julia
287+
wincmd(c::String) =
288+
run(Cmd(Cmd(["cmd.exe", "/s /c \" \$c \""]);
289+
windows_verbatim=true))
290+
wincmd_echo(s::String) =
291+
wincmd("echo " * Base.shell_escape_wincmd(s))
292+
wincmd_echo("hello \$(ENV["USER"]) & the \"whole\" world! (=^I^=)")
293+
```
294+
295+
But take head that if the input string `s` contains a `%`, the argument list
296+
and echo'ed text may get corrupted, resulting in arbitrary command execution.
297+
The argument can alternatively be passed as an environment variable, which
298+
avoids the problem with `%` and the need for the `windows_verbatim` flag:
299+
300+
```julia
301+
cmdargs = Base.shell_escape_wincmd("Passing args with %cmdargs% works 100%!")
302+
run(setenv(`cmd /C echo %cmdargs%`, "cmdargs" => cmdargs))
303+
```
304+
305+
!warning
306+
The argument parsing done by CMD when calling batch files (either inside
307+
`.bat` files or as arguments to them) is not fully compatible with the
308+
output of this function. In particular, the processing of `%` is different.
309+
310+
!important
311+
Due to a peculiar behavior of the CMD parser/interpreter, each command
312+
after a literal `|` character (indicating a command pipeline) must have
313+
`shell_escape_wincmd` applied twice since it will be parsed twice by CMD.
314+
This implies ENV variables would also be expanded twice!
315+
For example:
316+
```julia
317+
to_print = "All for 1 & 1 for all!"
318+
to_print_esc = Base.shell_escape_wincmd(Base.shell_escape_wincmd(to_print))
319+
run(Cmd(Cmd(["cmd", "/S /C \" break | echo \$(to_print_esc) \""]), windows_verbatim=true))
320+
```
272321
273322
With an I/O stream parameter `io`, the result will be written there,
274323
rather than returned as a string.

0 commit comments

Comments
 (0)