Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add winsomely that converts args to a command line #33465

Merged
merged 2 commits into from
Oct 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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...)
musm marked this conversation as resolved.
Show resolved Hide resolved
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having more time to think about this. I think we should remove " from this list.

Unless we're told otherwise, don't quote unless we actually need to do so - hopefully avoid problems if programs won't parse quotes properly

we should also add a ; force = false option that will indeed quote everything if needed.

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