diff --git a/middleware/commands.go b/middleware/commands.go index 6fb4a72e..c9a4733e 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 3a5b3334..83b7678d 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"