Skip to content

Commit f0ab5bb

Browse files
musmmgkuhnvtjnash
authored andcommitted
Add winsomely that converts args to a command line (#33465)
Converts the collection of strings 'args' into a Windows command line. Co-authored-by: Mustafa M. <[email protected]> Co-authored-by: Markus Kuhn <[email protected]> Co-authored-by: Jameson Nash <[email protected]>
1 parent cf5957e commit f0ab5bb

File tree

3 files changed

+146
-0
lines changed

3 files changed

+146
-0
lines changed

base/cmd.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ shell_escape(cmd::Cmd; special::AbstractString="") =
102102
shell_escape(cmd.exec..., special=special)
103103
shell_escape_posixly(cmd::Cmd) =
104104
shell_escape_posixly(cmd.exec...)
105+
shell_escape_winsomely(cmd::Cmd) =
106+
shell_escape_winsomely(cmd.exec...)
105107

106108
function show(io::IO, cmd::Cmd)
107109
print_env = cmd.env !== nothing

base/shell.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,62 @@ julia> Base.shell_escape_posixly("echo", "this", "&&", "that")
253253
"""
254254
shell_escape_posixly(args::AbstractString...) =
255255
sprint(print_shell_escaped_posixly, args...)
256+
257+
258+
function print_shell_escaped_winsomely(io::IO, args::AbstractString...)
259+
first = true
260+
for arg in args
261+
first || write(io, ' ')
262+
first = false
263+
# Quote any arg that contains a whitespace (' ' or '\t') or a double quote mark '"'.
264+
# It's also valid to quote an arg with just a whitespace,
265+
# but the following may be 'safer', and both implementations are valid anyways.
266+
quotes = any(c -> c in (' ', '\t', '"'), arg) || isempty(arg)
267+
quotes && write(io, '"')
268+
backslashes = 0
269+
for c in arg
270+
if c == '\\'
271+
backslashes += 1
272+
else
273+
# escape all backslashes and the following double quote
274+
c == '"' && (backslashes = backslashes * 2 + 1)
275+
for j = 1:backslashes
276+
# backslashes aren't special here
277+
write(io, '\\')
278+
end
279+
backslashes = 0
280+
write(io, c)
281+
end
282+
end
283+
# escape all backslashes, letting the terminating double quote we add below to then be interpreted as a special char
284+
quotes && (backslashes *= 2)
285+
for j = 1:backslashes
286+
write(io, '\\')
287+
end
288+
quotes && write(io, '"')
289+
end
290+
return nothing
291+
end
292+
293+
294+
"""
295+
shell_escaped_winsomely(args::Union{Cmd,AbstractString...})::String
296+
297+
Convert the collection of strings `args` into single string suitable for passing as the argument
298+
string for a Windows command line. Windows passes the entire command line as a single string to
299+
the application (unlike POSIX systems, where the list of arguments are passed separately).
300+
Many Windows API applications (including julia.exe), use the conventions of the [Microsoft C
301+
runtime](https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments) to
302+
split that command line into a list of strings. This function implements the inverse of such a
303+
C runtime command-line parser. It joins command-line arguments to be passed to a Windows console
304+
application into a command line, escaping or quoting meta characters such as space,
305+
double quotes and backslash where needed. This may be useful in concert with the `windows_verbatim`
306+
flag to [`Cmd`](@ref) when constructing process pipelines.
307+
308+
# Example
309+
```jldoctest
310+
julia> println(shell_escaped_winsomely("A B\\", "C"))
311+
"A B\\" C
312+
"""
313+
shell_escape_winsomely(args::AbstractString...) =
314+
sprint(print_shell_escaped_winsomely, args..., sizehint=(sum(length, args)) + 3*length(args))

test/spawn.jl

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,3 +675,88 @@ end
675675
if Sys.iswindows()
676676
rm(busybox, force=true)
677677
end
678+
679+
680+
# shell escaping on Windows
681+
@testset "shell_escape_winsomely" begin
682+
# Note argument A can be parsed both as A or "A".
683+
# We do not test that the parsing satisfies either of these conditions.
684+
# In other words, tests may fail even for valid parsing.
685+
# This is done to avoid overly verbose tests.
686+
687+
# input :
688+
# output: ""
689+
@test Base.shell_escape_winsomely("") == "\"\""
690+
691+
@test Base.shell_escape_winsomely("A") == "A"
692+
693+
@test Base.shell_escape_winsomely(`A`) == "A"
694+
695+
# input : hello world
696+
# output: "hello world"
697+
@test Base.shell_escape_winsomely("hello world") == "\"hello world\""
698+
699+
# input : hello world
700+
# output: "hello world"
701+
@test Base.shell_escape_winsomely("hello\tworld") == "\"hello\tworld\""
702+
703+
# input : hello"world
704+
# output: "hello\"world" (also valid) hello\"world
705+
@test Base.shell_escape_winsomely("hello\"world") == "\"hello\\\"world\""
706+
707+
# input : hello""world
708+
# output: "hello\"\"world" (also valid) hello\"\"world
709+
@test Base.shell_escape_winsomely("hello\"\"world") == "\"hello\\\"\\\"world\""
710+
711+
# input : hello\world
712+
# output: hello\world
713+
@test Base.shell_escape_winsomely("hello\\world") == "hello\\world"
714+
715+
# input : hello\\world
716+
# output: hello\\world
717+
@test Base.shell_escape_winsomely("hello\\\\world") == "hello\\\\world"
718+
719+
# input : hello\"world
720+
# output: "hello\"world" (also valid) hello\"world
721+
@test Base.shell_escape_winsomely("hello\\\"world") == "\"hello\\\\\\\"world\""
722+
723+
# input : hello\\"world
724+
# output: "hello\\\\\"world" (also valid) hello\\\\\"world
725+
@test Base.shell_escape_winsomely("hello\\\\\"world") == "\"hello\\\\\\\\\\\"world\""
726+
727+
# input : hello world\
728+
# output: "hello world\\"
729+
@test Base.shell_escape_winsomely("hello world\\") == "\"hello world\\\\\""
730+
731+
# input : A\B
732+
# output: A\B"
733+
@test Base.shell_escape_winsomely("A\\B") == "A\\B"
734+
735+
# input : [A\, B]
736+
# output: "A\ B"
737+
@test Base.shell_escape_winsomely("A\\", "B") == "A\\ B"
738+
739+
# input : A"B
740+
# output: "A\"B"
741+
@test Base.shell_escape_winsomely("A\"B") == "\"A\\\"B\""
742+
743+
# input : [A B\, C]
744+
# output: "A B\\" C
745+
@test Base.shell_escape_winsomely("A B\\", "C") == "\"A B\\\\\" C"
746+
747+
# input : [A "B, C]
748+
# output: "A \"B" C
749+
@test Base.shell_escape_winsomely("A \"B", "C") == "\"A \\\"B\" C"
750+
751+
# input : [A B\, C]
752+
# output: "A B\\" C
753+
@test Base.shell_escape_winsomely("A B\\", "C") == "\"A B\\\\\" C"
754+
755+
# input :[A\ B\, C]
756+
# output: "A\ B\\" C
757+
@test Base.shell_escape_winsomely("A\\ B\\", "C") == "\"A\\ B\\\\\" C"
758+
759+
# input : [A\ B\, C, D K]
760+
# output: "A\ B\\" C "D K"
761+
@test Base.shell_escape_winsomely("A\\ B\\", "C", "D K") == "\"A\\ B\\\\\" C \"D K\""
762+
end

0 commit comments

Comments
 (0)