Skip to content

Commit

Permalink
Add winsomely that converts args to a command line (#33465)
Browse files Browse the repository at this point in the history
Converts the collection of strings 'args' into a Windows command line.

Co-authored-by:    Mustafa M. <mus-m@outlook.com>
Co-authored-by:    Markus Kuhn <mgk25@cl.cam.ac.uk>
Co-authored-by:    Jameson Nash <vtjnash+github@gmail.com>
  • Loading branch information
3 people authored and StefanKarpinski committed Oct 11, 2019
1 parent cf5957e commit f0ab5bb
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 0 deletions.
2 changes: 2 additions & 0 deletions base/cmd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ shell_escape(cmd::Cmd; special::AbstractString="") =
shell_escape(cmd.exec..., special=special)
shell_escape_posixly(cmd::Cmd) =
shell_escape_posixly(cmd.exec...)
shell_escape_winsomely(cmd::Cmd) =
shell_escape_winsomely(cmd.exec...)

function show(io::IO, cmd::Cmd)
print_env = cmd.env !== nothing
Expand Down
59 changes: 59 additions & 0 deletions base/shell.jl
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,62 @@ julia> Base.shell_escape_posixly("echo", "this", "&&", "that")
"""
shell_escape_posixly(args::AbstractString...) =
sprint(print_shell_escaped_posixly, args...)


function print_shell_escaped_winsomely(io::IO, args::AbstractString...)
first = true
for arg in args
first || write(io, ' ')
first = false
# Quote any arg that contains a whitespace (' ' or '\t') or a double quote mark '"'.
# It's also valid to quote an arg with just a whitespace,
# but the following may be 'safer', and both implementations are valid anyways.
quotes = any(c -> c in (' ', '\t', '"'), arg) || isempty(arg)
quotes && write(io, '"')
backslashes = 0
for c in arg
if c == '\\'
backslashes += 1
else
# escape all backslashes and the following double quote
c == '"' && (backslashes = backslashes * 2 + 1)
for j = 1:backslashes
# backslashes aren't special here
write(io, '\\')
end
backslashes = 0
write(io, c)
end
end
# escape all backslashes, letting the terminating double quote we add below to then be interpreted as a special char
quotes && (backslashes *= 2)
for j = 1:backslashes
write(io, '\\')
end
quotes && write(io, '"')
end
return nothing
end


"""
shell_escaped_winsomely(args::Union{Cmd,AbstractString...})::String
Convert the collection of strings `args` into single string suitable for passing as the argument
string for a Windows command line. Windows passes the entire command line as a single string to
the application (unlike POSIX systems, where the list of arguments are passed separately).
Many Windows API applications (including julia.exe), use the conventions of the [Microsoft C
runtime](https://docs.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments) to
split that command line into a list of strings. This function implements the inverse of such a
C runtime command-line parser. It joins command-line arguments to be passed to a Windows console
application into a command line, escaping or quoting meta characters such as space,
double quotes and backslash where needed. This may be useful in concert with the `windows_verbatim`
flag to [`Cmd`](@ref) when constructing process pipelines.
# Example
```jldoctest
julia> println(shell_escaped_winsomely("A B\\", "C"))
"A B\\" C
"""
shell_escape_winsomely(args::AbstractString...) =
sprint(print_shell_escaped_winsomely, args..., sizehint=(sum(length, args)) + 3*length(args))
85 changes: 85 additions & 0 deletions test/spawn.jl
Original file line number Diff line number Diff line change
Expand Up @@ -675,3 +675,88 @@ end
if Sys.iswindows()
rm(busybox, force=true)
end


# shell escaping on Windows
@testset "shell_escape_winsomely" begin
# Note argument A can be parsed both as A or "A".
# We do not test that the parsing satisfies either of these conditions.
# In other words, tests may fail even for valid parsing.
# This is done to avoid overly verbose tests.

# input :
# output: ""
@test Base.shell_escape_winsomely("") == "\"\""

@test Base.shell_escape_winsomely("A") == "A"

@test Base.shell_escape_winsomely(`A`) == "A"

# input : hello world
# output: "hello world"
@test Base.shell_escape_winsomely("hello world") == "\"hello world\""

# input : hello world
# output: "hello world"
@test Base.shell_escape_winsomely("hello\tworld") == "\"hello\tworld\""

# input : hello"world
# output: "hello\"world" (also valid) hello\"world
@test Base.shell_escape_winsomely("hello\"world") == "\"hello\\\"world\""

# input : hello""world
# output: "hello\"\"world" (also valid) hello\"\"world
@test Base.shell_escape_winsomely("hello\"\"world") == "\"hello\\\"\\\"world\""

# input : hello\world
# output: hello\world
@test Base.shell_escape_winsomely("hello\\world") == "hello\\world"

# input : hello\\world
# output: hello\\world
@test Base.shell_escape_winsomely("hello\\\\world") == "hello\\\\world"

# input : hello\"world
# output: "hello\"world" (also valid) hello\"world
@test Base.shell_escape_winsomely("hello\\\"world") == "\"hello\\\\\\\"world\""

# input : hello\\"world
# output: "hello\\\\\"world" (also valid) hello\\\\\"world
@test Base.shell_escape_winsomely("hello\\\\\"world") == "\"hello\\\\\\\\\\\"world\""

# input : hello world\
# output: "hello world\\"
@test Base.shell_escape_winsomely("hello world\\") == "\"hello world\\\\\""

# input : A\B
# output: A\B"
@test Base.shell_escape_winsomely("A\\B") == "A\\B"

# input : [A\, B]
# output: "A\ B"
@test Base.shell_escape_winsomely("A\\", "B") == "A\\ B"

# input : A"B
# output: "A\"B"
@test Base.shell_escape_winsomely("A\"B") == "\"A\\\"B\""

# input : [A B\, C]
# output: "A B\\" C
@test Base.shell_escape_winsomely("A B\\", "C") == "\"A B\\\\\" C"

# input : [A "B, C]
# output: "A \"B" C
@test Base.shell_escape_winsomely("A \"B", "C") == "\"A \\\"B\" C"

# input : [A B\, C]
# output: "A B\\" C
@test Base.shell_escape_winsomely("A B\\", "C") == "\"A B\\\\\" C"

# input :[A\ B\, C]
# output: "A\ B\\" C
@test Base.shell_escape_winsomely("A\\ B\\", "C") == "\"A\\ B\\\\\" C"

# input : [A\ B\, C, D K]
# output: "A\ B\\" C "D K"
@test Base.shell_escape_winsomely("A\\ B\\", "C", "D K") == "\"A\\ B\\\\\" C \"D K\""
end

0 comments on commit f0ab5bb

Please sign in to comment.