Skip to content

Commit

Permalink
Add platform-specific variants of Process.parse_arguments (#12278)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil authored Aug 16, 2022
1 parent f306598 commit 91d6927
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 20 deletions.
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

0 comments on commit 91d6927

Please sign in to comment.