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 platform-specific variants of Process.parse_arguments #12278

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
78 changes: 61 additions & 17 deletions spec/std/process_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -442,31 +442,75 @@ describe Process do
end
end

describe "parse_arguments" do
it { Process.parse_arguments("").should eq(%w[]) }
it { Process.parse_arguments(" ").should eq(%w[]) }
it { Process.parse_arguments("foo").should eq(%w[foo]) }
it { Process.parse_arguments("foo bar").should eq(%w[foo bar]) }
it { Process.parse_arguments(%q("foo bar" 'foo bar' baz)).should eq(["foo bar", "foo bar", "baz"]) }
it { Process.parse_arguments(%q("foo bar"'foo bar'baz)).should eq(["foo barfoo barbaz"]) }
it { Process.parse_arguments(%q(foo\ bar)).should eq(["foo bar"]) }
it { Process.parse_arguments(%q("foo\ bar")).should eq(["foo\\ bar"]) }
it { Process.parse_arguments(%q('foo\ bar')).should eq(["foo\\ bar"]) }
it { Process.parse_arguments("\\").should eq(["\\"]) }
it { Process.parse_arguments(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "\\hello/", "Fizz Buzz"]) }
it { Process.parse_arguments(%q[foo"bar"baz]).should eq(["foobarbaz"]) }
it { Process.parse_arguments(%q[foo'bar'baz]).should eq(["foobarbaz"]) }
it { Process.parse_arguments(%(this 'is a "'very wei"rd co"m"mand please" don't do t'h'a't p"leas"e)).should eq(["this", "is a \"very", "weird command please", "dont do that", "please"]) }
{% if flag?(:unix) %}
describe ".parse_arguments" do
it "uses the native platform rules" do
Process.parse_arguments(%q[a\ b'c']).should eq [%q[a bc]]
end
end
{% elsif flag?(:win32) %}
describe ".parse_arguments" do
it "uses the native platform rules" do
Process.parse_arguments(%q[a\ b'c']).should eq [%q[a\], %q[b'c']]
end
end
{% else %}
pending ".parse_arguments"
{% end %}

