From 90d05bd4b7a15b901cebe8fd3d2f1e80f68fe3d3 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 16 Jul 2022 11:45:22 +0800 Subject: [PATCH 1/3] Add platform-specific variants of `Process.parse_arguments` --- spec/std/process_spec.cr | 78 ++++++++++++++++++++++++++++-------- src/process/shell.cr | 86 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 20 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 8fea7f6cf57a..1a7fdebe8a0d 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -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(["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(["a\\", "b'c'"]) + end + end + {% else %} + pending ".parse_arguments" + {% end %} + + describe ".parse_arguments_posix" do + it { Process.parse_arguments_posix("").should eq(%w[]) } + it { Process.parse_arguments_posix(" ").should eq(%w[]) } + it { Process.parse_arguments_posix("foo").should eq(%w[foo]) } + it { Process.parse_arguments_posix("foo bar").should eq(%w[foo bar]) } + it { Process.parse_arguments_posix(%q("foo bar" 'foo bar' baz)).should eq(["foo bar", "foo bar", "baz"]) } + it { Process.parse_arguments_posix(%q("foo bar"'foo bar'baz)).should eq(["foo barfoo barbaz"]) } + it { Process.parse_arguments_posix(%q(foo\ bar)).should eq(["foo bar"]) } + it { Process.parse_arguments_posix(%q("foo\ bar")).should eq(["foo\\ bar"]) } + it { Process.parse_arguments_posix(%q('foo\ bar')).should eq(["foo\\ bar"]) } + it { Process.parse_arguments_posix("\\").should eq(["\\"]) } + it { Process.parse_arguments_posix(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "\\hello/", "Fizz Buzz"]) } + it { Process.parse_arguments_posix(%q[foo"bar"baz]).should eq(["foobarbaz"]) } + it { Process.parse_arguments_posix(%q[foo'bar'baz]).should eq(["foobarbaz"]) } + it { Process.parse_arguments_posix(%(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"]) } 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(%w[]) } + it { Process.parse_arguments_windows(%q[ ]).should eq(%w[]) } + it { Process.parse_arguments_windows(%q[foo]).should eq(%w[foo]) } + it { Process.parse_arguments_windows(%q[foo bar]).should eq(%w[foo bar]) } + it { Process.parse_arguments_windows(%q["foo bar" 'foo bar' baz]).should eq(["foo bar", "'foo", "bar'", "baz"]) } + it { Process.parse_arguments_windows(%q["foo bar"baz]).should eq(["foo barbaz"]) } + it { Process.parse_arguments_windows(%q[foo"bar baz"]).should eq(["foobar baz"]) } + it { Process.parse_arguments_windows(%q[foo\bar]).should eq(["foo\\bar"]) } + it { Process.parse_arguments_windows(%q[foo\ bar]).should eq(["foo\\", "bar"]) } + it { Process.parse_arguments_windows(%q[foo\\bar]).should eq(["foo\\\\bar"]) } + it { Process.parse_arguments_windows(%q[foo\\\bar]).should eq(["foo\\\\\\bar"]) } + it { Process.parse_arguments_windows(%q[ /LIBPATH:C:\crystal\lib ]).should eq(["/LIBPATH:C:\\crystal\\lib"]) } + it { Process.parse_arguments_windows(%q[a\\\b d"e f"g h]).should eq(["a\\\\\\b", "de fg", "h"]) } + it { Process.parse_arguments_windows(%q[a\\\"b c d]).should eq(["a\\\"b", "c", "d"]) } + it { Process.parse_arguments_windows(%q[a\\\\"b c" d e]).should eq(["a\\\\b c", "d", "e"]) } + it { Process.parse_arguments_windows(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "'\\hello/'", "Fizz\\", "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(["this", "'is", "a", "'very weird", "command", "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 diff --git a/src/process/shell.cr b/src/process/shell.cr index fa5f31c9bed7..9784a2dec386 100644 --- a/src/process/shell.cr +++ b/src/process/shell.cr @@ -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_unix(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) @@ -158,4 +176,66 @@ 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 + reader.next_char + elsif backslash_count > 0 + backslash_count.times { str << '\\' } + elsif reader.has_next? + char = reader.current_char + break if char.in?(' ', '\t') && !quote + str << char + reader.next_char + else + break + end + end + end + + tokens << token + end + + raise ArgumentError.new("Unmatched quote") if quote + tokens + end end From 39a4277d7baf202147c4c59455d121451810f838 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Sat, 16 Jul 2022 11:53:14 +0800 Subject: [PATCH 2/3] typo --- src/process/shell.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/process/shell.cr b/src/process/shell.cr index 9784a2dec386..1270d41b9f73 100644 --- a/src/process/shell.cr +++ b/src/process/shell.cr @@ -110,7 +110,7 @@ class Process # `parse_arguments_windows` on Windows. def self.parse_arguments(line : String) : Array(String) {% if flag?(:unix) %} - parse_arguments_unix(line) + parse_arguments_posix(line) {% elsif flag?(:win32) %} parse_arguments_windows(line) {% else %} From 6288a5078153a85849cec194f664c41c860a366d Mon Sep 17 00:00:00 2001 From: HertzDevil Date: Sun, 24 Jul 2022 18:16:30 +0800 Subject: [PATCH 3/3] fixup --- spec/std/process_spec.cr | 66 ++++++++++++++++++++-------------------- src/process/shell.cr | 11 +++---- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/spec/std/process_spec.cr b/spec/std/process_spec.cr index 1a7fdebe8a0d..2d40623f4738 100644 --- a/spec/std/process_spec.cr +++ b/spec/std/process_spec.cr @@ -445,13 +445,13 @@ describe Process do {% if flag?(:unix) %} describe ".parse_arguments" do it "uses the native platform rules" do - Process.parse_arguments(%q(a\ b'c')).should eq(["a bc"]) + 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(["a\\", "b'c'"]) + Process.parse_arguments(%q[a\ b'c']).should eq [%q[a\], %q[b'c']] end end {% else %} @@ -459,20 +459,20 @@ describe Process do {% end %} describe ".parse_arguments_posix" do - it { Process.parse_arguments_posix("").should eq(%w[]) } - it { Process.parse_arguments_posix(" ").should eq(%w[]) } - it { Process.parse_arguments_posix("foo").should eq(%w[foo]) } - it { Process.parse_arguments_posix("foo bar").should eq(%w[foo bar]) } - it { Process.parse_arguments_posix(%q("foo bar" 'foo bar' baz)).should eq(["foo bar", "foo bar", "baz"]) } - it { Process.parse_arguments_posix(%q("foo bar"'foo bar'baz)).should eq(["foo barfoo barbaz"]) } - it { Process.parse_arguments_posix(%q(foo\ bar)).should eq(["foo bar"]) } - it { Process.parse_arguments_posix(%q("foo\ bar")).should eq(["foo\\ bar"]) } - it { Process.parse_arguments_posix(%q('foo\ bar')).should eq(["foo\\ bar"]) } - it { Process.parse_arguments_posix("\\").should eq(["\\"]) } - it { Process.parse_arguments_posix(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "\\hello/", "Fizz Buzz"]) } - it { Process.parse_arguments_posix(%q[foo"bar"baz]).should eq(["foobarbaz"]) } - it { Process.parse_arguments_posix(%q[foo'bar'baz]).should eq(["foobarbaz"]) } - it { Process.parse_arguments_posix(%(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"]) } + 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 @@ -488,23 +488,23 @@ describe Process do end describe ".parse_arguments_windows" do - it { Process.parse_arguments_windows(%q[]).should eq(%w[]) } - it { Process.parse_arguments_windows(%q[ ]).should eq(%w[]) } - it { Process.parse_arguments_windows(%q[foo]).should eq(%w[foo]) } - it { Process.parse_arguments_windows(%q[foo bar]).should eq(%w[foo bar]) } - it { Process.parse_arguments_windows(%q["foo bar" 'foo bar' baz]).should eq(["foo bar", "'foo", "bar'", "baz"]) } - it { Process.parse_arguments_windows(%q["foo bar"baz]).should eq(["foo barbaz"]) } - it { Process.parse_arguments_windows(%q[foo"bar baz"]).should eq(["foobar baz"]) } - it { Process.parse_arguments_windows(%q[foo\bar]).should eq(["foo\\bar"]) } - it { Process.parse_arguments_windows(%q[foo\ bar]).should eq(["foo\\", "bar"]) } - it { Process.parse_arguments_windows(%q[foo\\bar]).should eq(["foo\\\\bar"]) } - it { Process.parse_arguments_windows(%q[foo\\\bar]).should eq(["foo\\\\\\bar"]) } - it { Process.parse_arguments_windows(%q[ /LIBPATH:C:\crystal\lib ]).should eq(["/LIBPATH:C:\\crystal\\lib"]) } - it { Process.parse_arguments_windows(%q[a\\\b d"e f"g h]).should eq(["a\\\\\\b", "de fg", "h"]) } - it { Process.parse_arguments_windows(%q[a\\\"b c d]).should eq(["a\\\"b", "c", "d"]) } - it { Process.parse_arguments_windows(%q[a\\\\"b c" d e]).should eq(["a\\\\b c", "d", "e"]) } - it { Process.parse_arguments_windows(%q["foo bar" '\hello/' Fizz\ Buzz]).should eq(["foo bar", "'\\hello/'", "Fizz\\", "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(["this", "'is", "a", "'very weird", "command", "please don't do t'h'a't please"]) } + 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 diff --git a/src/process/shell.cr b/src/process/shell.cr index 1270d41b9f73..e206c6c19a5a 100644 --- a/src/process/shell.cr +++ b/src/process/shell.cr @@ -218,17 +218,16 @@ class Process else quote = !quote end - reader.next_char - elsif backslash_count > 0 + else backslash_count.times { str << '\\' } - elsif reader.has_next? + break unless reader.has_next? + # `current_char` is neither '\\' nor '"' char = reader.current_char break if char.in?(' ', '\t') && !quote str << char - reader.next_char - else - break end + + reader.next_char end end