From c3f1e374fef52765698eadee6f4785a901165ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leandro=20L=C3=B3pez?= Date: Wed, 2 Aug 2017 12:01:50 -0300 Subject: [PATCH 01/58] Return process Stderr in shellRun error exec.Cmd.Output returns only the Stdout of the executed process, thus the actual error message was being lost. In cases where the subprocess return a exec.ExitError use exec.ExitError.Stderr as the ouput. It should've been possible to use exec.Cmd.CombinedOutput that returns both Stdout and Stderr in an unified way, but with this a successful command that also returns something in Stderr will interpolate Stderr in the returned string. --- shellwords_test.go | 4 ++++ util_posix.go | 3 +++ util_windows.go | 3 +++ 3 files changed, 10 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 63b196b..85806a7 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -92,6 +92,10 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an error") } + expected := "exit status 2:go: unknown subcommand \"Version\"\nRun 'go help' for usage.\n" + if expected != err.Error() { + t.Fatalf("Expected %q, but %q", expected, err.Error()) + } } func TestEnv(t *testing.T) { diff --git a/util_posix.go b/util_posix.go index 4f8ac55..31bdda5 100644 --- a/util_posix.go +++ b/util_posix.go @@ -13,6 +13,9 @@ func shellRun(line string) (string, error) { shell := os.Getenv("SHELL") b, err := exec.Command(shell, "-c", line).Output() if err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + b = eerr.Stderr + } return "", errors.New(err.Error() + ":" + string(b)) } return strings.TrimSpace(string(b)), nil diff --git a/util_windows.go b/util_windows.go index 7cad4cf..5e06565 100644 --- a/util_windows.go +++ b/util_windows.go @@ -11,6 +11,9 @@ func shellRun(line string) (string, error) { shell := os.Getenv("COMSPEC") b, err := exec.Command(shell, "/c", line).Output() if err != nil { + if eerr, ok := err.(*exec.ExitError); ok { + b = eerr.Stderr + } return "", errors.New(err.Error() + ":" + string(b)) } return strings.TrimSpace(string(b)), nil From 0cc3489770abc4e60bc9809d281b88085346d92f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:23:28 +0900 Subject: [PATCH 02/58] support $(command) fixes #7 --- shellwords.go | 32 +++++++++++++++++++++++++++----- shellwords_test.go | 9 +++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/shellwords.go b/shellwords.go index 1078039..b0a03df 100644 --- a/shellwords.go +++ b/shellwords.go @@ -44,7 +44,7 @@ func NewParser() *Parser { func (p *Parser) Parse(line string) ([]string, error) { args := []string{} buf := "" - var escaped, doubleQuoted, singleQuoted, backQuote bool + var escaped, doubleQuoted, singleQuoted, backQuote, dollarEnv bool backtick := "" pos := -1 @@ -68,7 +68,7 @@ loop: } if isSpace(r) { - if singleQuoted || doubleQuoted || backQuote { + if singleQuoted || doubleQuoted || backQuote || dollarEnv { buf += string(r) backtick += string(r) } else if got { @@ -84,7 +84,7 @@ loop: switch r { case '`': - if !singleQuoted && !doubleQuoted { + if !singleQuoted && !doubleQuoted && !dollarEnv { if p.ParseBacktick { if backQuote { out, err := shellRun(backtick) @@ -100,6 +100,28 @@ loop: backtick = "" backQuote = !backQuote } + case ')': + if !singleQuoted && !doubleQuoted && !backQuote { + if p.ParseBacktick { + if dollarEnv { + out, err := shellRun(backtick) + if err != nil { + return nil, err + } + buf = out + } + backtick = "" + dollarEnv = !dollarEnv + continue + } + backtick = "" + dollarEnv = !dollarEnv + } + case '(': + if !singleQuoted && !doubleQuoted && !backQuote && !dollarEnv && len(buf) > 0 && buf == "$" { + dollarEnv = true + continue + } case '"': if !singleQuoted { doubleQuoted = !doubleQuoted @@ -119,7 +141,7 @@ loop: got = true buf += string(r) - if backQuote { + if backQuote || dollarEnv { backtick += string(r) } } @@ -131,7 +153,7 @@ loop: args = append(args, buf) } - if escaped || singleQuoted || doubleQuoted || backQuote { + if escaped || singleQuoted || doubleQuoted || backQuote || dollarEnv { return nil, errors.New("invalid command line string") } diff --git a/shellwords_test.go b/shellwords_test.go index 85806a7..8c4160e 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -83,6 +83,15 @@ func TestBacktick(t *testing.T) { if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } + + args, err = parser.Parse(`echo $(echo "foo")`) + if err != nil { + t.Fatal(err) + } + expected = []string{"echo", "foo"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } } func TestBacktickError(t *testing.T) { From 65c1effe09a5832f7e783eb71385ead11ce6b6aa Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:28:15 +0900 Subject: [PATCH 03/58] s/dollarEnv/dollarQuote/g --- shellwords.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/shellwords.go b/shellwords.go index b0a03df..e06820b 100644 --- a/shellwords.go +++ b/shellwords.go @@ -44,7 +44,7 @@ func NewParser() *Parser { func (p *Parser) Parse(line string) ([]string, error) { args := []string{} buf := "" - var escaped, doubleQuoted, singleQuoted, backQuote, dollarEnv bool + var escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote bool backtick := "" pos := -1 @@ -68,7 +68,7 @@ loop: } if isSpace(r) { - if singleQuoted || doubleQuoted || backQuote || dollarEnv { + if singleQuoted || doubleQuoted || backQuote || dollarQuote { buf += string(r) backtick += string(r) } else if got { @@ -84,7 +84,7 @@ loop: switch r { case '`': - if !singleQuoted && !doubleQuoted && !dollarEnv { + if !singleQuoted && !doubleQuoted && !dollarQuote { if p.ParseBacktick { if backQuote { out, err := shellRun(backtick) @@ -103,7 +103,7 @@ loop: case ')': if !singleQuoted && !doubleQuoted && !backQuote { if p.ParseBacktick { - if dollarEnv { + if dollarQuote { out, err := shellRun(backtick) if err != nil { return nil, err @@ -111,15 +111,15 @@ loop: buf = out } backtick = "" - dollarEnv = !dollarEnv + dollarQuote = !dollarQuote continue } backtick = "" - dollarEnv = !dollarEnv + dollarQuote = !dollarQuote } case '(': - if !singleQuoted && !doubleQuoted && !backQuote && !dollarEnv && len(buf) > 0 && buf == "$" { - dollarEnv = true + if !singleQuoted && !doubleQuoted && !backQuote && !dollarQuote && len(buf) > 0 && buf == "$" { + dollarQuote = true continue } case '"': @@ -141,7 +141,7 @@ loop: got = true buf += string(r) - if backQuote || dollarEnv { + if backQuote || dollarQuote { backtick += string(r) } } @@ -153,7 +153,7 @@ loop: args = append(args, buf) } - if escaped || singleQuoted || doubleQuoted || backQuote || dollarEnv { + if escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote { return nil, errors.New("invalid command line string") } From f06ba08f94b44dd4f890296a8f32204036117161 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:35:44 +0900 Subject: [PATCH 04/58] add test --- shellwords_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 8c4160e..0f493c9 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -92,6 +92,11 @@ func TestBacktick(t *testing.T) { if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } + + args, err = parser.Parse(`echo $(echo1)`) + if err == nil { + t.Fatal("Should be an error") + } } func TestBacktickError(t *testing.T) { From 9bf3abd2d01bdea109f2a6de8b74e90f5a083dd5 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:48:05 +0900 Subject: [PATCH 05/58] fix dollar quote --- shellwords.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shellwords.go b/shellwords.go index e06820b..b5abeb3 100644 --- a/shellwords.go +++ b/shellwords.go @@ -120,15 +120,16 @@ loop: case '(': if !singleQuoted && !doubleQuoted && !backQuote && !dollarQuote && len(buf) > 0 && buf == "$" { dollarQuote = true + buf += "(" continue } case '"': - if !singleQuoted { + if !singleQuoted && !dollarQuote { doubleQuoted = !doubleQuoted continue } case '\'': - if !doubleQuoted { + if !doubleQuoted && !dollarQuote { singleQuoted = !singleQuoted continue } From 2d76dfc0965a7da36114ba22a20738bfaf80c7a0 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:48:17 +0900 Subject: [PATCH 06/58] add tests --- shellwords_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/shellwords_test.go b/shellwords_test.go index 0f493c9..84db064 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -84,7 +84,7 @@ func TestBacktick(t *testing.T) { t.Fatalf("Expected %#v, but %#v:", expected, args) } - args, err = parser.Parse(`echo $(echo "foo")`) + args, err = parser.Parse(`echo $(echo foo)`) if err != nil { t.Fatal(err) } @@ -93,9 +93,11 @@ func TestBacktick(t *testing.T) { t.Fatalf("Expected %#v, but %#v:", expected, args) } - args, err = parser.Parse(`echo $(echo1)`) - if err == nil { - t.Fatal("Should be an error") + parser.ParseBacktick = false + args, err = parser.Parse(`echo $(echo "foo")`) + expected = []string{"echo", `$(echo "foo")`} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) } } @@ -110,6 +112,10 @@ func TestBacktickError(t *testing.T) { if expected != err.Error() { t.Fatalf("Expected %q, but %q", expected, err.Error()) } + _, err = parser.Parse(`echo $(echo1)`) + if err == nil { + t.Fatal("Should be an error") + } } func TestEnv(t *testing.T) { From 11908c93d86a6062ac8601f698b77a12f3451c2f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:59:46 +0900 Subject: [PATCH 07/58] paren should be an error --- shellwords.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/shellwords.go b/shellwords.go index b5abeb3..0e84b6d 100644 --- a/shellwords.go +++ b/shellwords.go @@ -118,10 +118,14 @@ loop: dollarQuote = !dollarQuote } case '(': - if !singleQuoted && !doubleQuoted && !backQuote && !dollarQuote && len(buf) > 0 && buf == "$" { - dollarQuote = true - buf += "(" - continue + if !singleQuoted && !doubleQuoted && !backQuote { + if !dollarQuote && len(buf) > 0 && buf == "$" { + dollarQuote = true + buf += "(" + continue + } else { + return nil, errors.New("invalid command line string") + } } case '"': if !singleQuoted && !dollarQuote { From 7def019b2de0caccb2c8ba799f41e1dfd593de4d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 00:59:54 +0900 Subject: [PATCH 08/58] add tests --- shellwords_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 84db064..abe7c72 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -116,6 +116,22 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an error") } + _, err = parser.Parse(`echo $(echo1`) + if err == nil { + t.Fatal("Should be an error") + } + _, err = parser.Parse(`echo $ (echo1`) + if err == nil { + t.Fatal("Should be an error") + } + _, err = parser.Parse(`echo (echo1`) + if err == nil { + t.Fatal("Should be an error") + } + _, err = parser.Parse(`echo )echo1`) + if err == nil { + t.Fatal("Should be an error") + } } func TestEnv(t *testing.T) { From 253c54f03e167eda21ecbb2b6658693ddf651d33 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 3 Aug 2017 01:05:39 +0900 Subject: [PATCH 09/58] add test --- shellwords_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index abe7c72..594f9ef 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -99,6 +99,14 @@ func TestBacktick(t *testing.T) { if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } + args, err = parser.Parse("echo $(`echo1)") + if err != nil { + t.Fatal(err) + } + expected = []string{"echo", "$(`echo1)"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } } func TestBacktickError(t *testing.T) { From 168068573a32eee977b1b59fc69c29ee63401ec1 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 28 Aug 2017 13:11:52 +0900 Subject: [PATCH 10/58] go1.5 doesn't have exec.ExitError --- util_go15.go | 24 ++++++++++++++++++++++++ util_posix.go | 2 +- util_windows.go | 2 ++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 util_go15.go diff --git a/util_go15.go b/util_go15.go new file mode 100644 index 0000000..180f00f --- /dev/null +++ b/util_go15.go @@ -0,0 +1,24 @@ +// +build !go1.6 + +package shellwords + +import ( + "os" + "os/exec" + "runtime" + "strings" +) + +func shellRun(line string) (string, error) { + var b []byte + var err error + if runtime.GOOS == "windows" { + b, err = exec.Command(os.Getenv("COMSPEC"), "/c", line).Output() + } else { + b, err = exec.Command(os.Getenv("SHELL"), "-c", line).Output() + } + if err != nil { + return "", err + } + return strings.TrimSpace(string(b)), nil +} diff --git a/util_posix.go b/util_posix.go index 31bdda5..eaf1011 100644 --- a/util_posix.go +++ b/util_posix.go @@ -1,4 +1,4 @@ -// +build !windows +// +build !windows,go1.6 package shellwords diff --git a/util_windows.go b/util_windows.go index 5e06565..e46f89a 100644 --- a/util_windows.go +++ b/util_windows.go @@ -1,3 +1,5 @@ +// +build windows,go1.6 + package shellwords import ( From 7329b2c71e2262e6a71bc1195910e7a7cc05076d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 28 Jan 2018 21:23:04 +0900 Subject: [PATCH 11/58] skip redirect --- shellwords.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/shellwords.go b/shellwords.go index 0e84b6d..96feca7 100644 --- a/shellwords.go +++ b/shellwords.go @@ -139,6 +139,12 @@ loop: } case ';', '&', '|', '<', '>': if !(escaped || singleQuoted || doubleQuoted || backQuote) { + if r == '>' { + if c := buf[0]; '0' <= c && c <= '9' { + i -= 1 + got = false + } + } pos = i break loop } From c82e0dc2543e966f7973408b9e9f958e087fb61e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 28 Jan 2018 21:34:07 +0900 Subject: [PATCH 12/58] handle redirect --- shellwords_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 594f9ef..1a1b0f6 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -227,3 +227,22 @@ func TestHaveMore(t *testing.T) { t.Fatalf("Commands should not be remaining") } } + +func TestHaveRedirect(t *testing.T) { + parser := NewParser() + parser.ParseEnv = true + + line := "ls -la 2>foo" + args, err := parser.Parse(line) + if err != nil { + t.Fatalf(err.Error()) + } + expected := []string{"ls", "-la"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } + + if parser.Position == 0 { + t.Fatalf("Commands should be remaining") + } +} From 24da3644884892074c10048bb5b8ee33d6e775cb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 28 Jan 2018 21:35:28 +0900 Subject: [PATCH 13/58] add example --- _example/pipe.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 _example/pipe.go diff --git a/_example/pipe.go b/_example/pipe.go new file mode 100644 index 0000000..b08ae00 --- /dev/null +++ b/_example/pipe.go @@ -0,0 +1,44 @@ +// +build ignore + +package main + +import ( + "fmt" + "log" + + "github.com/mattn/go-shellwords" +) + +func isSpace(r byte) bool { + switch r { + case ' ', '\t', '\r', '\n': + return true + } + return false +} + +func main() { + line := ` + /usr/bin/ls -la | sort 2>&1 | tee files.log + ` + parser := shellwords.NewParser() + + for { + args, err := parser.Parse(line) + if err != nil { + log.Fatal(err) + } + fmt.Println(args) + if parser.Position < 0 { + break + } + i := parser.Position + for ; i < len(line); i++ { + if isSpace(line[i]) { + break + } + } + fmt.Println(string([]rune(line)[parser.Position:i])) + line = string([]rune(line)[i+1:]) + } +} From a09e470e9f00d5d72a6df965544a110fb57cb3bb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 5 Jun 2018 13:12:32 +0900 Subject: [PATCH 14/58] fix #14 --- shellwords.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shellwords.go b/shellwords.go index 96feca7..bcd1e1f 100644 --- a/shellwords.go +++ b/shellwords.go @@ -139,7 +139,7 @@ loop: } case ';', '&', '|', '<', '>': if !(escaped || singleQuoted || doubleQuoted || backQuote) { - if r == '>' { + if r == '>' && len(buf) > 0 { if c := buf[0]; '0' <= c && c <= '9' { i -= 1 got = false From e4848b26ec12fa9b4b74c945afbe7e101676c778 Mon Sep 17 00:00:00 2001 From: Itay Donanhirsh Date: Mon, 22 Oct 2018 22:22:50 -0700 Subject: [PATCH 15/58] allow to specify custom getenv --- shellwords.go | 22 +++++++++++++++++----- shellwords_test.go | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/shellwords.go b/shellwords.go index bcd1e1f..f93e264 100644 --- a/shellwords.go +++ b/shellwords.go @@ -21,13 +21,17 @@ func isSpace(r rune) bool { return false } -func replaceEnv(s string) string { +func replaceEnv(getenv func(string) string, s string) string { + if getenv == nil { + getenv = os.Getenv + } + return envRe.ReplaceAllStringFunc(s, func(s string) string { s = s[1:] if s[0] == '{' { s = s[1 : len(s)-1] } - return os.Getenv(s) + return getenv(s) }) } @@ -35,10 +39,18 @@ type Parser struct { ParseEnv bool ParseBacktick bool Position int + + // If ParseEnv is true, use this for getenv. + // If nil, use os.Getenv. + Getenv func(string) string } func NewParser() *Parser { - return &Parser{ParseEnv, ParseBacktick, 0} + return &Parser{ + ParseEnv: ParseEnv, + ParseBacktick: ParseBacktick, + Position: 0, + } } func (p *Parser) Parse(line string) ([]string, error) { @@ -73,7 +85,7 @@ loop: backtick += string(r) } else if got { if p.ParseEnv { - buf = replaceEnv(buf) + buf = replaceEnv(p.Getenv, buf) } args = append(args, buf) buf = "" @@ -159,7 +171,7 @@ loop: if got { if p.ParseEnv { - buf = replaceEnv(buf) + buf = replaceEnv(p.Getenv, buf) } args = append(args, buf) } diff --git a/shellwords_test.go b/shellwords_test.go index 1a1b0f6..29e97ea 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -157,6 +157,20 @@ func TestEnv(t *testing.T) { } } +func TestCustomEnv(t *testing.T) { + parser := NewParser() + parser.ParseEnv = true + parser.Getenv = func(k string) string { return map[string]string{"FOO": "baz"}[k] } + args, err := parser.Parse("echo $FOO") + if err != nil { + t.Fatal(err) + } + expected := []string{"echo", "baz"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } +} + func TestNoEnv(t *testing.T) { parser := NewParser() parser.ParseEnv = true From 286369f411434826d3a6a4c13e94bf9c8c148f28 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Fri, 22 Feb 2019 16:17:58 +0000 Subject: [PATCH 16/58] Enable go modules --- go.mod | 1 + 1 file changed, 1 insertion(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d96dbd --- /dev/null +++ b/go.mod @@ -0,0 +1 @@ +module github.com/mattn/go-shellwords From 2b0d8a75988a32113c1807f6dbb83897f91d5ffb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 23 Feb 2019 02:09:29 +0900 Subject: [PATCH 17/58] Fix test --- shellwords_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shellwords_test.go b/shellwords_test.go index 29e97ea..2bacc42 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -116,7 +116,7 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an error") } - expected := "exit status 2:go: unknown subcommand \"Version\"\nRun 'go help' for usage.\n" + expected := "exit status 2:go Version: unknown command\nRun 'go help' for usage.\n" if expected != err.Error() { t.Fatalf("Expected %q, but %q", expected, err.Error()) } From 9dc32850fea51a7b062fd91fb21baa422c26bf64 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 25 Feb 2019 11:43:52 +0900 Subject: [PATCH 18/58] Handle FOO=$(command) --- shellwords.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shellwords.go b/shellwords.go index f93e264..4da62f1 100644 --- a/shellwords.go +++ b/shellwords.go @@ -4,6 +4,7 @@ import ( "errors" "os" "regexp" + "strings" ) var ( @@ -131,7 +132,7 @@ loop: } case '(': if !singleQuoted && !doubleQuoted && !backQuote { - if !dollarQuote && len(buf) > 0 && buf == "$" { + if !dollarQuote && len(buf) > 0 && strings.HasSuffix(buf, "$") { dollarQuote = true buf += "(" continue From a46243cae42cf3eaedebabce33a1f71ea8c4c831 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 25 Feb 2019 11:46:07 +0900 Subject: [PATCH 19/58] Add test case --- shellwords_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 2bacc42..823c802 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -124,6 +124,10 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an error") } + _, err = parser.Parse(`echo FOO=$(echo1)`) + if err == nil { + t.Fatal("Should be an error") + } _, err = parser.Parse(`echo $(echo1`) if err == nil { t.Fatal("Should be an error") From 04f810b90a2d8d0ad58902243fc43bb517b9a352 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 25 Feb 2019 11:59:29 +0900 Subject: [PATCH 20/58] Fix $(command) --- shellwords.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/shellwords.go b/shellwords.go index 4da62f1..41429d8 100644 --- a/shellwords.go +++ b/shellwords.go @@ -121,7 +121,11 @@ loop: if err != nil { return nil, err } - buf = out + if r == ')' { + buf = buf[:len(buf)-len(backtick)-2] + out + } else { + buf = buf[:len(buf)-len(backtick)-1] + out + } } backtick = "" dollarQuote = !dollarQuote @@ -132,7 +136,7 @@ loop: } case '(': if !singleQuoted && !doubleQuoted && !backQuote { - if !dollarQuote && len(buf) > 0 && strings.HasSuffix(buf, "$") { + if !dollarQuote && strings.HasSuffix(buf, "$") { dollarQuote = true buf += "(" continue From f077c0cda7ff490d1cd1ec6dc54ab07179d8213f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 25 Feb 2019 12:01:02 +0900 Subject: [PATCH 21/58] Fix test --- shellwords_test.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 823c802..65577fa 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -93,6 +93,15 @@ func TestBacktick(t *testing.T) { t.Fatalf("Expected %#v, but %#v:", expected, args) } + args, err = parser.Parse(`echo bar=$(echo 200)cm`) + if err != nil { + t.Fatal(err) + } + expected = []string{"echo", "bar=200cm"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } + parser.ParseBacktick = false args, err = parser.Parse(`echo $(echo "foo")`) expected = []string{"echo", `$(echo "foo")`} From fb92d1852e23c36c4fd8051050dbebca96d05f38 Mon Sep 17 00:00:00 2001 From: tengattack Date: Tue, 2 Apr 2019 19:14:41 +0800 Subject: [PATCH 22/58] Add dir option for parser --- shellwords.go | 6 ++++-- util_go15.go | 11 ++++++++--- util_posix.go | 8 ++++++-- util_windows.go | 8 ++++++-- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/shellwords.go b/shellwords.go index 41429d8..c205e8a 100644 --- a/shellwords.go +++ b/shellwords.go @@ -40,6 +40,7 @@ type Parser struct { ParseEnv bool ParseBacktick bool Position int + Dir string // If ParseEnv is true, use this for getenv. // If nil, use os.Getenv. @@ -51,6 +52,7 @@ func NewParser() *Parser { ParseEnv: ParseEnv, ParseBacktick: ParseBacktick, Position: 0, + Dir: "", } } @@ -100,7 +102,7 @@ loop: if !singleQuoted && !doubleQuoted && !dollarQuote { if p.ParseBacktick { if backQuote { - out, err := shellRun(backtick) + out, err := shellRun(backtick, p.Dir) if err != nil { return nil, err } @@ -117,7 +119,7 @@ loop: if !singleQuoted && !doubleQuoted && !backQuote { if p.ParseBacktick { if dollarQuote { - out, err := shellRun(backtick) + out, err := shellRun(backtick, p.Dir) if err != nil { return nil, err } diff --git a/util_go15.go b/util_go15.go index 180f00f..ddcbf22 100644 --- a/util_go15.go +++ b/util_go15.go @@ -9,14 +9,19 @@ import ( "strings" ) -func shellRun(line string) (string, error) { +func shellRun(line, dir string) (string, error) { var b []byte var err error + var cmd *exec.Cmd if runtime.GOOS == "windows" { - b, err = exec.Command(os.Getenv("COMSPEC"), "/c", line).Output() + cmd = exec.Command(os.Getenv("COMSPEC"), "/c", line) } else { - b, err = exec.Command(os.Getenv("SHELL"), "-c", line).Output() + cmd = exec.Command(os.Getenv("SHELL"), "-c", line) } + if dir != "" { + cmd.Dir = dir + } + b, err = cmd.Output() if err != nil { return "", err } diff --git a/util_posix.go b/util_posix.go index eaf1011..3aef2c4 100644 --- a/util_posix.go +++ b/util_posix.go @@ -9,9 +9,13 @@ import ( "strings" ) -func shellRun(line string) (string, error) { +func shellRun(line, dir string) (string, error) { shell := os.Getenv("SHELL") - b, err := exec.Command(shell, "-c", line).Output() + cmd := exec.Command(shell, "-c", line) + if dir != "" { + cmd.Dir = dir + } + b, err := cmd.Output() if err != nil { if eerr, ok := err.(*exec.ExitError); ok { b = eerr.Stderr diff --git a/util_windows.go b/util_windows.go index e46f89a..cda6850 100644 --- a/util_windows.go +++ b/util_windows.go @@ -9,9 +9,13 @@ import ( "strings" ) -func shellRun(line string) (string, error) { +func shellRun(line, dir string) (string, error) { shell := os.Getenv("COMSPEC") - b, err := exec.Command(shell, "/c", line).Output() + cmd := exec.Command(shell, "/c", line) + if dir != "" { + cmd.Dir = dir + } + b, err := cmd.Output() if err != nil { if eerr, ok := err.(*exec.ExitError); ok { b = eerr.Stderr From 0252dcfad5b98e6e045d68608e8cf0fed0368145 Mon Sep 17 00:00:00 2001 From: tengattack Date: Tue, 2 Apr 2019 19:25:14 +0800 Subject: [PATCH 23/58] Fix shell run tests --- shellwords_test.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/shellwords_test.go b/shellwords_test.go index 65577fa..fe52898 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -2,6 +2,7 @@ package shellwords import ( "os" + "path" "reflect" "testing" ) @@ -67,8 +68,29 @@ func TestLastSpace(t *testing.T) { } } +func TestShellRun(t *testing.T) { + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + pwd, err := shellRun("pwd", "") + if err != nil { + t.Fatal(err) + } + + pwd2, err := shellRun("pwd", path.Join(dir, "/_example")) + if err != nil { + t.Fatal(err) + } + + if pwd == pwd2 { + t.Fatal("`pwd` should be changed") + } +} + func TestBacktick(t *testing.T) { - goversion, err := shellRun("go version") + goversion, err := shellRun("go version", "") if err != nil { t.Fatal(err) } From 0cbbbd6664b7c671a78915948a91639b7ffc6c8f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 25 Apr 2019 23:56:59 +0900 Subject: [PATCH 24/58] Fix backquote in part of argument Fixes #25 --- shellwords.go | 2 +- shellwords_test.go | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/shellwords.go b/shellwords.go index f93e264..329a63f 100644 --- a/shellwords.go +++ b/shellwords.go @@ -103,7 +103,7 @@ loop: if err != nil { return nil, err } - buf = out + buf = buf[:len(buf)-len(backtick)] + out } backtick = "" backQuote = !backQuote diff --git a/shellwords_test.go b/shellwords_test.go index 2bacc42..cce07bd 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -260,3 +260,16 @@ func TestHaveRedirect(t *testing.T) { t.Fatalf("Commands should be remaining") } } + +func TestBackquoteInFlag(t *testing.T) { + parser := NewParser() + parser.ParseBacktick = true + args, err := parser.Parse("cmd -flag=`echo val1` -flag=val2") + if err != nil { + panic(err) + } + expected := []string{"cmd", "-flag=val1", "-flag=val2"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } +} From 6053b200e79b8be1e46cd20900b95844dd65b958 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 13:58:54 +0900 Subject: [PATCH 25/58] Fix dollar quote Support multiple command inside $() Fixes #27 --- go.mod | 2 ++ shellwords.go | 2 +- shellwords_test.go | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8d96dbd..927c8c7 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/mattn/go-shellwords + +go 1.13 diff --git a/shellwords.go b/shellwords.go index cf42b40..13d205d 100644 --- a/shellwords.go +++ b/shellwords.go @@ -155,7 +155,7 @@ loop: continue } case ';', '&', '|', '<', '>': - if !(escaped || singleQuoted || doubleQuoted || backQuote) { + if !(escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote) { if r == '>' && len(buf) > 0 { if c := buf[0]; '0' <= c && c <= '9' { i -= 1 diff --git a/shellwords_test.go b/shellwords_test.go index 1fc2245..198ec7f 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -1,6 +1,7 @@ package shellwords import ( + "go/build" "os" "reflect" "testing" @@ -118,6 +119,19 @@ func TestBacktick(t *testing.T) { } } +func TestBacktickMulti(t *testing.T) { + parser := NewParser() + parser.ParseBacktick = true + args, err := parser.Parse(`echo $(go env GOPATH && go env GOROOT)`) + if err != nil { + t.Fatal(err) + } + expected := []string{"echo", build.Default.GOPATH + "\n" + build.Default.GOROOT} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } +} + func TestBacktickError(t *testing.T) { parser := NewParser() parser.ParseBacktick = true From 9fda97a6f4992bb97064bab855244b383a92e739 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:06:36 +0900 Subject: [PATCH 26/58] Use codecov --- .travis.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 16d1430..6294d33 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ language: go +sudo: false go: - tip + before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover + - go get -t -v ./... + script: - - $HOME/gopath/bin/goveralls -repotoken 2FMhp57u8LcstKL9B190fLTcEnBtAAiEL + - ./go.test.sh + +after_success: + - bash <(curl -s https://codecov.io/bash) + From 0f04f03521faf11404dedc39039502efc1f8b162 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:11:48 +0900 Subject: [PATCH 27/58] Add test script --- go.test.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 go.test.sh diff --git a/go.test.sh b/go.test.sh new file mode 100644 index 0000000..a7deaca --- /dev/null +++ b/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done From 014d07a733efcf89df6839d09a37822f0d593ed0 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:16:26 +0900 Subject: [PATCH 28/58] chmod +x --- go.test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 go.test.sh diff --git a/go.test.sh b/go.test.sh old mode 100644 new mode 100755 From 34b6e72ffb7b656a87c282a280f4c5825fec423b Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:21:57 +0900 Subject: [PATCH 29/58] Fix badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1d235c..9e1e650 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # go-shellwords -[![Coverage Status](https://coveralls.io/repos/mattn/go-shellwords/badge.png?branch=master)](https://coveralls.io/r/mattn/go-shellwords?branch=master) +[![codecov](https://codecov.io/gh/mattn/go-shellwords/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-shellwords) [![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords) Parse line as shell words. From 58fb49c0de55a84efd0832508dcd81309c9f51ec Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:24:10 +0900 Subject: [PATCH 30/58] Remove dead code --- shellwords.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/shellwords.go b/shellwords.go index 366dd1f..2dca7f1 100644 --- a/shellwords.go +++ b/shellwords.go @@ -123,11 +123,7 @@ loop: if err != nil { return nil, err } - if r == ')' { - buf = buf[:len(buf)-len(backtick)-2] + out - } else { - buf = buf[:len(buf)-len(backtick)-1] + out - } + buf = buf[:len(buf)-len(backtick)-2] + out } backtick = "" dollarQuote = !dollarQuote From 36a9b3c57cb5caa559ff63fb7e9b585f1c00df75 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 14:38:17 +0900 Subject: [PATCH 31/58] Do not specify version --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 927c8c7..8d96dbd 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1 @@ module github.com/mattn/go-shellwords - -go 1.13 From 7813c15a28319e7c0b3f9808febcf7279f14bf5c Mon Sep 17 00:00:00 2001 From: mattn Date: Wed, 14 Aug 2019 14:58:28 +0900 Subject: [PATCH 32/58] Create FUNDING.yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..351ae11 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: mattn # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 0c573ec4532030eb578b762ea49f62a34e528870 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 14 Aug 2019 15:25:07 +0900 Subject: [PATCH 33/58] Add badge --- .travis.yml | 1 - README.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6294d33..b2904bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,3 @@ script: after_success: - bash <(curl -s https://codecov.io/bash) - diff --git a/README.md b/README.md index 9e1e650..e91902f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![codecov](https://codecov.io/gh/mattn/go-shellwords/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-shellwords) [![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords) +[![GoDoc](https://godoc.org/github.com/mattn/go-shellwords?status.svg)](http://godoc.org/github.com/mattn/go-shellwords) Parse line as shell words. From 43e18de005ddb9b3427de6743f1b4aaecc966b31 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sun, 12 Jan 2020 22:40:08 -0500 Subject: [PATCH 34/58] Support empty arguments Signed-off-by: Marc Khouzam --- shellwords.go | 6 ++++++ shellwords_test.go | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/shellwords.go b/shellwords.go index 2dca7f1..ff5e730 100644 --- a/shellwords.go +++ b/shellwords.go @@ -144,11 +144,17 @@ loop: } case '"': if !singleQuoted && !dollarQuote { + if doubleQuoted && buf == "" { + got = true + } doubleQuoted = !doubleQuoted continue } case '\'': if !doubleQuoted && !dollarQuote { + if singleQuoted && buf == "" { + got = true + } singleQuoted = !singleQuoted continue } diff --git a/shellwords_test.go b/shellwords_test.go index 741711f..2c47b06 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -69,6 +69,28 @@ func TestLastSpace(t *testing.T) { } } +func TestEmptyArgs(t *testing.T) { + args, err := Parse(`foo "" bar ''`) + if err != nil { + t.Fatal(err) + } + if len(args) != 4 { + t.Fatal("Should have three elements") + } + if args[0] != "foo" { + t.Fatal("1st element should be `foo`") + } + if args[1] != "" { + t.Fatal("2nd element should be empty") + } + if args[2] != "bar" { + t.Fatal("3rd element should be `bar`") + } + if args[3] != "" { + t.Fatal("4th element should be empty") + } +} + func TestShellRun(t *testing.T) { dir, err := os.Getwd() if err != nil { From 0171808b337e0b1e2aa3c0f7a0cb7119febaa393 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 17 Jan 2020 22:13:04 +0900 Subject: [PATCH 35/58] Accept empty whitespace arguments Fixes #32 --- shellwords.go | 4 ++-- shellwords_test.go | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/shellwords.go b/shellwords.go index ff5e730..8fe6c42 100644 --- a/shellwords.go +++ b/shellwords.go @@ -144,7 +144,7 @@ loop: } case '"': if !singleQuoted && !dollarQuote { - if doubleQuoted && buf == "" { + if doubleQuoted { got = true } doubleQuoted = !doubleQuoted @@ -152,7 +152,7 @@ loop: } case '\'': if !doubleQuoted && !dollarQuote { - if singleQuoted && buf == "" { + if singleQuoted { got = true } singleQuoted = !singleQuoted diff --git a/shellwords_test.go b/shellwords_test.go index 2c47b06..cb7f049 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -23,6 +23,12 @@ var testcases = []struct { {`var "--bar baz"`, []string{`var`, `--bar baz`}}, {`var --"bar baz"`, []string{`var`, `--bar baz`}}, {`var --"bar baz"`, []string{`var`, `--bar baz`}}, + {`a "b"`, []string{`a`, `b`}}, + {`a " b "`, []string{`a`, ` b `}}, + {`a " "`, []string{`a`, ` `}}, // FAILS ! + {`a 'b'`, []string{`a`, `b`}}, + {`a ' b '`, []string{`a`, ` b `}}, + {`a ' '`, []string{`a`, ` `}}, // FAILS ! } func TestSimple(t *testing.T) { @@ -32,7 +38,7 @@ func TestSimple(t *testing.T) { t.Fatal(err) } if !reflect.DeepEqual(args, testcase.expected) { - t.Fatalf("Expected %#v, but %#v:", testcase.expected, args) + t.Fatalf("Expected %#v for %q, but %#v:", testcase.expected, testcase.line, args) } } } From 0ee2d7a36f0d4980dcf451b61d95d8be69866f56 Mon Sep 17 00:00:00 2001 From: Michael Zhang Date: Mon, 27 Aug 2018 10:27:07 +1000 Subject: [PATCH 36/58] Fix issue #16 Split parsed shell env --- shellwords.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/shellwords.go b/shellwords.go index 8fe6c42..ef08086 100644 --- a/shellwords.go +++ b/shellwords.go @@ -88,9 +88,17 @@ loop: backtick += string(r) } else if got { if p.ParseEnv { - buf = replaceEnv(p.Getenv, buf) + parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} + strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) + if err != nil { + return nil, err + } + for _, str := range strs { + args = append(args, str) + } + } else { + args = append(args, buf) } - args = append(args, buf) buf = "" got = false } @@ -180,9 +188,17 @@ loop: if got { if p.ParseEnv { - buf = replaceEnv(p.Getenv, buf) + parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} + strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) + if err != nil { + return nil, err + } + for _, str := range strs { + args = append(args, str) + } + } else { + args = append(args, buf) } - args = append(args, buf) } if escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote { From 621bd1d7b13873cdc044cdbc4ca5b5c73c227141 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 17 Jan 2020 22:27:35 +0900 Subject: [PATCH 37/58] Add test Fixes #16, #17 --- shellwords_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/shellwords_test.go b/shellwords_test.go index cb7f049..6c7bbe0 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -255,7 +255,22 @@ func TestNoEnv(t *testing.T) { if err != nil { t.Fatal(err) } - expected := []string{"echo", ""} + expected := []string{"echo"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } +} + +func TestEnvArguments(t *testing.T) { + os.Setenv("FOO", "bar baz") + + parser := NewParser() + parser.ParseEnv = true + args, err := parser.Parse("echo $FOO") + if err != nil { + t.Fatal(err) + } + expected := []string{"echo", "bar", "baz"} if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } From ff765fb6a1d03940f3f941cc46ab39b33b012754 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 17 Jan 2020 22:36:18 +0900 Subject: [PATCH 38/58] Add test --- go.mod | 2 ++ shellwords_test.go | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/go.mod b/go.mod index 8d96dbd..677a9c4 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/mattn/go-shellwords + +go 1.14 diff --git a/shellwords_test.go b/shellwords_test.go index 6c7bbe0..50ae27b 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -276,6 +276,17 @@ func TestEnvArguments(t *testing.T) { } } +func TestEnvArgumentsFail(t *testing.T) { + os.Setenv("FOO", "bar '") + + parser := NewParser() + parser.ParseEnv = true + _, err := parser.Parse("echo $FOO") + if err == nil { + t.Fatal("Should be an error") + } +} + func TestDupEnv(t *testing.T) { os.Setenv("FOO", "bar") os.Setenv("FOO_BAR", "baz") From 41f4c50104bed637e8740a70efe17e29918eb37a Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 17 Jan 2020 23:00:35 +0900 Subject: [PATCH 39/58] Add test --- shellwords_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 50ae27b..2579a03 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -285,6 +285,19 @@ func TestEnvArgumentsFail(t *testing.T) { if err == nil { t.Fatal("Should be an error") } + _, err = parser.Parse("$FOO") + if err == nil { + t.Fatal("Should be an error") + } + _, err = parser.Parse("echo $FOO") + if err == nil { + t.Fatal("Should be an error") + } + os.Setenv("FOO", "bar `") + _, err = parser.Parse("$FOO ") + if err == nil { + t.Fatal("Should be an error") + } } func TestDupEnv(t *testing.T) { From 15c6c4ba21242f0256740b9417601db2d57a263b Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 17 Jan 2020 23:06:56 +0900 Subject: [PATCH 40/58] Revert change of go.mod --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 677a9c4..927c8c7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/mattn/go-shellwords -go 1.14 +go 1.13 From 58a811f1d6be5c3855704de934dd5ef1d1d6b7db Mon Sep 17 00:00:00 2001 From: "Markus F.X.J. Oberhumer" Date: Sun, 19 Jan 2020 18:57:42 +0100 Subject: [PATCH 41/58] Remove stray comments. --- shellwords_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shellwords_test.go b/shellwords_test.go index 2579a03..cf69343 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -25,10 +25,10 @@ var testcases = []struct { {`var --"bar baz"`, []string{`var`, `--bar baz`}}, {`a "b"`, []string{`a`, `b`}}, {`a " b "`, []string{`a`, ` b `}}, - {`a " "`, []string{`a`, ` `}}, // FAILS ! + {`a " "`, []string{`a`, ` `}}, {`a 'b'`, []string{`a`, `b`}}, {`a ' b '`, []string{`a`, ` b `}}, - {`a ' '`, []string{`a`, ` `}}, // FAILS ! + {`a ' '`, []string{`a`, ` `}}, } func TestSimple(t *testing.T) { From 29f20d48d23c2dc42566f276ba9d31caee6cc467 Mon Sep 17 00:00:00 2001 From: "Markus F.X.J. Oberhumer" Date: Sun, 19 Jan 2020 19:01:22 +0100 Subject: [PATCH 42/58] Convert 2 functions to testcase entries. --- shellwords_test.go | 40 ++-------------------------------------- 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/shellwords_test.go b/shellwords_test.go index cf69343..c168d8b 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -29,6 +29,8 @@ var testcases = []struct { {`a 'b'`, []string{`a`, `b`}}, {`a ' b '`, []string{`a`, ` b `}}, {`a ' '`, []string{`a`, ` `}}, + {"foo bar\\ ", []string{`foo`, `bar `}}, + {`foo "" bar ''`, []string{`foo`, ``, `bar`, ``}}, } func TestSimple(t *testing.T) { @@ -59,44 +61,6 @@ func TestError(t *testing.T) { } } -func TestLastSpace(t *testing.T) { - args, err := Parse("foo bar\\ ") - if err != nil { - t.Fatal(err) - } - if len(args) != 2 { - t.Fatal("Should have two elements") - } - if args[0] != "foo" { - t.Fatal("1st element should be `foo`") - } - if args[1] != "bar " { - t.Fatal("1st element should be `bar `") - } -} - -func TestEmptyArgs(t *testing.T) { - args, err := Parse(`foo "" bar ''`) - if err != nil { - t.Fatal(err) - } - if len(args) != 4 { - t.Fatal("Should have three elements") - } - if args[0] != "foo" { - t.Fatal("1st element should be `foo`") - } - if args[1] != "" { - t.Fatal("2nd element should be empty") - } - if args[2] != "bar" { - t.Fatal("3rd element should be `bar`") - } - if args[3] != "" { - t.Fatal("4th element should be empty") - } -} - func TestShellRun(t *testing.T) { dir, err := os.Getwd() if err != nil { From f722472e3c3c554d2af0ceb420936d573e053008 Mon Sep 17 00:00:00 2001 From: "Markus F.X.J. Oberhumer" Date: Sun, 19 Jan 2020 19:06:31 +0100 Subject: [PATCH 43/58] Add some tests for emtpy strings. --- shellwords_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index c168d8b..f98e9b8 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -12,6 +12,9 @@ var testcases = []struct { line string expected []string }{ + {``, []string{}}, + {`""`, []string{``}}, + {`''`, []string{``}}, {`var --bar=baz`, []string{`var`, `--bar=baz`}}, {`var --bar="baz"`, []string{`var`, `--bar=baz`}}, {`var "--bar=baz"`, []string{`var`, `--bar=baz`}}, From 24faaf1c49706397d3859f54a7dcfd73504e8fbf Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 5 Feb 2020 10:29:09 +0900 Subject: [PATCH 44/58] Fallback SHELL/COMPSEC Fixes #36 --- util_posix.go | 7 +++++-- util_windows.go | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/util_posix.go b/util_posix.go index 3aef2c4..988fc9e 100644 --- a/util_posix.go +++ b/util_posix.go @@ -1,4 +1,4 @@ -// +build !windows,go1.6 +// +build !windows package shellwords @@ -10,7 +10,10 @@ import ( ) func shellRun(line, dir string) (string, error) { - shell := os.Getenv("SHELL") + var shell string + if shell = os.Getenv("SHELL"); shell == "" { + shell = "/bin/sh" + } cmd := exec.Command(shell, "-c", line) if dir != "" { cmd.Dir = dir diff --git a/util_windows.go b/util_windows.go index cda6850..2054673 100644 --- a/util_windows.go +++ b/util_windows.go @@ -1,4 +1,4 @@ -// +build windows,go1.6 +// +build windows package shellwords @@ -10,7 +10,10 @@ import ( ) func shellRun(line, dir string) (string, error) { - shell := os.Getenv("COMSPEC") + var shell string + if shell = os.Getenv("COMSPEC"); shell == "" { + shell = "cmd" + } cmd := exec.Command(shell, "/c", line) if dir != "" { cmd.Dir = dir From fb350fde1da39be61206f9725a2c883a4794af9a Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 5 Feb 2020 10:29:27 +0900 Subject: [PATCH 45/58] Drop go1.5 --- util_go15.go | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 util_go15.go diff --git a/util_go15.go b/util_go15.go deleted file mode 100644 index ddcbf22..0000000 --- a/util_go15.go +++ /dev/null @@ -1,29 +0,0 @@ -// +build !go1.6 - -package shellwords - -import ( - "os" - "os/exec" - "runtime" - "strings" -) - -func shellRun(line, dir string) (string, error) { - var b []byte - var err error - var cmd *exec.Cmd - if runtime.GOOS == "windows" { - cmd = exec.Command(os.Getenv("COMSPEC"), "/c", line) - } else { - cmd = exec.Command(os.Getenv("SHELL"), "-c", line) - } - if dir != "" { - cmd.Dir = dir - } - b, err = cmd.Output() - if err != nil { - return "", err - } - return strings.TrimSpace(string(b)), nil -} From dde38073edf6d8d22fb77a199da50db8f4855754 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 5 Feb 2020 10:41:46 +0900 Subject: [PATCH 46/58] Add test --- shellwords_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index f98e9b8..39feb48 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -85,6 +85,31 @@ func TestShellRun(t *testing.T) { } } +func TestShellRunNoEnv(t *testing.T) { + old := os.Getenv("SHELL") + defer os.Setenv("SHELL", old) + os.Unsetenv("SHELL") + + dir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + pwd, err := shellRun("pwd", "") + if err != nil { + t.Fatal(err) + } + + pwd2, err := shellRun("pwd", path.Join(dir, "/_example")) + if err != nil { + t.Fatal(err) + } + + if pwd == pwd2 { + t.Fatal("`pwd` should be changed") + } +} + func TestBacktick(t *testing.T) { goversion, err := shellRun("go version", "") if err != nil { From 2c2fc2ed33c1ab441460f480e172d578b9906593 Mon Sep 17 00:00:00 2001 From: srinivas32 <70689972+srinivas32@users.noreply.github.com> Date: Fri, 30 Oct 2020 20:34:46 +0530 Subject: [PATCH 47/58] added power support arch ppc64le on yml file. Added power support for the travis.yml file with ppc64le. This is part of the Ubuntu distribution for ppc64le. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index b2904bf..ebd5edd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +arch: + - amd64 + - ppc64le language: go sudo: false go: From 8b3b118283ecc7b3f4ea44e5b369b746fa243208 Mon Sep 17 00:00:00 2001 From: Yoshio HANAWA Date: Sun, 22 Nov 2020 14:40:49 +0900 Subject: [PATCH 48/58] Support arguments consisting only of escaped string --- shellwords.go | 1 + shellwords_test.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/shellwords.go b/shellwords.go index ef08086..9899b3c 100644 --- a/shellwords.go +++ b/shellwords.go @@ -70,6 +70,7 @@ loop: if escaped { buf += string(r) escaped = false + got = true continue } diff --git a/shellwords_test.go b/shellwords_test.go index 39feb48..16044d4 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -34,6 +34,8 @@ var testcases = []struct { {`a ' '`, []string{`a`, ` `}}, {"foo bar\\ ", []string{`foo`, `bar `}}, {`foo "" bar ''`, []string{`foo`, ``, `bar`, ``}}, + {`foo \\`, []string{`foo`, `\`}}, + {`foo \& bar`, []string{`foo`, `&`, `bar`}}, } func TestSimple(t *testing.T) { From e741fdaabdc0d2aedcfbc3601dbafa4ae2f302c2 Mon Sep 17 00:00:00 2001 From: Yoshio HANAWA Date: Mon, 23 Nov 2020 17:01:13 +0900 Subject: [PATCH 49/58] Fix to return rune-based position for parser.Position --- shellwords.go | 4 +++- shellwords_test.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/shellwords.go b/shellwords.go index ef08086..4712c62 100644 --- a/shellwords.go +++ b/shellwords.go @@ -65,8 +65,10 @@ func (p *Parser) Parse(line string) ([]string, error) { pos := -1 got := false + i := -1 loop: - for i, r := range line { + for _, r := range line { + i++ if escaped { buf += string(r) escaped = false diff --git a/shellwords_test.go b/shellwords_test.go index 39feb48..06957cc 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -321,12 +321,12 @@ func TestHaveMore(t *testing.T) { parser := NewParser() parser.ParseEnv = true - line := "echo foo; seq 1 10" + line := "echo 🍺; seq 1 10" args, err := parser.Parse(line) if err != nil { t.Fatalf(err.Error()) } - expected := []string{"echo", "foo"} + expected := []string{"echo", "🍺"} if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } From 866d8eb558abdb0a447d93b9c58f41b98b6c37a6 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 1 Dec 2020 01:08:44 +0900 Subject: [PATCH 50/58] Fixes #44 --- shellwords.go | 133 +++++++++++++++++++++++++++++++++++---------- shellwords_test.go | 36 ++++++++++-- 2 files changed, 135 insertions(+), 34 deletions(-) diff --git a/shellwords.go b/shellwords.go index 576b792..236c14d 100644 --- a/shellwords.go +++ b/shellwords.go @@ -1,10 +1,12 @@ package shellwords import ( + "bytes" "errors" "os" "regexp" "strings" + "unicode" ) var ( @@ -27,13 +29,72 @@ func replaceEnv(getenv func(string) string, s string) string { getenv = os.Getenv } - return envRe.ReplaceAllStringFunc(s, func(s string) string { - s = s[1:] - if s[0] == '{' { - s = s[1 : len(s)-1] + var buf bytes.Buffer + rs := []rune(s) + for i := 0; i < len(rs); i++ { + r := rs[i] + if r == '\\' { + i++ + if i == len(rs) { + break + } + buf.WriteRune(rs[i]) + continue + } else if r == '$' { + i++ + if i == len(rs) { + buf.WriteRune(r) + break + } + if rs[i] == 0x7b { + i++ + p := i + for ; i < len(rs); i++ { + r = rs[i] + if r == '\\' { + i++ + if i == len(rs) { + return s + } + continue + } + if r == 0x7d || (!unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r)) { + break + } + } + if r != 0x7d { + return s + } + if i > p { + buf.WriteString(getenv(s[p:i])) + } + } else { + p := i + for ; i < len(rs); i++ { + r := rs[i] + if r == '\\' { + i++ + if i == len(rs) { + return s + } + continue + } + if !unicode.IsLetter(r) && r != '_' && !unicode.IsDigit(r) { + break + } + } + if i > p { + buf.WriteString(getenv(s[p:i])) + i-- + } else { + buf.WriteString(s[p:]) + } + } + } else { + buf.WriteRune(r) } - return getenv(s) - }) + } + return buf.String() } type Parser struct { @@ -56,6 +117,14 @@ func NewParser() *Parser { } } +type argType int + +const ( + argNo argType = iota + argSingle + argQuoted +) + func (p *Parser) Parse(line string) ([]string, error) { args := []string{} buf := "" @@ -63,7 +132,7 @@ func (p *Parser) Parse(line string) ([]string, error) { backtick := "" pos := -1 - got := false + got := argNo i := -1 loop: @@ -89,21 +158,25 @@ loop: if singleQuoted || doubleQuoted || backQuote || dollarQuote { buf += string(r) backtick += string(r) - } else if got { + } else if got != argNo { if p.ParseEnv { - parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} - strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) - if err != nil { - return nil, err - } - for _, str := range strs { - args = append(args, str) + if got == argSingle { + parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} + strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) + if err != nil { + return nil, err + } + for _, str := range strs { + args = append(args, str) + } + } else { + args = append(args, replaceEnv(p.Getenv, buf)) } } else { args = append(args, buf) } buf = "" - got = false + got = argNo } continue } @@ -156,7 +229,7 @@ loop: case '"': if !singleQuoted && !dollarQuote { if doubleQuoted { - got = true + got = argQuoted } doubleQuoted = !doubleQuoted continue @@ -164,7 +237,7 @@ loop: case '\'': if !doubleQuoted && !dollarQuote { if singleQuoted { - got = true + got = argSingle } singleQuoted = !singleQuoted continue @@ -174,7 +247,7 @@ loop: if r == '>' && len(buf) > 0 { if c := buf[0]; '0' <= c && c <= '9' { i -= 1 - got = false + got = argNo } } pos = i @@ -182,22 +255,26 @@ loop: } } - got = true + got = argSingle buf += string(r) if backQuote || dollarQuote { backtick += string(r) } } - if got { + if got != argNo { if p.ParseEnv { - parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} - strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) - if err != nil { - return nil, err - } - for _, str := range strs { - args = append(args, str) + if got == argSingle { + parser := &Parser{ParseEnv: false, ParseBacktick: false, Position: 0, Dir: p.Dir} + strs, err := parser.Parse(replaceEnv(p.Getenv, buf)) + if err != nil { + return nil, err + } + for _, str := range strs { + args = append(args, str) + } + } else { + args = append(args, replaceEnv(p.Getenv, buf)) } } else { args = append(args, buf) diff --git a/shellwords_test.go b/shellwords_test.go index cfe818c..b32a493 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -288,9 +288,9 @@ func TestEnvArgumentsFail(t *testing.T) { t.Fatal("Should be an error") } os.Setenv("FOO", "bar `") - _, err = parser.Parse("$FOO ") + result, err := parser.Parse("$FOO ") if err == nil { - t.Fatal("Should be an error") + t.Fatal("Should be an error: ", result) } } @@ -300,20 +300,20 @@ func TestDupEnv(t *testing.T) { parser := NewParser() parser.ParseEnv = true - args, err := parser.Parse("echo $$FOO$") + args, err := parser.Parse("echo $FOO$") if err != nil { t.Fatal(err) } - expected := []string{"echo", "$bar$"} + expected := []string{"echo", "bar$"} if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } - args, err = parser.Parse("echo $${FOO_BAR}$") + args, err = parser.Parse("echo ${FOO_BAR}$") if err != nil { t.Fatal(err) } - expected = []string{"echo", "$baz$"} + expected = []string{"echo", "baz$"} if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) } @@ -383,3 +383,27 @@ func TestBackquoteInFlag(t *testing.T) { t.Fatalf("Expected %#v, but %#v:", expected, args) } } + +func TestEnvInQuoted(t *testing.T) { + os.Setenv("FOO", "bar") + + parser := NewParser() + parser.ParseEnv = true + args, err := parser.Parse(`ssh 127.0.0.1 "echo $FOO"`) + if err != nil { + panic(err) + } + expected := []string{"ssh", "127.0.0.1", "echo bar"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } + + args, err = parser.Parse(`ssh 127.0.0.1 "echo \\$FOO"`) + if err != nil { + panic(err) + } + expected = []string{"ssh", "127.0.0.1", "echo $FOO"} + if !reflect.DeepEqual(args, expected) { + t.Fatalf("Expected %#v, but %#v:", expected, args) + } +} From ebba8f70cc41545f6ae4f1a0e61a082f957be768 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Tue, 1 Dec 2020 01:18:32 +0900 Subject: [PATCH 51/58] Fix build --- shellwords.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shellwords.go b/shellwords.go index 236c14d..fc29fa4 100644 --- a/shellwords.go +++ b/shellwords.go @@ -141,7 +141,7 @@ loop: if escaped { buf += string(r) escaped = false - got = true + got = argSingle continue } From a6d5f0bb915c6fe38e45794842eb147367886bb1 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:28:45 +0900 Subject: [PATCH 52/58] Use fmt.Errorf to be able to unwrap error --- shellwords_test.go | 8 +++++--- util_posix.go | 4 ++-- util_windows.go | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/shellwords_test.go b/shellwords_test.go index b32a493..8b3986c 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -1,8 +1,10 @@ package shellwords import ( + "errors" "go/build" "os" + "os/exec" "path" "reflect" "testing" @@ -183,9 +185,9 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an error") } - expected := "exit status 2:go Version: unknown command\nRun 'go help' for usage.\n" - if expected != err.Error() { - t.Fatalf("Expected %q, but %q", expected, err.Error()) + var eerr *exec.ExitError + if !errors.As(err, &eerr) { + t.Fatal("Should be able to unwrap to *exec.ExitError") } _, err = parser.Parse(`echo $(echo1)`) if err == nil { diff --git a/util_posix.go b/util_posix.go index 988fc9e..b56a901 100644 --- a/util_posix.go +++ b/util_posix.go @@ -3,7 +3,7 @@ package shellwords import ( - "errors" + "fmt" "os" "os/exec" "strings" @@ -23,7 +23,7 @@ func shellRun(line, dir string) (string, error) { if eerr, ok := err.(*exec.ExitError); ok { b = eerr.Stderr } - return "", errors.New(err.Error() + ":" + string(b)) + return "", fmt.Errorf("%s: %w", string(b), err) } return strings.TrimSpace(string(b)), nil } diff --git a/util_windows.go b/util_windows.go index 2054673..fd738a7 100644 --- a/util_windows.go +++ b/util_windows.go @@ -3,7 +3,7 @@ package shellwords import ( - "errors" + "fmt" "os" "os/exec" "strings" @@ -23,7 +23,7 @@ func shellRun(line, dir string) (string, error) { if eerr, ok := err.(*exec.ExitError); ok { b = eerr.Stderr } - return "", errors.New(err.Error() + ":" + string(b)) + return "", fmt.Errorf("%s: %w", string(b), err) } return strings.TrimSpace(string(b)), nil } From e8fb9a4639734fbd1ec04d06cb961116637a616f Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:29:16 +0900 Subject: [PATCH 53/58] Add github actions workflow file --- .github/workflows/ci.yml | 23 +++++++++++++++++++++++ README.md | 1 + 2 files changed, 24 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1218c69 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: ci +on: + pull_request: + branches: + - master + push: + branches: + - master +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + go: ["1.14", "1.15"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + - run: | + go test -cover -coverprofile coverage.txt -race -v ./... + - uses: codecov/codecov-action@v1 diff --git a/README.md b/README.md index e91902f..4d9d8a8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![codecov](https://codecov.io/gh/mattn/go-shellwords/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-shellwords) [![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords) [![GoDoc](https://godoc.org/github.com/mattn/go-shellwords?status.svg)](http://godoc.org/github.com/mattn/go-shellwords) +[![ci](https://github.com/mattn/go-shellwords/ci/badge.svg)](https://github.com/mattn/go-shellwords/actions) Parse line as shell words. From bd5dbc6867ea9448e2232f1731f04d95f98d22c0 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:30:05 +0900 Subject: [PATCH 54/58] Migrate from godoc.org to pkg.go.dev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4d9d8a8..ce71816 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![codecov](https://codecov.io/gh/mattn/go-shellwords/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-shellwords) [![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords) -[![GoDoc](https://godoc.org/github.com/mattn/go-shellwords?status.svg)](http://godoc.org/github.com/mattn/go-shellwords) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/mattn/go-shellwords)](https://pkg.go.dev/github.com/mattn/go-shellwords) [![ci](https://github.com/mattn/go-shellwords/ci/badge.svg)](https://github.com/mattn/go-shellwords/actions) Parse line as shell words. From 57cd80ab9b6602eba17eed7a52191ca56475d114 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:32:34 +0900 Subject: [PATCH 55/58] Remove unused regexp --- shellwords.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/shellwords.go b/shellwords.go index fc29fa4..f9dca9a 100644 --- a/shellwords.go +++ b/shellwords.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "os" - "regexp" "strings" "unicode" ) @@ -14,8 +13,6 @@ var ( ParseBacktick bool = false ) -var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`) - func isSpace(r rune) bool { switch r { case ' ', '\t', '\r', '\n': From 030cc65f0b04bbec2f08aaf01e6098317a6fec07 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:34:20 +0900 Subject: [PATCH 56/58] Add error check --- shellwords_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/shellwords_test.go b/shellwords_test.go index 8b3986c..b79d591 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -151,6 +151,9 @@ func TestBacktick(t *testing.T) { parser.ParseBacktick = false args, err = parser.Parse(`echo $(echo "foo")`) + if err != nil { + t.Fatal(err) + } expected = []string{"echo", `$(echo "foo")`} if !reflect.DeepEqual(args, expected) { t.Fatalf("Expected %#v, but %#v:", expected, args) From 31365670aa7dfd1cbe9c9abc35593e7e9f97adec Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Wed, 2 Dec 2020 00:35:37 +0900 Subject: [PATCH 57/58] Replace loop with unpacking --- shellwords.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/shellwords.go b/shellwords.go index f9dca9a..bd80931 100644 --- a/shellwords.go +++ b/shellwords.go @@ -163,9 +163,7 @@ loop: if err != nil { return nil, err } - for _, str := range strs { - args = append(args, str) - } + args = append(args, strs...) } else { args = append(args, replaceEnv(p.Getenv, buf)) } @@ -267,9 +265,7 @@ loop: if err != nil { return nil, err } - for _, str := range strs { - args = append(args, str) - } + args = append(args, strs...) } else { args = append(args, replaceEnv(p.Getenv, buf)) } From e976e985efa0a192fd8f8f771301a320388cb244 Mon Sep 17 00:00:00 2001 From: Mitsuo Heijo Date: Fri, 4 Dec 2020 23:33:01 +0900 Subject: [PATCH 58/58] Add a new function to parse a line containing environment variables --- README.md | 6 ++++++ shellwords.go | 29 +++++++++++++++++++++++++++++ shellwords_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/README.md b/README.md index e91902f..8840672 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,12 @@ args, err := shellwords.Parse("./foo --bar=baz") // args should be ["./foo", "--bar=baz"] ``` +```go +envs, args, err := shellwords.ParseWithEnvs("FOO=foo BAR=baz ./foo --bar=baz") +// envs should be ["FOO=foo", "BAR=baz"] +// args should be ["./foo", "--bar=baz"] +``` + ```go os.Setenv("FOO", "bar") p := shellwords.NewParser() diff --git a/shellwords.go b/shellwords.go index fc29fa4..4f11960 100644 --- a/shellwords.go +++ b/shellwords.go @@ -290,6 +290,35 @@ loop: return args, nil } +func (p *Parser) ParseWithEnvs(line string) (envs []string, args []string, err error) { + _args, err := p.Parse(line) + if err != nil { + return nil, nil, err + } + envs = []string{} + args = []string{} + parsingEnv := true + for _, arg := range _args { + if parsingEnv && isEnv(arg) { + envs = append(envs, arg) + } else { + if parsingEnv { + parsingEnv = false + } + args = append(args, arg) + } + } + return envs, args, nil +} + +func isEnv(arg string) bool { + return len(strings.Split(arg, "=")) == 2 +} + func Parse(line string) ([]string, error) { return NewParser().Parse(line) } + +func ParseWithEnvs(line string) (envs []string, args []string, err error) { + return NewParser().ParseWithEnvs(line) +} diff --git a/shellwords_test.go b/shellwords_test.go index b32a493..4f94528 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -407,3 +407,45 @@ func TestEnvInQuoted(t *testing.T) { t.Fatalf("Expected %#v, but %#v:", expected, args) } } + +func TestParseWithEnvs(t *testing.T) { + tests := []struct { + line string + wantEnvs, wantArgs []string + }{ + { + line: "FOO=foo cmd --args=A=B", + wantEnvs: []string{"FOO=foo"}, + wantArgs: []string{"cmd", "--args=A=B"}, + }, + { + line: "FOO=foo BAR=bar cmd --args=A=B -A=B", + wantEnvs: []string{"FOO=foo", "BAR=bar"}, + wantArgs: []string{"cmd", "--args=A=B", "-A=B"}, + }, + { + line: `sh -c "FOO=foo BAR=bar cmd --args=A=B -A=B"`, + wantEnvs: []string{}, + wantArgs: []string{"sh", "-c", "FOO=foo BAR=bar cmd --args=A=B -A=B"}, + }, + { + line: "cmd --args=A=B -A=B", + wantEnvs: []string{}, + wantArgs: []string{"cmd", "--args=A=B", "-A=B"}, + }, + } + for _, tt := range tests { + t.Run(tt.line, func(t *testing.T) { + envs, args, err := ParseWithEnvs(tt.line) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(envs, tt.wantEnvs) { + t.Errorf("Expected %#v, but %#v", tt.wantEnvs, envs) + } + if !reflect.DeepEqual(args, tt.wantArgs) { + t.Errorf("Expected %#v, but %#v", tt.wantArgs, args) + } + }) + } +}