From 29362e45bcdbcaf83bcdcfa1abc7aed65f228a07 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Oct 2015 14:03:33 -0600 Subject: [PATCH 1/5] Parse Windows commands differently than Unix commands Stinkin' backslashes --- middleware/commands.go | 82 ++++++++++++++++++++++++++++++++++--- middleware/commands_test.go | 67 ++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 5 deletions(-) diff --git a/middleware/commands.go b/middleware/commands.go index 6fb4a72e4..c9a4733e0 100644 --- a/middleware/commands.go +++ b/middleware/commands.go @@ -2,6 +2,9 @@ package middleware import ( "errors" + "runtime" + "strings" + "unicode" "github.com/flynn/go-shlex" ) @@ -9,11 +12,19 @@ import ( // SplitCommandAndArgs takes a command string and parses it // shell-style into the command and its separate arguments. func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { - parts, err := shlex.Split(command) - if err != nil { - err = errors.New("error parsing command: " + err.Error()) - return - } else if len(parts) == 0 { + var parts []string + + if runtime.GOOS == "windows" { + parts = parseWindowsCommand(command) // parse it Windows-style + } else { + parts, err = shlex.Split(command) // parse it Unix-style + if err != nil { + err = errors.New("error parsing command: " + err.Error()) + return + } + } + + if len(parts) == 0 { err = errors.New("no command contained in '" + command + "'") return } @@ -25,3 +36,64 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error) return } + +// parseWindowsCommand is a sad but good-enough attempt to +// split a command into the command and its arguments like +// the Windows command line would; only basic parsing is +// supported. This function has to be used on Windows instead +// of the shlex package because this function treats backslash +// characters properly. +// +// Loosely based off the rules here: http://stackoverflow.com/a/4094897/1048862 +// True parsing is much, much trickier. +func parseWindowsCommand(cmd string) []string { + var parts []string + var part string + var quoted bool + var backslashes int + + for _, ch := range cmd { + if ch == '\\' { + backslashes++ + continue + } + var evenBacksl = (backslashes % 2) == 0 + if backslashes > 0 && ch != '\\' { + numBacksl := (backslashes / 2) + 1 + if ch == '"' { + numBacksl-- + } + part += strings.Repeat(`\`, numBacksl) + backslashes = 0 + } + + if quoted { + if ch == '"' && evenBacksl { + quoted = false + continue + } + part += string(ch) + continue + } + + if unicode.IsSpace(ch) && len(part) > 0 { + parts = append(parts, part) + part = "" + continue + } + + if ch == '"' && evenBacksl { + quoted = true + continue + } + + part += string(ch) + } + + if len(part) > 0 { + parts = append(parts, part) + part = "" + } + + return parts +} diff --git a/middleware/commands_test.go b/middleware/commands_test.go index 3a5b33342..83b7678d5 100644 --- a/middleware/commands_test.go +++ b/middleware/commands_test.go @@ -6,6 +6,73 @@ import ( "testing" ) +func TestParseWindowsCommand(t *testing.T) { + for i, test := range []struct { + input string + expected []string + }{ + { // 0 + input: `cmd`, + expected: []string{`cmd`}, + }, + { // 1 + input: `cmd arg1 arg2`, + expected: []string{`cmd`, `arg1`, `arg2`}, + }, + { // 2 + input: `cmd "combined arg" arg2`, + expected: []string{`cmd`, `combined arg`, `arg2`}, + }, + { // 3 + input: `mkdir C:\Windows\foo\bar`, + expected: []string{`mkdir`, `C:\Windows\foo\bar`}, + }, + { // 4 + input: `"command here"`, + expected: []string{`command here`}, + }, + { // 5 + input: `cmd \"arg\"`, + expected: []string{`cmd`, `"arg"`}, + }, + { // 6 + input: `cmd "a \"quoted value\""`, + expected: []string{`cmd`, `a "quoted value"`}, + }, + { // 7 + input: `mkdir "C:\directory name\foobar"`, + expected: []string{`mkdir`, `C:\directory name\foobar`}, + }, + { // 8 + input: `mkdir C:\ space`, + expected: []string{`mkdir`, `C:\`, `space`}, + }, + { // 9 + input: `mkdir "C:\ space"`, + expected: []string{`mkdir`, `C:\ space`}, + }, + { // 10 + input: `\\"`, + expected: []string{`\`}, + }, + { // 11 + input: `"\\\""`, + expected: []string{`\"`}, + }, + } { + actual := parseWindowsCommand(test.input) + if len(actual) != len(test.expected) { + t.Errorf("Test %d: Expected %d parts, got %d: %#v", i, len(test.expected), len(actual), actual) + continue + } + for j := 0; j < len(actual); j++ { + if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { + t.Errorf("Test %d: Expected: %v Actual: %v (index %d)", i, expectedPart, actualPart, j) + } + } + } +} + func TestSplitCommandAndArgs(t *testing.T) { var parseErrorContent = "error parsing command:" var noCommandErrContent = "no command contained in" From 794d271152906edd1629cce296ebd9bc969d321f Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Oct 2015 14:11:30 -0600 Subject: [PATCH 2/5] Remove extra tests that were Linux-specific These tests with the backslash seem to assert that shlex (our Unix shell parsing library) is working properly, not our wrapper function (that parses commands for both Windows and non-Windows). These tests break on Windows so I have removed them. --- middleware/commands_test.go | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/middleware/commands_test.go b/middleware/commands_test.go index 83b7678d5..770bdd7df 100644 --- a/middleware/commands_test.go +++ b/middleware/commands_test.go @@ -118,49 +118,28 @@ func TestSplitCommandAndArgs(t *testing.T) { expectedArgs: []string{`arg1 arg1`}, expectedErrContent: ``, }, - // Test case 4 - command with single argument with space character - escaped - { - input: `command arg1\ arg1`, - expectedCommand: `command`, - expectedArgs: []string{`arg1 arg1`}, - expectedErrContent: ``, - }, - // Test case 6 - command with escaped quote character - { - input: `command "arg1 \" arg1"`, - expectedCommand: `command`, - expectedArgs: []string{`arg1 " arg1`}, - expectedErrContent: ``, - }, - // Test case 7 - command with escaped backslash - { - input: `command '\arg1'`, - expectedCommand: `command`, - expectedArgs: []string{`\arg1`}, - expectedErrContent: ``, - }, - // Test case 8 - command with comments + // Test case 5 - command with comments { input: `command arg1 #comment1 comment2`, expectedCommand: `command`, expectedArgs: []string{`arg1`}, expectedErrContent: "", }, - // Test case 9 - command with multiple spaces and tab character + // Test case 6 - command with multiple spaces and tab character { input: "command arg1 arg2\targ3", expectedCommand: `command`, expectedArgs: []string{`arg1`, `arg2`, "arg3"}, expectedErrContent: "", }, - // Test case 10 - command with unclosed quotes + // Test case 7 - command with unclosed quotes { input: `command "arg1 arg2`, expectedCommand: "", expectedArgs: nil, expectedErrContent: parseErrorContent, }, - // Test case 11 - command with unclosed quotes + // Test case 8 - command with unclosed quotes { input: `command 'arg1 arg2"`, expectedCommand: "", From fec491fb1207fce686a30a6bcc002156cd8294e7 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Wed, 21 Oct 2015 14:15:42 -0600 Subject: [PATCH 3/5] Removed another test that is Windows-specific We're not trying to test the shlex library; just our wrapper function --- middleware/commands_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/middleware/commands_test.go b/middleware/commands_test.go index 770bdd7df..5274a9e33 100644 --- a/middleware/commands_test.go +++ b/middleware/commands_test.go @@ -118,28 +118,21 @@ func TestSplitCommandAndArgs(t *testing.T) { expectedArgs: []string{`arg1 arg1`}, expectedErrContent: ``, }, - // Test case 5 - command with comments - { - input: `command arg1 #comment1 comment2`, - expectedCommand: `command`, - expectedArgs: []string{`arg1`}, - expectedErrContent: "", - }, - // Test case 6 - command with multiple spaces and tab character + // Test case 5 - command with multiple spaces and tab character { input: "command arg1 arg2\targ3", expectedCommand: `command`, expectedArgs: []string{`arg1`, `arg2`, "arg3"}, expectedErrContent: "", }, - // Test case 7 - command with unclosed quotes + // Test case 6 - command with unclosed quotes { input: `command "arg1 arg2`, expectedCommand: "", expectedArgs: nil, expectedErrContent: parseErrorContent, }, - // Test case 8 - command with unclosed quotes + // Test case 7 - command with unclosed quotes { input: `command 'arg1 arg2"`, expectedCommand: "", From 0d004ccbab59857c48e8dca4e9e26f1cdf588942 Mon Sep 17 00:00:00 2001 From: makpoc Date: Fri, 23 Oct 2015 20:21:05 +0300 Subject: [PATCH 4/5] Attempt to fix windows command parsing + add more tests --- middleware/commands.go | 60 ++++++++++++++++++++++++++++++++++++- middleware/commands_test.go | 45 +++++++++++++++++++++++----- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/middleware/commands.go b/middleware/commands.go index c9a4733e0..6daa4eb9e 100644 --- a/middleware/commands.go +++ b/middleware/commands.go @@ -2,6 +2,7 @@ package middleware import ( "errors" + "fmt" "runtime" "strings" "unicode" @@ -46,7 +47,7 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error) // // Loosely based off the rules here: http://stackoverflow.com/a/4094897/1048862 // True parsing is much, much trickier. -func parseWindowsCommand(cmd string) []string { +func parseWindowsCommand2(cmd string) []string { var parts []string var part string var quoted bool @@ -97,3 +98,60 @@ func parseWindowsCommand(cmd string) []string { return parts } + +func parseWindowsCommand(cmd string) []string { + var parts []string + var part string + var inQuotes bool + var wasBackslash bool + + prefix := "DEBUG:" + + fmt.Println(prefix, "Parsing cmd:", cmd) + + for i, ch := range cmd { + fmt.Println(" ", prefix, "Looking at char:", string(ch), "at index", string(i)) + + if ch == '\\' { + wasBackslash = true + // put it in the part - for now we don't know if it's escaping char or path separator + part += string(ch) + continue + } + + if ch == '"' { + if wasBackslash { + // remove the backslash from the part and add the escaped quote instead + part = part[:len(part)-1] + part += string(ch) + wasBackslash = false + continue + } else { + // normal escaping quotes + fmt.Println(" ", prefix, "and it's a quote") + inQuotes = !inQuotes + continue + + } + } + + if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 { + fmt.Println(" ", prefix, "and it's a space outside quotes") + parts = append(parts, part) + part = "" + wasBackslash = false + continue + } + + wasBackslash = false + part += string(ch) + } + + if len(part) > 0 { + parts = append(parts, part) + part = "" + } + + fmt.Println(prefix, strings.Join(parts, ",")) + return parts +} diff --git a/middleware/commands_test.go b/middleware/commands_test.go index 5274a9e33..0a2587788 100644 --- a/middleware/commands_test.go +++ b/middleware/commands_test.go @@ -7,7 +7,7 @@ import ( ) func TestParseWindowsCommand(t *testing.T) { - for i, test := range []struct { + tests := []struct { input string expected []string }{ @@ -51,25 +51,54 @@ func TestParseWindowsCommand(t *testing.T) { input: `mkdir "C:\ space"`, expected: []string{`mkdir`, `C:\ space`}, }, - { // 10 - input: `\\"`, - expected: []string{`\`}, + // 10 + { + input: `mkdir \\?\C:\Users`, + expected: []string{`mkdir`, `\\?\C:\Users`}, }, - { // 11 - input: `"\\\""`, - expected: []string{`\"`}, + // 11 + { + input: `mkdir "\\?\C:\Program Files"`, + expected: []string{`mkdir`, `\\?\C:\Program Files`}, }, - } { + } + var nTests int + for i, test := range tests { + fmt.Printf("====== Test %d ======\n", i) actual := parseWindowsCommand(test.input) if len(actual) != len(test.expected) { + fmt.Printf("Test %d: Expected %d parts, got %d: %#v", i, len(test.expected), len(actual), actual) + fmt.Println() t.Errorf("Test %d: Expected %d parts, got %d: %#v", i, len(test.expected), len(actual), actual) continue } for j := 0; j < len(actual); j++ { if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { + fmt.Printf("Test %d: Expected: %v Actual: %v (index %d)", i, expectedPart, actualPart, j) + fmt.Println() t.Errorf("Test %d: Expected: %v Actual: %v (index %d)", i, expectedPart, actualPart, j) } } + nTests += 1 + } + + for _, test := range tests { + fmt.Printf("====== Test %d ======\n", nTests) + actual := parseWindowsCommand2(test.input) + if len(actual) != len(test.expected) { + fmt.Printf("Test %d: Expected %d parts, got %d: %#v", nTests, len(test.expected), len(actual), actual) + fmt.Println() + t.Errorf("Test %d: Expected %d parts, got %d: %#v", nTests, len(test.expected), len(actual), actual) + continue + } + for j := 0; j < len(actual); j++ { + if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { + fmt.Printf("Test %d: Expected: %v Actual: %v (index %d)", nTests, expectedPart, actualPart, j) + fmt.Println() + t.Errorf("Test %d: Expected: %v Actual: %v (index %d)", nTests, expectedPart, actualPart, j) + } + } + nTests += 1 } } From d1b667fbce4603272ffd90272e10544131de4597 Mon Sep 17 00:00:00 2001 From: Makpoc Date: Sat, 24 Oct 2015 15:33:04 +0300 Subject: [PATCH 5/5] Two quotes next to each other result in one escaped quote; Add Split Example, add/refactor tests for every platform. --- middleware/commands.go | 133 ++++++++------------- middleware/commands_test.go | 231 ++++++++++++++++++++++++------------ 2 files changed, 206 insertions(+), 158 deletions(-) diff --git a/middleware/commands.go b/middleware/commands.go index 6daa4eb9e..5c241161e 100644 --- a/middleware/commands.go +++ b/middleware/commands.go @@ -2,23 +2,23 @@ package middleware import ( "errors" - "fmt" "runtime" - "strings" "unicode" "github.com/flynn/go-shlex" ) +var runtimeGoos = runtime.GOOS + // SplitCommandAndArgs takes a command string and parses it // shell-style into the command and its separate arguments. func SplitCommandAndArgs(command string) (cmd string, args []string, err error) { var parts []string - if runtime.GOOS == "windows" { + if runtimeGoos == "windows" { parts = parseWindowsCommand(command) // parse it Windows-style } else { - parts, err = shlex.Split(command) // parse it Unix-style + parts, err = parseUnixCommand(command) // parse it Unix-style if err != nil { err = errors.New("error parsing command: " + err.Error()) return @@ -38,112 +38,76 @@ func SplitCommandAndArgs(command string) (cmd string, args []string, err error) return } -// parseWindowsCommand is a sad but good-enough attempt to -// split a command into the command and its arguments like -// the Windows command line would; only basic parsing is -// supported. This function has to be used on Windows instead -// of the shlex package because this function treats backslash -// characters properly. -// -// Loosely based off the rules here: http://stackoverflow.com/a/4094897/1048862 -// True parsing is much, much trickier. -func parseWindowsCommand2(cmd string) []string { - var parts []string - var part string - var quoted bool - var backslashes int - - for _, ch := range cmd { - if ch == '\\' { - backslashes++ - continue - } - var evenBacksl = (backslashes % 2) == 0 - if backslashes > 0 && ch != '\\' { - numBacksl := (backslashes / 2) + 1 - if ch == '"' { - numBacksl-- - } - part += strings.Repeat(`\`, numBacksl) - backslashes = 0 - } - - if quoted { - if ch == '"' && evenBacksl { - quoted = false - continue - } - part += string(ch) - continue - } - - if unicode.IsSpace(ch) && len(part) > 0 { - parts = append(parts, part) - part = "" - continue - } - - if ch == '"' && evenBacksl { - quoted = true - continue - } - - part += string(ch) - } - - if len(part) > 0 { - parts = append(parts, part) - part = "" - } - - return parts +// parseUnixCommand parses a unix style command line and returns the +// command and its arguments or an error +func parseUnixCommand(cmd string) ([]string, error) { + return shlex.Split(cmd) } +// parseWindowsCommand parses windows command lines and +// returns the command and the arguments as an array. It +// should be able to parse commonly used command lines. +// Only basic syntax is supported: +// - spaces in double quotes are not token delimiters +// - double quotes are escaped by either backspace or another double quote +// - except for the above case backspaces are path separators (not special) +// +// Many sources point out that escaping quotes using backslash can be unsafe. +// Use two double quotes when possible. (Source: http://stackoverflow.com/a/31413730/2616179 ) +// +// This function has to be used on Windows instead +// of the shlex package because this function treats backslash +// characters properly. func parseWindowsCommand(cmd string) []string { + const backslash = '\\' + const quote = '"' + var parts []string var part string var inQuotes bool - var wasBackslash bool - - prefix := "DEBUG:" - - fmt.Println(prefix, "Parsing cmd:", cmd) + var lastRune rune for i, ch := range cmd { - fmt.Println(" ", prefix, "Looking at char:", string(ch), "at index", string(i)) - if ch == '\\' { - wasBackslash = true - // put it in the part - for now we don't know if it's escaping char or path separator + if i != 0 { + lastRune = rune(cmd[i-1]) + } + + if ch == backslash { + // put it in the part - for now we don't know if it's an + // escaping char or path separator part += string(ch) continue } - if ch == '"' { - if wasBackslash { + if ch == quote { + if lastRune == backslash { // remove the backslash from the part and add the escaped quote instead part = part[:len(part)-1] part += string(ch) - wasBackslash = false continue - } else { - // normal escaping quotes - fmt.Println(" ", prefix, "and it's a quote") - inQuotes = !inQuotes - continue - } + + if lastRune == quote { + // revert the last change of the inQuotes state + // it was an escaping quote + inQuotes = !inQuotes + part += string(ch) + continue + } + + // normal escaping quotes + inQuotes = !inQuotes + continue + } if unicode.IsSpace(ch) && !inQuotes && len(part) > 0 { - fmt.Println(" ", prefix, "and it's a space outside quotes") parts = append(parts, part) part = "" - wasBackslash = false continue } - wasBackslash = false part += string(ch) } @@ -152,6 +116,5 @@ func parseWindowsCommand(cmd string) []string { part = "" } - fmt.Println(prefix, strings.Join(parts, ",")) return parts } diff --git a/middleware/commands_test.go b/middleware/commands_test.go index 0a2587788..3001e65a5 100644 --- a/middleware/commands_test.go +++ b/middleware/commands_test.go @@ -2,107 +2,176 @@ package middleware import ( "fmt" + "runtime" "strings" "testing" ) +func TestParseUnixCommand(t *testing.T) { + tests := []struct { + input string + expected []string + }{ + // 0 - emtpy command + { + input: ``, + expected: []string{}, + }, + // 1 - command without arguments + { + input: `command`, + expected: []string{`command`}, + }, + // 2 - command with single argument + { + input: `command arg1`, + expected: []string{`command`, `arg1`}, + }, + // 3 - command with multiple arguments + { + input: `command arg1 arg2`, + expected: []string{`command`, `arg1`, `arg2`}, + }, + // 4 - command with single argument with space character - in quotes + { + input: `command "arg1 arg1"`, + expected: []string{`command`, `arg1 arg1`}, + }, + // 5 - command with multiple spaces and tab character + { + input: "command arg1 arg2\targ3", + expected: []string{`command`, `arg1`, `arg2`, `arg3`}, + }, + // 6 - command with single argument with space character - escaped with backspace + { + input: `command arg1\ arg2`, + expected: []string{`command`, `arg1 arg2`}, + }, + // 7 - single quotes should escape special chars + { + input: `command 'arg1\ arg2'`, + expected: []string{`command`, `arg1\ arg2`}, + }, + } + + for i, test := range tests { + errorPrefix := fmt.Sprintf("Test [%d]: ", i) + errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input) + actual, _ := parseUnixCommand(test.input) + if len(actual) != len(test.expected) { + t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual) + continue + } + for j := 0; j < len(actual); j++ { + if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { + t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j) + } + } + } +} + func TestParseWindowsCommand(t *testing.T) { tests := []struct { input string expected []string }{ - { // 0 + { // 0 - empty command - do not fail + input: ``, + expected: []string{}, + }, + { // 1 - cmd without args input: `cmd`, expected: []string{`cmd`}, }, - { // 1 + { // 2 - multiple args input: `cmd arg1 arg2`, expected: []string{`cmd`, `arg1`, `arg2`}, }, - { // 2 + { // 3 - multiple args with space input: `cmd "combined arg" arg2`, expected: []string{`cmd`, `combined arg`, `arg2`}, }, - { // 3 + { // 4 - path without spaces input: `mkdir C:\Windows\foo\bar`, expected: []string{`mkdir`, `C:\Windows\foo\bar`}, }, - { // 4 + { // 5 - command with space in quotes input: `"command here"`, expected: []string{`command here`}, }, - { // 5 + { // 6 - argument with escaped quotes (two quotes) + input: `cmd ""arg""`, + expected: []string{`cmd`, `"arg"`}, + }, + { // 7 - argument with escaped quotes (backslash) input: `cmd \"arg\"`, expected: []string{`cmd`, `"arg"`}, }, - { // 6 - input: `cmd "a \"quoted value\""`, - expected: []string{`cmd`, `a "quoted value"`}, + { // 8 - two quotes (escaped) inside an inQuote element + input: `cmd "a ""quoted value"`, + expected: []string{`cmd`, `a "quoted value`}, }, - { // 7 + // TODO - see how many quotes are dislayed if we use "", """, """"""" + { // 9 - two quotes outside an inQuote element + input: `cmd a ""quoted value`, + expected: []string{`cmd`, `a`, `"quoted`, `value`}, + }, + { // 10 - path with space in quotes input: `mkdir "C:\directory name\foobar"`, expected: []string{`mkdir`, `C:\directory name\foobar`}, }, - { // 8 + { // 11 - space without quotes input: `mkdir C:\ space`, expected: []string{`mkdir`, `C:\`, `space`}, }, - { // 9 + { // 12 - space in quotes input: `mkdir "C:\ space"`, expected: []string{`mkdir`, `C:\ space`}, }, - // 10 - { + { // 13 - UNC input: `mkdir \\?\C:\Users`, expected: []string{`mkdir`, `\\?\C:\Users`}, }, - // 11 - { + { // 14 - UNC with space input: `mkdir "\\?\C:\Program Files"`, expected: []string{`mkdir`, `\\?\C:\Program Files`}, }, - } - var nTests int - for i, test := range tests { - fmt.Printf("====== Test %d ======\n", i) - actual := parseWindowsCommand(test.input) - if len(actual) != len(test.expected) { - fmt.Printf("Test %d: Expected %d parts, got %d: %#v", i, len(test.expected), len(actual), actual) - fmt.Println() - t.Errorf("Test %d: Expected %d parts, got %d: %#v", i, len(test.expected), len(actual), actual) - continue - } - for j := 0; j < len(actual); j++ { - if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { - fmt.Printf("Test %d: Expected: %v Actual: %v (index %d)", i, expectedPart, actualPart, j) - fmt.Println() - t.Errorf("Test %d: Expected: %v Actual: %v (index %d)", i, expectedPart, actualPart, j) - } - } - nTests += 1 + + { // 15 - unclosed quotes - treat as if the path ends with quote + input: `mkdir "c:\Program files`, + expected: []string{`mkdir`, `c:\Program files`}, + }, + { // 16 - quotes used inside the argument + input: `mkdir "c:\P"rogra"m f"iles`, + expected: []string{`mkdir`, `c:\Program files`}, + }, } - for _, test := range tests { - fmt.Printf("====== Test %d ======\n", nTests) - actual := parseWindowsCommand2(test.input) + for i, test := range tests { + errorPrefix := fmt.Sprintf("Test [%d]: ", i) + errorSuffix := fmt.Sprintf(" Command to parse: [%s]", test.input) + + actual := parseWindowsCommand(test.input) if len(actual) != len(test.expected) { - fmt.Printf("Test %d: Expected %d parts, got %d: %#v", nTests, len(test.expected), len(actual), actual) - fmt.Println() - t.Errorf("Test %d: Expected %d parts, got %d: %#v", nTests, len(test.expected), len(actual), actual) + t.Errorf(errorPrefix+"Expected %d parts, got %d: %#v."+errorSuffix, len(test.expected), len(actual), actual) continue } for j := 0; j < len(actual); j++ { if expectedPart, actualPart := test.expected[j], actual[j]; expectedPart != actualPart { - fmt.Printf("Test %d: Expected: %v Actual: %v (index %d)", nTests, expectedPart, actualPart, j) - fmt.Println() - t.Errorf("Test %d: Expected: %v Actual: %v (index %d)", nTests, expectedPart, actualPart, j) + t.Errorf(errorPrefix+"Expected: %v Actual: %v (index %d)."+errorSuffix, expectedPart, actualPart, j) } } - nTests += 1 } } func TestSplitCommandAndArgs(t *testing.T) { + + // force linux parsing. It's more robust and covers error cases + runtimeGoos = "linux" + defer func() { + runtimeGoos = runtime.GOOS + }() + var parseErrorContent = "error parsing command:" var noCommandErrContent = "no command contained in" @@ -112,56 +181,42 @@ func TestSplitCommandAndArgs(t *testing.T) { expectedArgs []string expectedErrContent string }{ - // Test case 0 - emtpy command + // 0 - emtpy command { input: ``, expectedCommand: ``, expectedArgs: nil, expectedErrContent: noCommandErrContent, }, - // Test case 1 - command without arguments + // 1 - command without arguments { input: `command`, expectedCommand: `command`, expectedArgs: nil, expectedErrContent: ``, }, - // Test case 2 - command with single argument + // 2 - command with single argument { input: `command arg1`, expectedCommand: `command`, expectedArgs: []string{`arg1`}, expectedErrContent: ``, }, - // Test case 3 - command with multiple arguments + // 3 - command with multiple arguments { input: `command arg1 arg2`, expectedCommand: `command`, expectedArgs: []string{`arg1`, `arg2`}, expectedErrContent: ``, }, - // Test case 4 - command with single argument with space character - in quotes - { - input: `command "arg1 arg1"`, - expectedCommand: `command`, - expectedArgs: []string{`arg1 arg1`}, - expectedErrContent: ``, - }, - // Test case 5 - command with multiple spaces and tab character - { - input: "command arg1 arg2\targ3", - expectedCommand: `command`, - expectedArgs: []string{`arg1`, `arg2`, "arg3"}, - expectedErrContent: "", - }, - // Test case 6 - command with unclosed quotes + // 4 - command with unclosed quotes { input: `command "arg1 arg2`, expectedCommand: "", expectedArgs: nil, expectedErrContent: parseErrorContent, }, - // Test case 7 - command with unclosed quotes + // 5 - command with unclosed quotes { input: `command 'arg1 arg2"`, expectedCommand: "", @@ -188,19 +243,49 @@ func TestSplitCommandAndArgs(t *testing.T) { // test if command matches if test.expectedCommand != actualCommand { - t.Errorf("Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand) + t.Errorf(errorPrefix+"Expected command: [%s], actual: [%s]."+errorSuffix, test.expectedCommand, actualCommand) } // test if arguments match if len(test.expectedArgs) != len(actualArgs) { - t.Errorf("Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs) - } - - for j, actualArg := range actualArgs { - expectedArg := test.expectedArgs[j] - if actualArg != expectedArg { - t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg) + t.Errorf(errorPrefix+"Wrong number of arguments! Expected [%v], actual [%v]."+errorSuffix, test.expectedArgs, actualArgs) + } else { + // test args only if the count matches. + for j, actualArg := range actualArgs { + expectedArg := test.expectedArgs[j] + if actualArg != expectedArg { + t.Errorf(errorPrefix+"Argument at position [%d] differ! Expected [%s], actual [%s]"+errorSuffix, j, expectedArg, actualArg) + } } } } } + +func ExampleSplitCommandAndArgs() { + var commandLine string + var command string + var args []string + + // just for the test - change GOOS and reset it at the end of the test + runtimeGoos = "windows" + defer func() { + runtimeGoos = runtime.GOOS + }() + + commandLine = `mkdir /P "C:\Program Files"` + command, args, _ = SplitCommandAndArgs(commandLine) + + fmt.Printf("Windows: %s: %s [%s]\n", commandLine, command, strings.Join(args, ",")) + + // set GOOS to linux + runtimeGoos = "linux" + + commandLine = `mkdir -p /path/with\ space` + command, args, _ = SplitCommandAndArgs(commandLine) + + fmt.Printf("Linux: %s: %s [%s]\n", commandLine, command, strings.Join(args, ",")) + + // Output: + // Windows: mkdir /P "C:\Program Files": mkdir [/P,C:\Program Files] + // Linux: mkdir -p /path/with\ space: mkdir [-p,/path/with space] +}