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'] 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/.travis.yml b/.travis.yml index 16d1430..ebd5edd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,16 @@ +arch: + - amd64 + - ppc64le 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) diff --git a/README.md b/README.md index b1d235c..bdd5319 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # 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) +[![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. @@ -12,6 +14,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/_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:]) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..927c8c7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/mattn/go-shellwords + +go 1.13 diff --git a/go.test.sh b/go.test.sh new file mode 100755 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 diff --git a/shellwords.go b/shellwords.go index 1078039..01afd94 100644 --- a/shellwords.go +++ b/shellwords.go @@ -1,9 +1,11 @@ package shellwords import ( + "bytes" "errors" "os" - "regexp" + "strings" + "unicode" ) var ( @@ -11,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': @@ -21,40 +21,124 @@ func isSpace(r rune) bool { return false } -func replaceEnv(s string) string { - return envRe.ReplaceAllStringFunc(s, func(s string) string { - s = s[1:] - if s[0] == '{' { - s = s[1 : len(s)-1] +func replaceEnv(getenv func(string) string, s string) string { + if getenv == nil { + getenv = os.Getenv + } + + 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 os.Getenv(s) - }) + } + return buf.String() } type Parser struct { ParseEnv bool ParseBacktick bool Position int + Dir string + + // 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, + Dir: "", + } } +type argType int + +const ( + argNo argType = iota + argSingle + argQuoted +) + func (p *Parser) Parse(line string) ([]string, error) { args := []string{} buf := "" - var escaped, doubleQuoted, singleQuoted, backQuote bool + var escaped, doubleQuoted, singleQuoted, backQuote, dollarQuote bool backtick := "" pos := -1 - got := false + got := argNo + i := -1 loop: - for i, r := range line { + for _, r := range line { + i++ if escaped { buf += string(r) escaped = false + got = argSingle continue } @@ -68,30 +152,40 @@ loop: } if isSpace(r) { - if singleQuoted || doubleQuoted || backQuote { + if singleQuoted || doubleQuoted || backQuote || dollarQuote { buf += string(r) backtick += string(r) - } else if got { + } else if got != argNo { if p.ParseEnv { - buf = replaceEnv(buf) + 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 + } + args = append(args, strs...) + } else { + args = append(args, replaceEnv(p.Getenv, buf)) + } + } else { + args = append(args, buf) } - args = append(args, buf) buf = "" - got = false + got = argNo } continue } switch r { case '`': - if !singleQuoted && !doubleQuoted { + 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 } - buf = out + buf = buf[:len(buf)-len(backtick)] + out } backtick = "" backQuote = !backQuote @@ -100,38 +194,87 @@ loop: backtick = "" backQuote = !backQuote } + case ')': + if !singleQuoted && !doubleQuoted && !backQuote { + if p.ParseBacktick { + if dollarQuote { + out, err := shellRun(backtick, p.Dir) + if err != nil { + return nil, err + } + buf = buf[:len(buf)-len(backtick)-2] + out + } + backtick = "" + dollarQuote = !dollarQuote + continue + } + backtick = "" + dollarQuote = !dollarQuote + } + case '(': + if !singleQuoted && !doubleQuoted && !backQuote { + if !dollarQuote && strings.HasSuffix(buf, "$") { + dollarQuote = true + buf += "(" + continue + } else { + return nil, errors.New("invalid command line string") + } + } case '"': - if !singleQuoted { + if !singleQuoted && !dollarQuote { + if doubleQuoted { + got = argQuoted + } doubleQuoted = !doubleQuoted continue } case '\'': - if !doubleQuoted { + if !doubleQuoted && !dollarQuote { + if singleQuoted { + got = argSingle + } singleQuoted = !singleQuoted 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 + got = argNo + } + } pos = i break loop } } - got = true + got = argSingle buf += string(r) - if backQuote { + if backQuote || dollarQuote { backtick += string(r) } } - if got { + if got != argNo { if p.ParseEnv { - buf = replaceEnv(buf) + 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 + } + args = append(args, strs...) + } else { + args = append(args, replaceEnv(p.Getenv, buf)) + } + } else { + args = append(args, buf) } - args = append(args, buf) } - if escaped || singleQuoted || doubleQuoted || backQuote { + if escaped || singleQuoted || doubleQuoted || backQuote || dollarQuote { return nil, errors.New("invalid command line string") } @@ -140,6 +283,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 63b196b..57b8395 100644 --- a/shellwords_test.go +++ b/shellwords_test.go @@ -1,7 +1,11 @@ package shellwords import ( + "errors" + "go/build" "os" + "os/exec" + "path" "reflect" "testing" ) @@ -10,6 +14,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`}}, @@ -21,6 +28,16 @@ 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`, ` `}}, + {`a 'b'`, []string{`a`, `b`}}, + {`a ' b '`, []string{`a`, ` b `}}, + {`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) { @@ -30,7 +47,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) } } } @@ -51,24 +68,54 @@ func TestError(t *testing.T) { } } -func TestLastSpace(t *testing.T) { - args, err := Parse("foo bar\\ ") +func TestShellRun(t *testing.T) { + dir, err := os.Getwd() if err != nil { t.Fatal(err) } - if len(args) != 2 { - t.Fatal("Should have two elements") + + pwd, err := shellRun("pwd", "") + if err != nil { + t.Fatal(err) } - if args[0] != "foo" { - t.Fatal("1st element should be `foo`") + + pwd2, err := shellRun("pwd", path.Join(dir, "/_example")) + if err != nil { + t.Fatal(err) } - if args[1] != "bar " { - t.Fatal("1st element should be `bar `") + + if pwd == pwd2 { + t.Fatal("`pwd` should be changed") + } +} + +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") + goversion, err := shellRun("go version", "") if err != nil { t.Fatal(err) } @@ -83,6 +130,55 @@ 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) + } + + 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")`) + if err != nil { + t.Fatal(err) + } + expected = []string{"echo", `$(echo "foo")`} + 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 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) { @@ -92,6 +188,34 @@ func TestBacktickError(t *testing.T) { if err == nil { t.Fatal("Should be an 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 { + 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") + } + _, 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) { @@ -109,6 +233,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 @@ -116,32 +254,71 @@ 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) + } +} + +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") + } + _, 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 `") + result, err := parser.Parse("$FOO ") + if err == nil { + t.Fatal("Should be an error: ", result) + } +} + func TestDupEnv(t *testing.T) { os.Setenv("FOO", "bar") os.Setenv("FOO_BAR", "baz") 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) } @@ -151,12 +328,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) } @@ -179,3 +356,101 @@ 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") + } +} + +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) + } +} + +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) + } +} + +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) + } + }) + } +} diff --git a/util_posix.go b/util_posix.go index 4f8ac55..b56a901 100644 --- a/util_posix.go +++ b/util_posix.go @@ -3,17 +3,27 @@ package shellwords import ( - "errors" + "fmt" "os" "os/exec" "strings" ) -func shellRun(line string) (string, error) { - shell := os.Getenv("SHELL") - b, err := exec.Command(shell, "-c", line).Output() +func shellRun(line, dir string) (string, error) { + var shell string + if shell = os.Getenv("SHELL"); shell == "" { + shell = "/bin/sh" + } + cmd := exec.Command(shell, "-c", line) + if dir != "" { + cmd.Dir = dir + } + b, err := cmd.Output() if err != nil { - return "", errors.New(err.Error() + ":" + string(b)) + if eerr, ok := err.(*exec.ExitError); ok { + b = eerr.Stderr + } + 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 7cad4cf..fd738a7 100644 --- a/util_windows.go +++ b/util_windows.go @@ -1,17 +1,29 @@ +// +build windows + package shellwords import ( - "errors" + "fmt" "os" "os/exec" "strings" ) -func shellRun(line string) (string, error) { - shell := os.Getenv("COMSPEC") - b, err := exec.Command(shell, "/c", line).Output() +func shellRun(line, dir string) (string, error) { + var shell string + if shell = os.Getenv("COMSPEC"); shell == "" { + shell = "cmd" + } + cmd := exec.Command(shell, "/c", line) + if dir != "" { + cmd.Dir = dir + } + b, err := cmd.Output() if err != nil { - return "", errors.New(err.Error() + ":" + string(b)) + if eerr, ok := err.(*exec.ExitError); ok { + b = eerr.Stderr + } + return "", fmt.Errorf("%s: %w", string(b), err) } return strings.TrimSpace(string(b)), nil }