describe ".parse_arguments_posix" do
it { Process.parse_arguments_posix(%q[]).should eq([] of String) }
it { Process.parse_arguments_posix(%q[ ]).should eq([] of String) }
it { Process.parse_arguments_posix(%q[foo]).should eq [%q[foo]] }
it { Process.parse_arguments_posix(%q[foo bar]).should eq [%q[foo], %q[bar]] }
it { Process.parse_arguments_posix(%q["foo bar" 'foo bar' baz]).should eq [%q[foo bar], %q[foo bar], %q[baz]] }
it { Process.parse_arguments_posix(%q["foo bar"'foo bar'baz]).should eq [%q[foo barfoo barbaz]] }
it { Process.parse_arguments_posix(%q[foo\ bar]).should eq [%q[foo bar]] }
it { Process.parse_arguments_posix(%q["foo\ bar"]).should eq [%q[foo\ bar]] }
it { Process.parse_arguments_posix(%q['foo\ bar']).should eq [%q[foo\ bar]] }
it { Process.parse_arguments_posix(%q[\]).should eq [%q[\]] }
it { Process.parse_arguments_posix(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq [%q[foo bar], %q[\hello/], %q[Fizz Buzz]] }
it { Process.parse_arguments_posix(%q[foo"bar"baz]).should eq [%q[foobarbaz]] }
it { Process.parse_arguments_posix(%q[foo'bar'baz]).should eq [%q[foobarbaz]] }
it { Process.parse_arguments_posix(%q[this 'is a "'very wei"rd co"m"mand please" don't do t'h'a't p"leas"e]).should eq [%q[this], %q[is a "very], %q[weird command please], %q[dont do that], %q[please]] }

it "raises an error when double quote is unclosed" do
expect_raises ArgumentError, "Unmatched quote" do
Process.parse_arguments(%q["foo])
Process.parse_arguments_posix(%q["foo])
end
end

it "raises an error if single quote is unclosed" do
expect_raises ArgumentError, "Unmatched quote" do
Process.parse_arguments(%q['foo])
Process.parse_arguments_posix(%q['foo])
end
end
end

describe ".parse_arguments_windows" do
it { Process.parse_arguments_windows(%q[]).should eq([] of String) }
it { Process.parse_arguments_windows(%q[ ]).should eq([] of String) }
it { Process.parse_arguments_windows(%q[foo]).should eq [%q[foo]] }
it { Process.parse_arguments_windows(%q[foo bar]).should eq [%q[foo], %q[bar]] }
it { Process.parse_arguments_windows(%q["foo bar" 'foo bar' baz]).should eq [%q[foo bar], %q['foo], %q[bar'], %q[baz]] }
it { Process.parse_arguments_windows(%q["foo bar"baz]).should eq [%q[foo barbaz]] }
it { Process.parse_arguments_windows(%q[foo"bar baz"]).should eq [%q[foobar baz]] }
it { Process.parse_arguments_windows(%q[foo\bar]).should eq [%q[foo\bar]] }
it { Process.parse_arguments_windows(%q[foo\ bar]).should eq [%q[foo\], %q[bar]] }
it { Process.parse_arguments_windows(%q[foo\\bar]).should eq [%q[foo\\bar]] }
it { Process.parse_arguments_windows(%q[foo\\\bar]).should eq [%q[foo\\\bar]] }
it { Process.parse_arguments_windows(%q[ /LIBPATH:C:\crystal\lib ]).should eq [%q[/LIBPATH:C:\crystal\lib]] }
it { Process.parse_arguments_windows(%q[a\\\b d"e f"g h]).should eq [%q[a\\\b], %q[de fg], %q[h]] }
it { Process.parse_arguments_windows(%q[a\\\"b c d]).should eq [%q[a\"b], %q[c], %q[d]] }
it { Process.parse_arguments_windows(%q[a\\\\"b c" d e]).should eq [%q[a\\b c], %q[d], %q[e]] }
it { Process.parse_arguments_windows(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq [%q[foo bar], %q['\hello/'], %q[Fizz\], %q[Buzz]] }
it { Process.parse_arguments_windows(%q[this 'is a "'very wei"rd co"m"mand please" don't do t'h'a't p"leas"e"]).should eq [%q[this], %q['is], %q[a], %q['very weird], %q[command], %q[please don't do t'h'a't please]] }

it "raises an error if double quote is unclosed" do
expect_raises ArgumentError, "Unmatched quote" do
Process.parse_arguments_windows(%q["foo])
Process.parse_arguments_windows(%q[\"foo])
Process.parse_arguments_windows(%q["f\"oo\\\"])
end
end
end
Expand Down
85 changes: 82 additions & 3 deletions src/process/shell.cr
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,32 @@ class Process
quote_windows({arg})
end

# Split a *line* string into the array of tokens in the same way the POSIX shell.
# Splits the given *line* into individual command-line arguments in a
# platform-specific manner, unquoting tokens if necessary.
#
# Equivalent to `parse_arguments_posix` on Unix-like systems. Equivalent to
# `parse_arguments_windows` on Windows.
def self.parse_arguments(line : String) : Array(String)
{% if flag?(:unix) %}
parse_arguments_posix(line)
{% elsif flag?(:win32) %}
parse_arguments_windows(line)
{% else %}
raise NotImplementedError.new("Process.parse_arguments")
{% end %}
end

# Splits the given *line* into individual command-line arguments according to
# POSIX shell rules, unquoting tokens if necessary.
#
# Raises `ArgumentError` if a quotation mark is unclosed.
#
# ```
# Process.parse_arguments(%q["foo bar" '\hello/' Fizz\ Buzz]) # => ["foo bar", "\\hello/", "Fizz Buzz"]
# Process.parse_arguments_posix(%q["foo bar" '\hello/' Fizz\ Buzz]) # => ["foo bar", "\\hello/", "Fizz Buzz"]
# ```
#
# See https://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_03
def self.parse_arguments(line : String) : Array(String)
def self.parse_arguments_posix(line : String) : Array(String)
tokens = [] of String

reader = Char::Reader.new(line)
Expand Down Expand Up @@ -158,4 +176,65 @@ class Process

tokens
end

# Splits the given *line* into individual command-line arguments according to
# Microsoft's standard C runtime, unquoting tokens if necessary.
#
# Raises `ArgumentError` if a quotation mark is unclosed. Leading spaces in
# *line* are ignored. Otherwise, this method is equivalent to
# [`CommandLineToArgvW`](https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw)
# for some unspecified program name.
#
# NOTE: This does **not** take strings that are passed to the CMD shell or
# used in a batch script.
#
# ```
# Process.parse_arguments_windows(%q[foo"bar \\\"hello\\" Fizz\Buzz]) # => ["foobar \\\"hello\\", "Fizz\\Buzz"]
# ```
def self.parse_arguments_windows(line : String) : Array(String)
tokens = [] of String
reader = Char::Reader.new(line)
quote = false

while true
# skip whitespace
while reader.current_char.in?(' ', '\t')
reader.next_char
end
break unless reader.has_next?

token = String.build do |str|
while true
backslash_count = 0
while reader.current_char == '\\'
backslash_count += 1
reader.next_char
end

if reader.current_char == '"'
(backslash_count // 2).times { str << '\\' }
if backslash_count.odd?
str << '"'
else
quote = !quote
end
else
backslash_count.times { str << '\\' }
break unless reader.has_next?
# `current_char` is neither '\\' nor '"'
char = reader.current_char
break if char.in?(' ', '\t') && !quote
str << char
end

reader.next_char
end
end

tokens << token
end

raise ArgumentError.new("Unmatched quote") if quote
tokens
end
end