diff --git a/go.mod b/go.mod index 1ab09f5..5aef076 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.21 require ( github.com/fatih/color v1.16.0 github.com/marcusolsson/tui-go v0.4.0 - github.com/mattn/go-zglob v0.0.1 - github.com/samsarahq/go v0.0.0-20230622164605-46203ca3a6d8 + github.com/mattn/go-zglob v0.0.6 + github.com/samsarahq/go v0.0.0-20240911161602-ad0977dc0ab1 github.com/stretchr/testify v1.8.4 go.uber.org/multierr v1.11.0 golang.org/x/sync v0.6.0 - mvdan.cc/sh v2.5.1-0.20180917103936-65d8e2a4b156+incompatible + mvdan.cc/sh v2.6.4+incompatible ) require ( diff --git a/go.sum b/go.sum index 495fd99..589a87b 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4-0.20181117163326-c88d7e5f2e24 h1:rOzgGniffGANEp43kp5fz6AbxucSI0rkpqUag2RUT9M= github.com/mattn/go-runewidth v0.0.4-0.20181117163326-c88d7e5f2e24/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY= -github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= +github.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A= +github.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -43,8 +43,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/samsarahq/go v0.0.0-20230622164605-46203ca3a6d8 h1:3k6siUM/qYdvMe7oMO/VOfD3Hv4oMDSd7YwdZn16o4s= -github.com/samsarahq/go v0.0.0-20230622164605-46203ca3a6d8/go.mod h1:3sbyY5uD6qrfO+gLTor5Fny8qozzWm1m8Kk6KY04M0o= +github.com/samsarahq/go v0.0.0-20240911161602-ad0977dc0ab1 h1:Gy8FWqhuYbXTc/GjhFARCgdjdquiTTOj69ucSrQFyXI= +github.com/samsarahq/go v0.0.0-20240911161602-ad0977dc0ab1/go.mod h1:3sbyY5uD6qrfO+gLTor5Fny8qozzWm1m8Kk6KY04M0o= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= @@ -72,5 +72,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh v2.5.1-0.20180917103936-65d8e2a4b156+incompatible h1:YLmgyUXMWLcF6Bk3uCn6V2APFmu0tUnuaiJ14mmkSHs= -mvdan.cc/sh v2.5.1-0.20180917103936-65d8e2a4b156+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= +mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= +mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= diff --git a/shell/shell.go b/shell/shell.go index 8835eb3..8d054ab 100644 --- a/shell/shell.go +++ b/shell/shell.go @@ -35,15 +35,6 @@ func Stdin(reader io.Reader) RunOption { } } -// Env sets the environment variables for the command. -func Env(vars map[string]string) RunOption { - return func(r *interp.Runner) { - for k, v := range vars { - r.Env.Set(k, v) - } - } -} - // Run executes a shell command. func Run(ctx context.Context, command string, opts ...RunOption) error { p, err := syntax.NewParser().Parse(strings.NewReader(command), "") diff --git a/vendor/github.com/mattn/go-zglob/.travis.yml b/vendor/github.com/mattn/go-zglob/.travis.yml deleted file mode 100644 index 5f72342..0000000 --- a/vendor/github.com/mattn/go-zglob/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: go -go: - - tip -sudo: false -script: - - go test diff --git a/vendor/github.com/mattn/go-zglob/README.md b/vendor/github.com/mattn/go-zglob/README.md index c0d4d11..ca9aeaa 100644 --- a/vendor/github.com/mattn/go-zglob/README.md +++ b/vendor/github.com/mattn/go-zglob/README.md @@ -1,6 +1,6 @@ # go-zglob -[![Build Status](https://travis-ci.org/mattn/go-zglob.svg)](https://travis-ci.org/mattn/go-zglob) +[![Build Status](https://github.com/mattn/go-zglob/actions/workflows/go.yml/badge.svg)](https://github.com/mattn/go-zglob/actions/workflows/go.yml) zglob @@ -12,10 +12,18 @@ matches, err := zglob.Glob(`./foo/b*/**/z*.txt`) ## Installation -``` +For using library: + +```console $ go get github.com/mattn/go-zglob ``` +For using command: + +```console +$ go install github.com/mattn/go-zglob/cmd/zglob@latest +``` + ## License MIT diff --git a/vendor/github.com/mattn/go-zglob/fastwalk/fastwalk_portable.go b/vendor/github.com/mattn/go-zglob/fastwalk/fastwalk_portable.go index e8ea50d..17719cd 100644 --- a/vendor/github.com/mattn/go-zglob/fastwalk/fastwalk_portable.go +++ b/vendor/github.com/mattn/go-zglob/fastwalk/fastwalk_portable.go @@ -18,7 +18,7 @@ import ( func readDir(dirName string, fn func(dirName, entName string, typ os.FileMode) error) error { fis, err := ioutil.ReadDir(dirName) if err != nil { - return err + return nil } for _, fi := range fis { if err := fn(dirName, fi.Name(), fi.Mode()&os.ModeType); err != nil { diff --git a/vendor/github.com/mattn/go-zglob/zglob.go b/vendor/github.com/mattn/go-zglob/zglob.go index bcc3cc0..3fb50b9 100644 --- a/vendor/github.com/mattn/go-zglob/zglob.go +++ b/vendor/github.com/mattn/go-zglob/zglob.go @@ -1,8 +1,10 @@ package zglob import ( + "bytes" "fmt" "os" + "path" "path/filepath" "regexp" "runtime" @@ -18,21 +20,41 @@ var ( ) type zenv struct { - dre *regexp.Regexp + dirmask string fre *regexp.Regexp pattern string root string } +func toSlash(path string) string { + if filepath.Separator == '/' { + return path + } + var buf bytes.Buffer + cc := []rune(path) + for i := 0; i < len(cc); i++ { + if i < len(cc)-2 && cc[i] == '\\' && (cc[i+1] == '{' || cc[i+1] == '}') { + buf.WriteRune(cc[i]) + buf.WriteRune(cc[i+1]) + i++ + } else if cc[i] == '\\' { + buf.WriteRune('/') + } else { + buf.WriteRune(cc[i]) + } + } + return buf.String() +} + func New(pattern string) (*zenv, error) { globmask := "" root := "" - for n, i := range strings.Split(filepath.ToSlash(pattern), "/") { - if root == "" && strings.Index(i, "*") != -1 { + for n, i := range strings.Split(toSlash(pattern), "/") { + if root == "" && strings.ContainsAny(i, "*{") { if globmask == "" { root = "." } else { - root = filepath.ToSlash(globmask) + root = toSlash(globmask) } } if n == 0 && i == "~" { @@ -46,7 +68,7 @@ func New(pattern string) (*zenv, error) { i = strings.Trim(strings.Trim(os.Getenv(i[1:]), "()"), `"`) } - globmask = filepath.Join(globmask, i) + globmask = path.Join(globmask, i) if n == 0 { if runtime.GOOS == "windows" && filepath.VolumeName(i) != "" { globmask = i + "/" @@ -57,7 +79,7 @@ func New(pattern string) (*zenv, error) { } if root == "" { return &zenv{ - dre: nil, + dirmask: "", fre: nil, pattern: pattern, root: "", @@ -66,50 +88,123 @@ func New(pattern string) (*zenv, error) { if globmask == "" { globmask = "." } - globmask = filepath.ToSlash(filepath.Clean(globmask)) + globmask = toSlash(path.Clean(globmask)) cc := []rune(globmask) - dirmask := "" - filemask := "" + var dirmask strings.Builder + var filemask strings.Builder + staticDir := true for i := 0; i < len(cc); i++ { - if cc[i] == '*' { + if i < len(cc)-2 && cc[i] == '\\' { + i++ + fmt.Fprintf(&filemask, "[\\x%02X]", cc[i]) + if staticDir { + dirmask.WriteRune(cc[i]) + } + } else if cc[i] == '*' { + staticDir = false if i < len(cc)-2 && cc[i+1] == '*' && cc[i+2] == '/' { - filemask += "(.*/)?" - if dirmask == "" { - dirmask = filemask - } + filemask.WriteString("(.*/)?") i += 2 } else { - filemask += "[^/]*" + filemask.WriteString("[^/]*") + } + } else if cc[i] == '[' { // range + staticDir = false + var b strings.Builder + for j := i + 1; j < len(cc); j++ { + if cc[j] == ']' { + i = j + break + } else { + b.WriteRune(cc[j]) + } + } + if pattern := b.String(); pattern != "" { + filemask.WriteByte('[') + filemask.WriteString(pattern) + filemask.WriteByte(']') + continue } } else { + if cc[i] == '{' { + staticDir = false + var b strings.Builder + for j := i + 1; j < len(cc); j++ { + if cc[j] == ',' { + b.WriteByte('|') + } else if cc[j] == '}' { + i = j + break + } else { + c := cc[j] + if c == '/' { + b.WriteRune(c) + } else if ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c { + b.WriteRune(c) + } else { + fmt.Fprintf(&b, "[\\x%02X]", c) + } + } + } + if pattern := b.String(); pattern != "" { + filemask.WriteByte('(') + filemask.WriteString(pattern) + filemask.WriteByte(')') + continue + } + } else if i < len(cc)-1 && cc[i] == '!' && cc[i+1] == '(' { + i++ + var b strings.Builder + for j := i + 1; j < len(cc); j++ { + if cc[j] == ')' { + i = j + break + } else { + c := cc[j] + fmt.Fprintf(&b, "[^\\x%02X/]*", c) + } + } + if pattern := b.String(); pattern != "" { + if dirmask.Len() == 0 { + m := filemask.String() + dirmask.WriteString(m) + root = m + } + filemask.WriteString(pattern) + continue + } + } c := cc[i] if c == '/' || ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || 255 < c { - filemask += string(c) + filemask.WriteRune(c) } else { - filemask += fmt.Sprintf("[\\x%02X]", c) + fmt.Fprintf(&filemask, "[\\x%02X]", c) } - if c == '/' && dirmask == "" && strings.Index(filemask, "*") != -1 { - dirmask = filemask + if staticDir { + dirmask.WriteRune(c) } } } - if dirmask == "" { - dirmask = filemask - } - if len(filemask) > 0 && filemask[len(filemask)-1] == '/' { + if m := filemask.String(); len(m) > 0 && m[len(m)-1] == '/' { if root == "" { - root = filemask + root = m } - filemask += "[^/]*" + filemask.WriteString("[^/]*") } + var pat string if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { - dirmask = "(?i:" + dirmask + ")" - filemask = "(?i:" + filemask + ")" + pat = "^(?i:" + filemask.String() + ")$" + } else { + pat = "^" + filemask.String() + "$" + } + fre, err := regexp.Compile(pat) + if err != nil { + return nil, err } return &zenv{ - dre: regexp.MustCompile("^" + dirmask), - fre: regexp.MustCompile("^" + filemask + "$"), + dirmask: path.Dir(dirmask.String()) + "/", + fre: fre, pattern: pattern, root: filepath.Clean(root), }, nil @@ -138,7 +233,7 @@ func glob(pattern string, followSymlinks bool) ([]string, error) { relative := !filepath.IsAbs(pattern) matches := []string{} - fastwalk.FastWalk(zenv.root, func(path string, info os.FileMode) error { + err = fastwalk.FastWalk(zenv.root, func(path string, info os.FileMode) error { if zenv.root == "." && len(zenv.root) < len(path) { path = path[len(zenv.root)+1:] } @@ -164,7 +259,7 @@ func glob(pattern string, followSymlinks bool) ([]string, error) { mu.Unlock() return nil } - if !zenv.dre.MatchString(path + "/") { + if len(path) < len(zenv.dirmask) && !strings.HasPrefix(zenv.dirmask, path+"/") { return filepath.SkipDir } } @@ -179,6 +274,11 @@ func glob(pattern string, followSymlinks bool) ([]string, error) { } return nil }) + + if err != nil { + return nil, err + } + return matches, nil } diff --git a/vendor/github.com/samsarahq/go/oops/oops.go b/vendor/github.com/samsarahq/go/oops/oops.go index 9a0c5bc..7531a09 100644 --- a/vendor/github.com/samsarahq/go/oops/oops.go +++ b/vendor/github.com/samsarahq/go/oops/oops.go @@ -447,8 +447,11 @@ func Wrapf(err error, format string, a ...interface{}) error { return wrapf(err, fmt.Sprintf(format, a...)) } -// Cause extracts the cause error of an oops error. If err is not an oops -// error, err itself is returned. +// Deprecated: Use [errors.Is] or [errors.As] which are part of the standard library. +// +// Note that the behaviour of Cause differs from [errors.Is] and [errors.As]. Cause follows the error chain as long as the error is an oopsError. When a non oopsError is encountered, Cause returns the inner error of the oopsError. [errors.Is] and [errors.As] will follow the error chain until it finds an error that matches the target type. +// +// Cause extracts the cause error of an oops error. If err is not an oops error, err itself is returned. // // You can use Cause to check if an error is an expected error. For example, if // you know than EOF error is fine, you can handle it with Cause. diff --git a/vendor/github.com/samsarahq/go/oops/xerrors.go b/vendor/github.com/samsarahq/go/oops/xerrors.go index 2258b95..313d8c9 100644 --- a/vendor/github.com/samsarahq/go/oops/xerrors.go +++ b/vendor/github.com/samsarahq/go/oops/xerrors.go @@ -40,6 +40,7 @@ type Wrapper interface { Unwrap() error } +// Deprecated: use [errors.Unwrap] which is part of the standard library. // Unwrap returns the result of calling the Unwrap method on err, if err implements // Unwrap. Otherwise, Unwrap returns nil. func Unwrap(err error) error { @@ -50,6 +51,7 @@ func Unwrap(err error) error { return u.Unwrap() } +// Deprecated: use [errors.Is] which is part of the standard library. // Is reports whether any error in err's chain matches target. // // An error is considered to match a target if it is equal to that target or if @@ -71,6 +73,7 @@ func Is(err, target error) bool { } } +// Deprecated: use [errors.As] which is part of the standard library. // As finds the first error in err's chain that matches the type to which target // points, and if so, sets the target to its value and returns true. An error // matches a type if it is assignable to the target type, or if it has a method diff --git a/vendor/github.com/samsarahq/go/snapshotter/snapshotter.go b/vendor/github.com/samsarahq/go/snapshotter/snapshotter.go index 46bcc58..ef28f1d 100644 --- a/vendor/github.com/samsarahq/go/snapshotter/snapshotter.go +++ b/vendor/github.com/samsarahq/go/snapshotter/snapshotter.go @@ -2,6 +2,7 @@ package snapshotter import ( "encoding/json" + "errors" "flag" "io/ioutil" "os" @@ -14,6 +15,7 @@ import ( type T interface { Name() string + Error(args ...interface{}) Errorf(format string, args ...interface{}) Helper() } @@ -31,6 +33,39 @@ func isRewriteWithFailOnDiff() bool { return rewriteEnvVar || *rewriteWithFailOnDiff } +type SnapshotMode int + +const ( + SnapshotModeUndefined SnapshotMode = iota + // SnapshotModeCheck means a snapshot diff will fail the test, and the + // snapshot will not be updated. + SnapshotModeCheck + // SnapshotModeRewrite means a snapshot diff will be ignored, and the snapshot + // will be rewritten. + SnapshotModeRewrite + // SnapshotModeCheckAndRewrite means a snapshot diff will fail the test, and + // the snapshot will be updated. + SnapshotModeCheckAndRewrite +) + +// GlobalSnapshotMode returns the snapshot mode configured in global state +// via environment variables and/or command-line arguments. +func GlobalSnapshotMode() (SnapshotMode, error) { + if isRewrite() && isRewriteWithFailOnDiff() { + return SnapshotModeUndefined, errors.New("choose one of rewriteWithFailOnDiff and rewriteSnapshots, otherwise unexpected behavior can occur.") + } + + if isRewrite() { + return SnapshotModeRewrite, nil + } + + if isRewriteWithFailOnDiff() { + return SnapshotModeCheckAndRewrite, nil + } + + return SnapshotModeCheck, nil +} + func jsonRoundTrip(value interface{}) (interface{}, error) { bytes, err := json.Marshal(value) if err != nil { @@ -116,20 +151,28 @@ func (s *Snapshotter) rewrite(name string) { } } +// Returns the name of the snapshot file that will/would be created when running Verify. +// Includes the testdata/ path +func (s *Snapshotter) SnapshotFileName() string { + s.t.Helper() + nameSuffix := "" + if s.name != "" { + nameSuffix = "_" + strings.Replace(strings.Replace(s.name, "/", "-", -1), ":", "-", -1) + } + return filepath.Join("testdata", strings.Replace(strings.Replace(s.t.Name(), "/", "-", -1), ":", "-", -1)+nameSuffix+".snapshots.json") +} + // Verify finishes a snapshot test. It either compares the test output, or it // rewrites the test output. func (s *Snapshotter) Verify() { s.t.Helper() - if isRewrite() && isRewriteWithFailOnDiff() { - s.t.Errorf("choose one of rewriteWithFailOnDiff and rewriteSnapshots, otherwise unexpected behavior can occur.") + mode, err := GlobalSnapshotMode() + if err != nil { + s.t.Error(err) return } - nameSuffix := "" - if s.name != "" { - nameSuffix = "_" + strings.Replace(strings.Replace(s.name, "/", "-", -1), ":", "-", -1) - } - name := filepath.Join("testdata", strings.Replace(strings.Replace(s.t.Name(), "/", "-", -1), ":", "-", -1)+nameSuffix+".snapshots.json") - if isRewrite() { + name := s.SnapshotFileName() + if mode == SnapshotModeRewrite { s.rewrite(name) } else { // When no snapshots file exists and no snapshots have been taken, do nothing. @@ -148,7 +191,7 @@ func (s *Snapshotter) Verify() { return } - if isRewriteWithFailOnDiff() { + if mode == SnapshotModeCheckAndRewrite { s.rewrite(name) } diff --git a/vendor/modules.txt b/vendor/modules.txt index 9cfd637..85b3b73 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -35,8 +35,8 @@ github.com/mattn/go-isatty # github.com/mattn/go-runewidth v0.0.4-0.20181117163326-c88d7e5f2e24 ## explicit github.com/mattn/go-runewidth -# github.com/mattn/go-zglob v0.0.1 -## explicit +# github.com/mattn/go-zglob v0.0.6 +## explicit; go 1.12 github.com/mattn/go-zglob github.com/mattn/go-zglob/fastwalk # github.com/mitchellh/go-wordwrap v1.0.0 @@ -45,7 +45,7 @@ github.com/mitchellh/go-wordwrap # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib -# github.com/samsarahq/go v0.0.0-20230622164605-46203ca3a6d8 +# github.com/samsarahq/go v0.0.0-20240911161602-ad0977dc0ab1 ## explicit; go 1.17 github.com/samsarahq/go/oops github.com/samsarahq/go/snapshotter @@ -73,7 +73,7 @@ golang.org/x/text/transform # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 -# mvdan.cc/sh v2.5.1-0.20180917103936-65d8e2a4b156+incompatible +# mvdan.cc/sh v2.6.4+incompatible ## explicit mvdan.cc/sh/expand mvdan.cc/sh/interp diff --git a/vendor/mvdan.cc/sh/interp/arith.go b/vendor/mvdan.cc/sh/expand/arith.go similarity index 68% rename from vendor/mvdan.cc/sh/interp/arith.go rename to vendor/mvdan.cc/sh/expand/arith.go index 47edec1..84ce554 100644 --- a/vendor/mvdan.cc/sh/interp/arith.go +++ b/vendor/mvdan.cc/sh/expand/arith.go @@ -1,57 +1,66 @@ // Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information -package interp +package expand import ( - "context" "fmt" "strconv" "mvdan.cc/sh/syntax" ) -func (r *Runner) arithm(ctx context.Context, expr syntax.ArithmExpr) int { +func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) { switch x := expr.(type) { case *syntax.Word: - str := r.loneWord(ctx, x) + str, err := Literal(cfg, x) + if err != nil { + return 0, err + } // recursively fetch vars - for str != "" { - val := r.getVar(str) + i := 0 + for str != "" && syntax.ValidName(str) { + val := cfg.envGet(str) if val == "" { break } + if i++; i >= maxNameRefDepth { + break + } str = val } // default to 0 - return atoi(str) + return atoi(str), nil case *syntax.ParenArithm: - return r.arithm(ctx, x.X) + return Arithm(cfg, x.X) case *syntax.UnaryArithm: switch x.Op { case syntax.Inc, syntax.Dec: - name := x.X.(*syntax.Word).Parts[0].(*syntax.Lit).Value - old := atoi(r.getVar(name)) + name := x.X.(*syntax.Word).Lit() + old := atoi(cfg.envGet(name)) val := old if x.Op == syntax.Inc { val++ } else { val-- } - r.setVarString(ctx, name, strconv.Itoa(val)) + cfg.envSet(name, strconv.Itoa(val)) if x.Post { - return old + return old, nil } - return val + return val, nil + } + val, err := Arithm(cfg, x.X) + if err != nil { + return 0, err } - val := r.arithm(ctx, x.X) switch x.Op { case syntax.Not: - return oneIf(val == 0) + return oneIf(val == 0), nil case syntax.Plus: - return val + return val, nil default: // syntax.Minus - return -val + return -val, nil } case *syntax.BinaryArithm: switch x.Op { @@ -59,16 +68,27 @@ func (r *Runner) arithm(ctx context.Context, expr syntax.ArithmExpr) int { syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn, syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn, syntax.ShlAssgn, syntax.ShrAssgn: - return r.assgnArit(ctx, x) + return cfg.assgnArit(x) case syntax.Quest: // Colon can't happen here - cond := r.arithm(ctx, x.X) + cond, err := Arithm(cfg, x.X) + if err != nil { + return 0, err + } b2 := x.Y.(*syntax.BinaryArithm) // must have Op==Colon if cond == 1 { - return r.arithm(ctx, b2.X) + return Arithm(cfg, b2.X) } - return r.arithm(ctx, b2.Y) + return Arithm(cfg, b2.Y) + } + left, err := Arithm(cfg, x.X) + if err != nil { + return 0, err } - return binArit(x.Op, r.arithm(ctx, x.X), r.arithm(ctx, x.Y)) + right, err := Arithm(cfg, x.Y) + if err != nil { + return 0, err + } + return binArit(x.Op, left, right), nil default: panic(fmt.Sprintf("unexpected arithm expr: %T", x)) } @@ -88,10 +108,13 @@ func atoi(s string) int { return n } -func (r *Runner) assgnArit(ctx context.Context, b *syntax.BinaryArithm) int { - name := b.X.(*syntax.Word).Parts[0].(*syntax.Lit).Value - val := atoi(r.getVar(name)) - arg := r.arithm(ctx, b.Y) +func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) { + name := b.X.(*syntax.Word).Lit() + val := atoi(cfg.envGet(name)) + arg, err := Arithm(cfg, b.Y) + if err != nil { + return 0, err + } switch b.Op { case syntax.Assgn: val = arg @@ -116,8 +139,8 @@ func (r *Runner) assgnArit(ctx context.Context, b *syntax.BinaryArithm) int { case syntax.ShrAssgn: val >>= uint(arg) } - r.setVarString(ctx, name, strconv.Itoa(val)) - return val + cfg.envSet(name, strconv.Itoa(val)) + return val, nil } func intPow(a, b int) int { diff --git a/vendor/mvdan.cc/sh/expand/braces.go b/vendor/mvdan.cc/sh/expand/braces.go index 24ece8f..d8010da 100644 --- a/vendor/mvdan.cc/sh/expand/braces.go +++ b/vendor/mvdan.cc/sh/expand/braces.go @@ -5,9 +5,9 @@ package expand import "mvdan.cc/sh/syntax" -// Braces performs Bash brace expansion on a word. For example, passing it a -// single-literal word "foo{bar,baz}" will return two single-literal words, -// "foobar" and "foobaz". +// Braces performs Bash brace expansion on words. For example, passing it a +// literal word "foo{bar,baz}" will return two literal words, "foobar" and +// "foobaz". // // It does not return an error; malformed brace expansions are simply skipped. // For example, "a{b{c,d}" results in the words "a{bc" and "a{bd". @@ -15,6 +15,10 @@ import "mvdan.cc/sh/syntax" // Note that the resulting words may have more word parts than necessary, such // as contiguous *syntax.Lit nodes, and that these parts may be shared between // words. -func Braces(word *syntax.Word) []*syntax.Word { - return syntax.ExpandBraces(word) +func Braces(words ...*syntax.Word) []*syntax.Word { + var res []*syntax.Word + for _, word := range words { + res = append(res, syntax.ExpandBraces(word)...) + } + return res } diff --git a/vendor/mvdan.cc/sh/expand/doc.go b/vendor/mvdan.cc/sh/expand/doc.go index db80b49..19d9518 100644 --- a/vendor/mvdan.cc/sh/expand/doc.go +++ b/vendor/mvdan.cc/sh/expand/doc.go @@ -2,7 +2,4 @@ // See LICENSE for licensing information // Package expand contains code to perform various shell expansions. -// -// This package is a work in progress and EXPERIMENTAL; its API is not -// subject to the 2.x backwards compatibility guarantee. package expand diff --git a/vendor/mvdan.cc/sh/expand/environ.go b/vendor/mvdan.cc/sh/expand/environ.go new file mode 100644 index 0000000..ebd90b7 --- /dev/null +++ b/vendor/mvdan.cc/sh/expand/environ.go @@ -0,0 +1,195 @@ +// Copyright (c) 2018, Daniel Martí +// See LICENSE for licensing information + +package expand + +import ( + "runtime" + "sort" + "strings" +) + +// Environ is the base interface for a shell's environment, allowing it to fetch +// variables by name and to iterate over all the currently set variables. +type Environ interface { + // Get retrieves a variable by its name. To check if the variable is + // set, use Variable.IsSet. + Get(name string) Variable + + // Each iterates over all the currently set variables, calling the + // supplied function on each variable. Iteration is stopped if the + // function returns false. + // + // The names used in the calls aren't required to be unique or sorted. + // If a variable name appears twice, the latest occurrence takes + // priority. + // + // Each is required to forward exported variables when executing + // programs. + Each(func(name string, vr Variable) bool) +} + +// WriteEnviron is an extension on Environ that supports modifying and deleting +// variables. +type WriteEnviron interface { + Environ + // Set sets a variable by name. If !vr.IsSet(), the variable is being + // unset; otherwise, the variable is being replaced. + // + // It is the implementation's responsibility to handle variable + // attributes correctly. For example, changing an exported variable's + // value does not unexport it, and overwriting a name reference variable + // should modify its target. + Set(name string, vr Variable) +} + +// Variable describes a shell variable, which can have a number of attributes +// and a value. +// +// A Variable is unset if its Value field is untyped nil, which can be checked +// via Variable.IsSet. The zero value of a Variable is thus a valid unset +// variable. +// +// If a variable is set, its Value field will be a []string if it is an indexed +// array, a map[string]string if it's an associative array, or a string +// otherwise. +type Variable struct { + Local bool + Exported bool + ReadOnly bool + NameRef bool // if true, Value must be string + Value interface{} // string, []string, or map[string]string +} + +// IsSet returns whether the variable is set. An empty variable is set, but an +// undeclared variable is not. +func (v Variable) IsSet() bool { + return v.Value != nil +} + +// String returns the variable's value as a string. In general, this only makes +// sense if the variable has a string value or no value at all. +func (v Variable) String() string { + switch x := v.Value.(type) { + case string: + return x + case []string: + if len(x) > 0 { + return x[0] + } + case map[string]string: + // nothing to do + } + return "" +} + +// maxNameRefDepth defines the maximum number of times to follow references when +// resolving a variable. Otherwise, simple name reference loops could crash a +// program quite easily. +const maxNameRefDepth = 100 + +// Resolve follows a number of nameref variables, returning the last reference +// name that was followed and the variable that it points to. +func (v Variable) Resolve(env Environ) (string, Variable) { + name := "" + for i := 0; i < maxNameRefDepth; i++ { + if !v.NameRef { + return name, v + } + name = v.Value.(string) + v = env.Get(name) + } + return name, Variable{} +} + +// FuncEnviron wraps a function mapping variable names to their string values, +// and implements Environ. Empty strings returned by the function will be +// treated as unset variables. All variables will be exported. +// +// Note that the returned Environ's Each method will be a no-op. +func FuncEnviron(fn func(string) string) Environ { + return funcEnviron(fn) +} + +type funcEnviron func(string) string + +func (f funcEnviron) Get(name string) Variable { + value := f(name) + if value == "" { + return Variable{} + } + return Variable{Exported: true, Value: value} +} + +func (f funcEnviron) Each(func(name string, vr Variable) bool) {} + +// ListEnviron returns an Environ with the supplied variables, in the form +// "key=value". All variables will be exported. +// +// On Windows, where environment variable names are case-insensitive, the +// resulting variable names will all be uppercase. +func ListEnviron(pairs ...string) Environ { + return listEnvironWithUpper(runtime.GOOS == "windows", pairs...) +} + +// listEnvironWithUpper implements ListEnviron, but letting the tests specify +// whether to uppercase all names or not. +func listEnvironWithUpper(upper bool, pairs ...string) Environ { + list := append([]string{}, pairs...) + if upper { + // Uppercase before sorting, so that we can remove duplicates + // without the need for linear search nor a map. + for i, s := range list { + if sep := strings.IndexByte(s, '='); sep > 0 { + list[i] = strings.ToUpper(s[:sep]) + s[sep:] + } + } + } + sort.Strings(list) + last := "" + for i := 0; i < len(list); { + s := list[i] + sep := strings.IndexByte(s, '=') + if sep <= 0 { + // invalid element; remove it + list = append(list[:i], list[i+1:]...) + continue + } + name := s[:sep] + if last == name { + // duplicate; the last one wins + list = append(list[:i-1], list[i:]...) + continue + } + last = name + i++ + } + return listEnviron(list) +} + +type listEnviron []string + +func (l listEnviron) Get(name string) Variable { + // TODO: binary search + prefix := name + "=" + for _, pair := range l { + if val := strings.TrimPrefix(pair, prefix); val != pair { + return Variable{Exported: true, Value: val} + } + } + return Variable{} +} + +func (l listEnviron) Each(fn func(name string, vr Variable) bool) { + for _, pair := range l { + i := strings.IndexByte(pair, '=') + if i < 0 { + // can't happen; see above + panic("expand.listEnviron: did not expect malformed name-value pair: " + pair) + } + name, value := pair[:i], pair[i+1:] + if !fn(name, Variable{Exported: true, Value: value}) { + return + } + } +} diff --git a/vendor/mvdan.cc/sh/expand/expand.go b/vendor/mvdan.cc/sh/expand/expand.go new file mode 100644 index 0000000..16f63d4 --- /dev/null +++ b/vendor/mvdan.cc/sh/expand/expand.go @@ -0,0 +1,799 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package expand + +import ( + "bytes" + "fmt" + "io" + "os" + "os/user" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "mvdan.cc/sh/syntax" +) + +// A Config specifies details about how shell expansion should be performed. The +// zero value is a valid configuration. +type Config struct { + // Env is used to get and set environment variables when performing + // shell expansions. Some special parameters are also expanded via this + // interface, such as: + // + // * "#", "@", "*", "0"-"9" for the shell's parameters + // * "?", "$", "PPID" for the shell's status and process + // * "HOME foo" to retrieve user foo's home directory (if unset, + // os/user.Lookup will be used) + // + // If nil, there are no environment variables set. Use + // ListEnviron(os.Environ()...) to use the system's environment + // variables. + Env Environ + + // TODO(mvdan): consider replacing NoGlob==true with ReadDir==nil. + + // NoGlob corresponds to the shell option that disables globbing. + NoGlob bool + // GlobStar corresponds to the shell option that allows globbing with + // "**". + GlobStar bool + + // CmdSubst expands a command substitution node, writing its standard + // output to the provided io.Writer. + // + // If nil, encountering a command substitution will result in an + // UnexpectedCommandError. + CmdSubst func(io.Writer, *syntax.CmdSubst) error + + // ReadDir is used for file path globbing. If nil, globbing is disabled. + // Use ioutil.ReadDir to use the filesystem directly. + ReadDir func(string) ([]os.FileInfo, error) + + bufferAlloc bytes.Buffer + fieldAlloc [4]fieldPart + fieldsAlloc [4][]fieldPart + + ifs string + // A pointer to a parameter expansion node, if we're inside one. + // Necessary for ${LINENO}. + curParam *syntax.ParamExp +} + +// UnexpectedCommandError is returned if a command substitution is encountered +// when Config.CmdSubst is nil. +type UnexpectedCommandError struct { + Node *syntax.CmdSubst +} + +func (u UnexpectedCommandError) Error() string { + return fmt.Sprintf("unexpected command substitution at %s", u.Node.Pos()) +} + +var zeroConfig = &Config{} + +func prepareConfig(cfg *Config) *Config { + if cfg == nil { + cfg = zeroConfig + } + if cfg.Env == nil { + cfg.Env = FuncEnviron(func(string) string { return "" }) + } + + cfg.ifs = " \t\n" + if vr := cfg.Env.Get("IFS"); vr.IsSet() { + cfg.ifs = vr.String() + } + return cfg +} + +func (cfg *Config) ifsRune(r rune) bool { + for _, r2 := range cfg.ifs { + if r == r2 { + return true + } + } + return false +} + +func (cfg *Config) ifsJoin(strs []string) string { + sep := "" + if cfg.ifs != "" { + sep = cfg.ifs[:1] + } + return strings.Join(strs, sep) +} + +func (cfg *Config) strBuilder() *bytes.Buffer { + b := &cfg.bufferAlloc + b.Reset() + return b +} + +func (cfg *Config) envGet(name string) string { + return cfg.Env.Get(name).String() +} + +func (cfg *Config) envSet(name, value string) { + wenv, ok := cfg.Env.(WriteEnviron) + if !ok { + // TODO: we should probably error here + return + } + wenv.Set(name, Variable{Value: value}) +} + +// Literal expands a single shell word. It is similar to Fields, but the result +// is a single string. This is the behavior when a word is used as the value in +// a shell variable assignment, for example. +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func Literal(cfg *Config, word *syntax.Word) (string, error) { + if word == nil { + return "", nil + } + cfg = prepareConfig(cfg) + field, err := cfg.wordField(word.Parts, quoteNone) + if err != nil { + return "", err + } + return cfg.fieldJoin(field), nil +} + +// Document expands a single shell word as if it were within double quotes. It +// is simlar to Literal, but without brace expansion, tilde expansion, and +// globbing. +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func Document(cfg *Config, word *syntax.Word) (string, error) { + if word == nil { + return "", nil + } + cfg = prepareConfig(cfg) + field, err := cfg.wordField(word.Parts, quoteDouble) + if err != nil { + return "", err + } + return cfg.fieldJoin(field), nil +} + +// Pattern expands a single shell word as a pattern, using syntax.QuotePattern +// on any non-quoted parts of the input word. The result can be used on +// syntax.TranslatePattern directly. +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func Pattern(cfg *Config, word *syntax.Word) (string, error) { + cfg = prepareConfig(cfg) + field, err := cfg.wordField(word.Parts, quoteNone) + if err != nil { + return "", err + } + buf := cfg.strBuilder() + for _, part := range field { + if part.quote > quoteNone { + buf.WriteString(syntax.QuotePattern(part.val)) + } else { + buf.WriteString(part.val) + } + } + return buf.String(), nil +} + +// Format expands a format string with a number of arguments, following the +// shell's format specifications. These include printf(1), among others. +// +// The resulting string is returned, along with the number of arguments used. +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func Format(cfg *Config, format string, args []string) (string, int, error) { + cfg = prepareConfig(cfg) + buf := cfg.strBuilder() + esc := false + var fmts []rune + initialArgs := len(args) + + for _, c := range format { + switch { + case esc: + esc = false + switch c { + case 'n': + buf.WriteRune('\n') + case 'r': + buf.WriteRune('\r') + case 't': + buf.WriteRune('\t') + case '\\': + buf.WriteRune('\\') + default: + buf.WriteRune('\\') + buf.WriteRune(c) + } + + case len(fmts) > 0: + switch c { + case '%': + buf.WriteByte('%') + fmts = nil + case 'c': + var b byte + if len(args) > 0 { + arg := "" + arg, args = args[0], args[1:] + if len(arg) > 0 { + b = arg[0] + } + } + buf.WriteByte(b) + fmts = nil + case '+', '-', ' ': + if len(fmts) > 1 { + return "", 0, fmt.Errorf("invalid format char: %c", c) + } + fmts = append(fmts, c) + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + fmts = append(fmts, c) + case 's', 'd', 'i', 'u', 'o', 'x': + arg := "" + if len(args) > 0 { + arg, args = args[0], args[1:] + } + var farg interface{} = arg + if c != 's' { + n, _ := strconv.ParseInt(arg, 0, 0) + if c == 'i' || c == 'd' { + farg = int(n) + } else { + farg = uint(n) + } + if c == 'i' || c == 'u' { + c = 'd' + } + } + fmts = append(fmts, c) + fmt.Fprintf(buf, string(fmts), farg) + fmts = nil + default: + return "", 0, fmt.Errorf("invalid format char: %c", c) + } + case c == '\\': + esc = true + case args != nil && c == '%': + // if args == nil, we are not doing format + // arguments + fmts = []rune{c} + default: + buf.WriteRune(c) + } + } + if len(fmts) > 0 { + return "", 0, fmt.Errorf("missing format char") + } + return buf.String(), initialArgs - len(args), nil +} + +func (cfg *Config) fieldJoin(parts []fieldPart) string { + switch len(parts) { + case 0: + return "" + case 1: // short-cut without a string copy + return parts[0].val + } + buf := cfg.strBuilder() + for _, part := range parts { + buf.WriteString(part.val) + } + return buf.String() +} + +func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) { + buf := cfg.strBuilder() + for _, part := range parts { + if part.quote > quoteNone { + buf.WriteString(syntax.QuotePattern(part.val)) + continue + } + buf.WriteString(part.val) + if syntax.HasPattern(part.val) { + glob = true + } + } + if glob { // only copy the string if it will be used + escaped = buf.String() + } + return escaped, glob +} + +// Fields expands a number of words as if they were arguments in a shell +// command. This includes brace expansion, tilde expansion, parameter expansion, +// command substitution, arithmetic expansion, and quote removal. +func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) { + cfg = prepareConfig(cfg) + fields := make([]string, 0, len(words)) + dir := cfg.envGet("PWD") + for _, expWord := range Braces(words...) { + wfields, err := cfg.wordFields(expWord.Parts) + if err != nil { + return nil, err + } + for _, field := range wfields { + path, doGlob := cfg.escapedGlobField(field) + var matches []string + abs := filepath.IsAbs(path) + if doGlob && !cfg.NoGlob { + base := "" + if !abs { + base = dir + } + matches, err = cfg.glob(base, path) + if err != nil { + return nil, err + } + } + if len(matches) == 0 { + fields = append(fields, cfg.fieldJoin(field)) + continue + } + for _, match := range matches { + if !abs { + match = strings.TrimPrefix(match, dir) + } + fields = append(fields, match) + } + } + } + return fields, nil +} + +type fieldPart struct { + val string + quote quoteLevel +} + +type quoteLevel uint + +const ( + quoteNone quoteLevel = iota + quoteDouble + quoteSingle +) + +func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) { + var field []fieldPart + for i, wp := range wps { + switch x := wp.(type) { + case *syntax.Lit: + s := x.Value + if i == 0 && ql == quoteNone { + if prefix, rest := cfg.expandUser(s); prefix != "" { + // TODO: return two separate fieldParts, + // like in wordFields? + s = prefix + rest + } + } + if ql == quoteDouble && strings.Contains(s, "\\") { + buf := cfg.strBuilder() + for i := 0; i < len(s); i++ { + b := s[i] + if b == '\\' && i+1 < len(s) { + switch s[i+1] { + case '\n': // remove \\\n + i++ + continue + case '"', '\\', '$', '`': // special chars + continue + } + } + buf.WriteByte(b) + } + s = buf.String() + } + field = append(field, fieldPart{val: s}) + case *syntax.SglQuoted: + fp := fieldPart{quote: quoteSingle, val: x.Value} + if x.Dollar { + fp.val, _, _ = Format(cfg, fp.val, nil) + } + field = append(field, fp) + case *syntax.DblQuoted: + wfield, err := cfg.wordField(x.Parts, quoteDouble) + if err != nil { + return nil, err + } + for _, part := range wfield { + part.quote = quoteDouble + field = append(field, part) + } + case *syntax.ParamExp: + val, err := cfg.paramExp(x) + if err != nil { + return nil, err + } + field = append(field, fieldPart{val: val}) + case *syntax.CmdSubst: + val, err := cfg.cmdSubst(x) + if err != nil { + return nil, err + } + field = append(field, fieldPart{val: val}) + case *syntax.ArithmExp: + n, err := Arithm(cfg, x.X) + if err != nil { + return nil, err + } + field = append(field, fieldPart{val: strconv.Itoa(n)}) + default: + panic(fmt.Sprintf("unhandled word part: %T", x)) + } + } + return field, nil +} + +func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) { + if cfg.CmdSubst == nil { + return "", UnexpectedCommandError{Node: cs} + } + buf := cfg.strBuilder() + if err := cfg.CmdSubst(buf, cs); err != nil { + return "", err + } + return strings.TrimRight(buf.String(), "\n"), nil +} + +func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) { + fields := cfg.fieldsAlloc[:0] + curField := cfg.fieldAlloc[:0] + allowEmpty := false + flush := func() { + if len(curField) == 0 { + return + } + fields = append(fields, curField) + curField = nil + } + splitAdd := func(val string) { + for i, field := range strings.FieldsFunc(val, cfg.ifsRune) { + if i > 0 { + flush() + } + curField = append(curField, fieldPart{val: field}) + } + } + for i, wp := range wps { + switch x := wp.(type) { + case *syntax.Lit: + s := x.Value + if i == 0 { + prefix, rest := cfg.expandUser(s) + curField = append(curField, fieldPart{ + quote: quoteSingle, + val: prefix, + }) + s = rest + } + if strings.Contains(s, "\\") { + buf := cfg.strBuilder() + for i := 0; i < len(s); i++ { + b := s[i] + if b == '\\' { + i++ + b = s[i] + } + buf.WriteByte(b) + } + s = buf.String() + } + curField = append(curField, fieldPart{val: s}) + case *syntax.SglQuoted: + allowEmpty = true + fp := fieldPart{quote: quoteSingle, val: x.Value} + if x.Dollar { + fp.val, _, _ = Format(cfg, fp.val, nil) + } + curField = append(curField, fp) + case *syntax.DblQuoted: + allowEmpty = true + if len(x.Parts) == 1 { + pe, _ := x.Parts[0].(*syntax.ParamExp) + if elems := cfg.quotedElems(pe); elems != nil { + for i, elem := range elems { + if i > 0 { + flush() + } + curField = append(curField, fieldPart{ + quote: quoteDouble, + val: elem, + }) + } + continue + } + } + wfield, err := cfg.wordField(x.Parts, quoteDouble) + if err != nil { + return nil, err + } + for _, part := range wfield { + part.quote = quoteDouble + curField = append(curField, part) + } + case *syntax.ParamExp: + val, err := cfg.paramExp(x) + if err != nil { + return nil, err + } + splitAdd(val) + case *syntax.CmdSubst: + val, err := cfg.cmdSubst(x) + if err != nil { + return nil, err + } + splitAdd(val) + case *syntax.ArithmExp: + n, err := Arithm(cfg, x.X) + if err != nil { + return nil, err + } + curField = append(curField, fieldPart{val: strconv.Itoa(n)}) + default: + panic(fmt.Sprintf("unhandled word part: %T", x)) + } + } + flush() + if allowEmpty && len(fields) == 0 { + fields = append(fields, curField) + } + return fields, nil +} + +// quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]} +func (cfg *Config) quotedElems(pe *syntax.ParamExp) []string { + if pe == nil || pe.Excl || pe.Length || pe.Width { + return nil + } + if pe.Param.Value == "@" { + return cfg.Env.Get("@").Value.([]string) + } + if nodeLit(pe.Index) != "@" { + return nil + } + val := cfg.Env.Get(pe.Param.Value).Value + if x, ok := val.([]string); ok { + return x + } + return nil +} + +func (cfg *Config) expandUser(field string) (prefix, rest string) { + if len(field) == 0 || field[0] != '~' { + return "", field + } + name := field[1:] + if i := strings.Index(name, "/"); i >= 0 { + rest = name[i:] + name = name[:i] + } + if name == "" { + return cfg.Env.Get("HOME").String(), rest + } + if vr := cfg.Env.Get("HOME " + name); vr.IsSet() { + return vr.String(), rest + } + + u, err := user.Lookup(name) + if err != nil { + return "", field + } + return u.HomeDir, rest +} + +func findAllIndex(pattern, name string, n int) [][]int { + expr, err := syntax.TranslatePattern(pattern, true) + if err != nil { + return nil + } + rx := regexp.MustCompile(expr) + return rx.FindAllStringIndex(name, n) +} + +// TODO: use this again to optimize globbing; see +// https://github.com/mvdan/sh/issues/213 +func hasGlob(path string) bool { + magicChars := `*?[` + if runtime.GOOS != "windows" { + magicChars = `*?[\` + } + return strings.ContainsAny(path, magicChars) +} + +var rxGlobStar = regexp.MustCompile(".*") + +// pathJoin2 is a simpler version of filepath.Join without cleaning the result, +// since that's needed for globbing. +func pathJoin2(elem1, elem2 string) string { + if elem1 == "" { + return elem2 + } + if strings.HasSuffix(elem1, string(filepath.Separator)) { + return elem1 + elem2 + } + return elem1 + string(filepath.Separator) + elem2 +} + +// pathSplit splits a file path into its elements, retaining empty ones. Before +// splitting, slashes are replaced with filepath.Separator, so that splitting +// Unix paths on Windows works as well. +func pathSplit(path string) []string { + path = filepath.FromSlash(path) + return strings.Split(path, string(filepath.Separator)) +} + +func (cfg *Config) glob(base, pattern string) ([]string, error) { + parts := pathSplit(pattern) + matches := []string{""} + if filepath.IsAbs(pattern) { + if parts[0] == "" { + // unix-like + matches[0] = string(filepath.Separator) + } else { + // windows (for some reason it won't work without the + // trailing separator) + matches[0] = parts[0] + string(filepath.Separator) + } + parts = parts[1:] + } + for _, part := range parts { + switch { + case part == "", part == ".", part == "..": + var newMatches []string + for _, dir := range matches { + // TODO(mvdan): reuse the previous ReadDir call + if cfg.ReadDir == nil { + continue // no globbing + } else if _, err := cfg.ReadDir(filepath.Join(base, dir)); err != nil { + continue // not actually a dir + } + newMatches = append(newMatches, pathJoin2(dir, part)) + } + matches = newMatches + continue + case part == "**" && cfg.GlobStar: + for i, match := range matches { + // "a/**" should match "a/ a/b a/b/cfg ..."; note + // how the zero-match case has a trailing + // separator. + matches[i] = pathJoin2(match, "") + } + // expand all the possible levels of ** + latest := matches + for { + var newMatches []string + for _, dir := range latest { + var err error + newMatches, err = cfg.globDir(base, dir, rxGlobStar, newMatches) + if err != nil { + return nil, err + } + } + if len(newMatches) == 0 { + // not another level of directories to + // try; stop + break + } + matches = append(matches, newMatches...) + latest = newMatches + } + continue + } + expr, err := syntax.TranslatePattern(part, true) + if err != nil { + // If any glob part is not a valid pattern, don't glob. + return nil, nil + } + rx := regexp.MustCompile("^" + expr + "$") + var newMatches []string + for _, dir := range matches { + newMatches, err = cfg.globDir(base, dir, rx, newMatches) + if err != nil { + return nil, err + } + } + matches = newMatches + } + return matches, nil +} + +func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matches []string) ([]string, error) { + if cfg.ReadDir == nil { + // TODO(mvdan): check this at the beginning of a glob? + return nil, nil + } + infos, err := cfg.ReadDir(filepath.Join(base, dir)) + if err != nil { + // Ignore the error, as this might be a file instead of a + // directory. v3 refactored globbing to only use one ReadDir + // call per directory instead of two, so it knows to skip this + // kind of path at the ReadDir call of its parent. + // Instead of backporting that complex rewrite into v2, just + // work around the edge case here. We might ignore other kinds + // of errors, but at least we don't fail on a correct glob. + return matches, nil + } + for _, info := range infos { + name := info.Name() + if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' { + continue + } + if rx.MatchString(name) { + matches = append(matches, pathJoin2(dir, name)) + } + } + return matches, nil +} + +// +// The config specifies shell expansion options; nil behaves the same as an +// empty config. +func ReadFields(cfg *Config, s string, n int, raw bool) []string { + cfg = prepareConfig(cfg) + type pos struct { + start, end int + } + var fpos []pos + + runes := make([]rune, 0, len(s)) + infield := false + esc := false + for _, r := range s { + if infield { + if cfg.ifsRune(r) && (raw || !esc) { + fpos[len(fpos)-1].end = len(runes) + infield = false + } + } else { + if !cfg.ifsRune(r) && (raw || !esc) { + fpos = append(fpos, pos{start: len(runes), end: -1}) + infield = true + } + } + if r == '\\' { + if raw || esc { + runes = append(runes, r) + } + esc = !esc + continue + } + runes = append(runes, r) + esc = false + } + if len(fpos) == 0 { + return nil + } + if infield { + fpos[len(fpos)-1].end = len(runes) + } + + switch { + case n == 1: + // include heading/trailing IFSs + fpos[0].start, fpos[0].end = 0, len(runes) + fpos = fpos[:1] + case n != -1 && n < len(fpos): + // combine to max n fields + fpos[n-1].end = fpos[len(fpos)-1].end + fpos = fpos[:n] + } + + var fields = make([]string, len(fpos)) + for i, p := range fpos { + fields[i] = string(runes[p.start:p.end]) + } + return fields +} diff --git a/vendor/mvdan.cc/sh/interp/param.go b/vendor/mvdan.cc/sh/expand/param.go similarity index 55% rename from vendor/mvdan.cc/sh/interp/param.go rename to vendor/mvdan.cc/sh/expand/param.go index 7a918bf..69e33ae 100644 --- a/vendor/mvdan.cc/sh/interp/param.go +++ b/vendor/mvdan.cc/sh/expand/param.go @@ -1,12 +1,10 @@ // Copyright (c) 2017, Daniel Martí // See LICENSE for licensing information -package interp +package expand import ( - "context" "fmt" - "os" "regexp" "sort" "strconv" @@ -17,151 +15,133 @@ import ( "mvdan.cc/sh/syntax" ) -func anyOfLit(v interface{}, vals ...string) string { - word, _ := v.(*syntax.Word) - if word == nil || len(word.Parts) != 1 { - return "" - } - lit, ok := word.Parts[0].(*syntax.Lit) - if !ok { - return "" - } - for _, val := range vals { - if lit.Value == val { - return val - } +func nodeLit(node syntax.Node) string { + if word, ok := node.(*syntax.Word); ok { + return word.Lit() } return "" } -// quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]} -func (r *Runner) quotedElems(pe *syntax.ParamExp) []string { - if pe == nil || pe.Excl || pe.Length || pe.Width { - return nil - } - if pe.Param.Value == "@" { - return r.Params - } - if anyOfLit(pe.Index, "@") == "" { - return nil - } - val, _ := r.lookupVar(pe.Param.Value) - if x, ok := val.Value.(IndexArray); ok { - return x - } - return nil +type UnsetParameterError struct { + Node *syntax.ParamExp + Message string +} + +func (u UnsetParameterError) Error() string { + return u.Message } -func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { +func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) { + oldParam := cfg.curParam + cfg.curParam = pe + defer func() { cfg.curParam = oldParam }() + name := pe.Param.Value - var vr Variable - set := false index := pe.Index switch name { - case "#": - vr.Value = StringVal(strconv.Itoa(len(r.Params))) case "@", "*": - vr.Value = IndexArray(r.Params) index = &syntax.Word{Parts: []syntax.WordPart{ &syntax.Lit{Value: name}, }} - case "?": - vr.Value = StringVal(strconv.Itoa(r.exit)) - case "$": - vr.Value = StringVal(strconv.Itoa(os.Getpid())) - case "PPID": - vr.Value = StringVal(strconv.Itoa(os.Getppid())) + } + var vr Variable + switch name { case "LINENO": - line := uint64(pe.Pos().Line()) - vr.Value = StringVal(strconv.FormatUint(line, 10)) - case "DIRSTACK": - vr.Value = IndexArray(r.dirStack) - case "0": - if r.filename != "" { - vr.Value = StringVal(r.filename) - } else { - vr.Value = StringVal("gosh") - } - case "1", "2", "3", "4", "5", "6", "7", "8", "9": - i := int(name[0] - '1') - if i < len(r.Params) { - vr.Value, set = StringVal(r.Params[i]), true - } + // This is the only parameter expansion that the environment + // interface cannot satisfy. + line := uint64(cfg.curParam.Pos().Line()) + vr.Value = strconv.FormatUint(line, 10) default: - vr, set = r.lookupVar(name) + vr = cfg.Env.Get(name) } - str := r.varStr(vr, 0) - if index != nil { - str = r.varInd(ctx, vr, index, 0) + orig := vr + _, vr = vr.Resolve(cfg.Env) + str, err := cfg.varInd(vr, index) + if err != nil { + return "", err } - slicePos := func(expr syntax.ArithmExpr) int { - p := r.arithm(ctx, expr) - if p < 0 { - p = len(str) + p - if p < 0 { - p = len(str) + slicePos := func(n int) int { + if n < 0 { + n = len(str) + n + if n < 0 { + n = len(str) } - } else if p > len(str) { - p = len(str) + } else if n > len(str) { + n = len(str) } - return p + return n } elems := []string{str} - if anyOfLit(index, "@", "*") != "" { + switch nodeLit(index) { + case "@", "*": switch x := vr.Value.(type) { case nil: elems = nil - case IndexArray: + case []string: elems = x } } switch { case pe.Length: n := len(elems) - if anyOfLit(index, "@", "*") == "" { + switch nodeLit(index) { + case "@", "*": + default: n = utf8.RuneCountInString(str) } str = strconv.Itoa(n) case pe.Excl: var strs []string if pe.Names != 0 { - strs = r.namesByPrefix(pe.Param.Value) - } else if vr.NameRef { - strs = append(strs, string(vr.Value.(StringVal))) - } else if x, ok := vr.Value.(IndexArray); ok { + strs = cfg.namesByPrefix(pe.Param.Value) + } else if orig.NameRef { + strs = append(strs, orig.Value.(string)) + } else if x, ok := vr.Value.([]string); ok { for i, e := range x { if e != "" { strs = append(strs, strconv.Itoa(i)) } } - } else if x, ok := vr.Value.(AssocArray); ok { + } else if x, ok := vr.Value.(map[string]string); ok { for k := range x { strs = append(strs, k) } } else if str != "" { - vr, _ = r.lookupVar(str) - strs = append(strs, r.varStr(vr, 0)) + vr = cfg.Env.Get(str) + strs = append(strs, vr.String()) } sort.Strings(strs) str = strings.Join(strs, " ") case pe.Slice != nil: if pe.Slice.Offset != nil { - offset := slicePos(pe.Slice.Offset) - str = str[offset:] + n, err := Arithm(cfg, pe.Slice.Offset) + if err != nil { + return "", err + } + str = str[slicePos(n):] } if pe.Slice.Length != nil { - length := slicePos(pe.Slice.Length) - str = str[:length] + n, err := Arithm(cfg, pe.Slice.Length) + if err != nil { + return "", err + } + str = str[:slicePos(n)] } case pe.Repl != nil: - orig := r.lonePattern(ctx, pe.Repl.Orig) - with := r.loneWord(ctx, pe.Repl.With) + orig, err := Pattern(cfg, pe.Repl.Orig) + if err != nil { + return "", err + } + with, err := Literal(cfg, pe.Repl.With) + if err != nil { + return "", err + } n := 1 if pe.Repl.All { n = -1 } locs := findAllIndex(orig, str, n) - buf := r.strBuilder() + buf := cfg.strBuilder() last := 0 for _, loc := range locs { buf.WriteString(str[last:loc[0]]) @@ -171,7 +151,10 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { buf.WriteString(str[last:]) str = buf.String() case pe.Exp != nil: - arg := r.loneWord(ctx, pe.Exp.Word) + arg, err := Literal(cfg, pe.Exp.Word) + if err != nil { + return "", err + } switch op := pe.Exp.Op; op { case syntax.SubstColPlus: if str == "" { @@ -179,11 +162,11 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { } fallthrough case syntax.SubstPlus: - if set { + if vr.IsSet() { str = arg } case syntax.SubstMinus: - if set { + if vr.IsSet() { break } fallthrough @@ -192,24 +175,25 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { str = arg } case syntax.SubstQuest: - if set { + if vr.IsSet() { break } fallthrough case syntax.SubstColQuest: if str == "" { - r.errf("%s\n", arg) - r.exit = 1 - r.setErr(ShellExitStatus(r.exit)) + return "", UnsetParameterError{ + Node: pe, + Message: arg, + } } case syntax.SubstAssgn: - if set { + if vr.IsSet() { break } fallthrough case syntax.SubstColAssgn: if str == "" { - r.setVarString(ctx, name, arg) + cfg.envSet(name, arg) str = arg } case syntax.RemSmallPrefix, syntax.RemLargePrefix, @@ -234,7 +218,7 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { // empty string means '?'; nothing to do there expr, err := syntax.TranslatePattern(arg, false) if err != nil { - return str + return str, nil } rx := regexp.MustCompile(expr) @@ -271,7 +255,7 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string { } } } - return str + return str, nil } func removePattern(str, pattern string, fromEnd, greedy bool) string { @@ -298,3 +282,67 @@ func removePattern(str, pattern string, fromEnd, greedy bool) string { } return str } + +func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) { + if idx == nil { + return vr.String(), nil + } + switch x := vr.Value.(type) { + case string: + n, err := Arithm(cfg, idx) + if err != nil { + return "", err + } + if n == 0 { + return x, nil + } + case []string: + switch nodeLit(idx) { + case "@": + return strings.Join(x, " "), nil + case "*": + return cfg.ifsJoin(x), nil + } + i, err := Arithm(cfg, idx) + if err != nil { + return "", err + } + if len(x) > 0 { + return x[i], nil + } + case map[string]string: + switch lit := nodeLit(idx); lit { + case "@", "*": + var strs []string + keys := make([]string, 0, len(x)) + for k := range x { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + strs = append(strs, x[k]) + } + if lit == "*" { + return cfg.ifsJoin(strs), nil + } + return strings.Join(strs, " "), nil + } + val, err := Literal(cfg, idx.(*syntax.Word)) + if err != nil { + return "", err + } + return x[val], nil + } + return "", nil +} + +func (cfg *Config) namesByPrefix(prefix string) []string { + var names []string + cfg.Env.Each(func(name string, vr Variable) bool { + if strings.HasPrefix(name, prefix) { + names = append(names, name) + } + return true + }) + return names +} diff --git a/vendor/mvdan.cc/sh/interp/builtin.go b/vendor/mvdan.cc/sh/interp/builtin.go index 64949a5..f36bfd0 100644 --- a/vendor/mvdan.cc/sh/interp/builtin.go +++ b/vendor/mvdan.cc/sh/interp/builtin.go @@ -13,6 +13,7 @@ import ( "strconv" "strings" + "mvdan.cc/sh/expand" "mvdan.cc/sh/syntax" ) @@ -29,6 +30,20 @@ func isBuiltin(name string) bool { return false } +func oneIf(b bool) int { + if b { + return 1 + } + return 0 +} + +// atoi is just a shorthand for strconv.Atoi that ignores the error, +// just like shells do. +func atoi(s string) int { + n, _ := strconv.Atoi(s) + return n +} + func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int { switch name { case "true", ":": @@ -55,6 +70,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.errf("set: %v\n", err) return 2 } + r.updateExpandOpts() case "shift": n := 1 switch len(args) { @@ -91,7 +107,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } for _, arg := range args { - if _, ok := r.lookupVar(arg); ok && vars { + if vr := r.lookupVar(arg); vr.IsSet() && vars { r.delVar(arg) continue } @@ -100,14 +116,14 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } } case "echo": - newline, expand := true, false + newline, doExpand := true, false echoOpts: for len(args) > 0 { switch args[0] { case "-n": newline = false case "-e": - expand = true + doExpand = true case "-E": // default default: break echoOpts @@ -118,8 +134,8 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a if i > 0 { r.out(" ") } - if expand { - _, arg, _ = r.expandFormat(arg, nil) + if doExpand { + arg, _, _ = expand.Format(r.ecfg, arg, nil) } r.out(arg) } @@ -133,7 +149,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a } format, args := args[0], args[1:] for { - n, s, err := r.expandFormat(format, args) + s, n, err := expand.Format(r.ecfg, format, args) if err != nil { r.errf("%v\n", err) return 1 @@ -144,49 +160,35 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a break } } - case "break": + case "break", "continue": if !r.inLoop { - r.errf("break is only useful in a loop") + r.errf("%s is only useful in a loop", name) break } - switch len(args) { - case 0: - r.breakEnclosing = 1 - case 1: - if n, err := strconv.Atoi(args[0]); err == nil { - r.breakEnclosing = n - break - } - fallthrough - default: - r.errf("usage: break [n]\n") - return 2 - } - case "continue": - if !r.inLoop { - r.errf("continue is only useful in a loop") - break + enclosing := &r.breakEnclosing + if name == "continue" { + enclosing = &r.contnEnclosing } switch len(args) { case 0: - r.contnEnclosing = 1 + *enclosing = 1 case 1: if n, err := strconv.Atoi(args[0]); err == nil { - r.contnEnclosing = n + *enclosing = n break } fallthrough default: - r.errf("usage: continue [n]\n") + r.errf("usage: %s [n]\n", name) return 2 } case "pwd": - r.outf("%s\n", r.getVar("PWD")) + r.outf("%s\n", r.envGet("PWD")) case "cd": var path string switch len(args) { case 0: - path = r.getVar("HOME") + path = r.envGet("HOME") case 1: path = args[0] default: @@ -462,13 +464,13 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a args = append(args, "REPLY") } - values := r.ifsFields(string(line), len(args), raw) + values := expand.ReadFields(r.ecfg, string(line), len(args), raw) for i, name := range args { val := "" if i < len(values) { val = values[i] } - r.setVar(ctx, name, nil, Variable{Value: StringVal(val)}) + r.setVar(name, nil, expand.Variable{Value: val}) } return 0 @@ -478,7 +480,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.errf("getopts: usage: getopts optstring name [arg]\n") return 2 } - optind, _ := strconv.Atoi(r.getVar("OPTIND")) + optind, _ := strconv.Atoi(r.envGet("OPTIND")) if optind-1 != r.optState.argidx { if optind < 1 { optind = 1 @@ -499,7 +501,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a opt, optarg, done := r.optState.Next(optstr, args) - r.setVarString(ctx, name, string(opt)) + r.setVarString(name, string(opt)) r.delVar("OPTARG") switch { case opt == '?' && diagnostics && !done: @@ -508,11 +510,11 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.errf("getopts: option requires an argument -- %q\n", optarg) default: if optarg != "" { - r.setVarString(ctx, "OPTARG", optarg) + r.setVarString("OPTARG", optarg) } } if optind-1 != r.optState.argidx { - r.setVarString(ctx, "OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10)) + r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10)) } return oneIf(done) @@ -559,6 +561,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a r.printOptLine(arg, *opt) } } + r.updateExpandOpts() default: // "trap", "umask", "alias", "unalias", "fg", "bg", @@ -575,62 +578,6 @@ func (r *Runner) printOptLine(name string, enabled bool) { r.outf("%s\t%s\n", name, status) } -func (r *Runner) ifsFields(s string, n int, raw bool) []string { - type pos struct { - start, end int - } - var fpos []pos - - runes := make([]rune, 0, len(s)) - infield := false - esc := false - for _, c := range s { - if infield { - if r.ifsRune(c) && (raw || !esc) { - fpos[len(fpos)-1].end = len(runes) - infield = false - } - } else { - if !r.ifsRune(c) && (raw || !esc) { - fpos = append(fpos, pos{start: len(runes), end: -1}) - infield = true - } - } - if c == '\\' { - if raw || esc { - runes = append(runes, c) - } - esc = !esc - continue - } - runes = append(runes, c) - esc = false - } - if len(fpos) == 0 { - return nil - } - if infield { - fpos[len(fpos)-1].end = len(runes) - } - - switch { - case n == 1: - // include heading/trailing IFSs - fpos[0].start, fpos[0].end = 0, len(runes) - fpos = fpos[:1] - case n != -1 && n < len(fpos): - // combine to max n fields - fpos[n-1].end = fpos[len(fpos)-1].end - fpos = fpos[:n] - } - - var fields = make([]string, len(fpos)) - for i, p := range fpos { - fields[i] = string(runes[p.start:p.end]) - } - return fields -} - func (r *Runner) readLine(raw bool) ([]byte, error) { var line []byte esc := false @@ -675,7 +622,7 @@ func (r *Runner) changeDir(path string) int { } r.Dir = path r.Vars["OLDPWD"] = r.Vars["PWD"] - r.Vars["PWD"] = Variable{Value: StringVal(path)} + r.Vars["PWD"] = expand.Variable{Value: path} return 0 } diff --git a/vendor/mvdan.cc/sh/interp/doc.go b/vendor/mvdan.cc/sh/interp/doc.go index 6bd8f8a..70b9b02 100644 --- a/vendor/mvdan.cc/sh/interp/doc.go +++ b/vendor/mvdan.cc/sh/interp/doc.go @@ -4,7 +4,4 @@ // Package interp implements an interpreter that executes shell // programs. It aims to support POSIX, but its support is not complete // yet. It also supports some Bash features. -// -// This package is a work in progress and EXPERIMENTAL; its API is not -// subject to the 2.x backwards compatibility guarantee. package interp diff --git a/vendor/mvdan.cc/sh/interp/expand.go b/vendor/mvdan.cc/sh/interp/expand.go deleted file mode 100644 index 5d0bbb8..0000000 --- a/vendor/mvdan.cc/sh/interp/expand.go +++ /dev/null @@ -1,508 +0,0 @@ -// Copyright (c) 2017, Daniel Martí -// See LICENSE for licensing information - -package interp - -import ( - "context" - "fmt" - "os" - "os/user" - "path/filepath" - "regexp" - "runtime" - "sort" - "strconv" - "strings" - - "mvdan.cc/sh/expand" - "mvdan.cc/sh/syntax" -) - -func (r *Runner) expandFormat(format string, args []string) (int, string, error) { - buf := r.strBuilder() - esc := false - var fmts []rune - initialArgs := len(args) - - for _, c := range format { - switch { - case esc: - esc = false - switch c { - case 'n': - buf.WriteRune('\n') - case 'r': - buf.WriteRune('\r') - case 't': - buf.WriteRune('\t') - case '\\': - buf.WriteRune('\\') - default: - buf.WriteRune('\\') - buf.WriteRune(c) - } - - case len(fmts) > 0: - switch c { - case '%': - buf.WriteByte('%') - fmts = nil - case 'c': - var b byte - if len(args) > 0 { - arg := "" - arg, args = args[0], args[1:] - if len(arg) > 0 { - b = arg[0] - } - } - buf.WriteByte(b) - fmts = nil - case '+', '-', ' ': - if len(fmts) > 1 { - return 0, "", fmt.Errorf("invalid format char: %c", c) - } - fmts = append(fmts, c) - case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': - fmts = append(fmts, c) - case 's', 'd', 'i', 'u', 'o', 'x': - arg := "" - if len(args) > 0 { - arg, args = args[0], args[1:] - } - var farg interface{} = arg - if c != 's' { - n, _ := strconv.ParseInt(arg, 0, 0) - if c == 'i' || c == 'd' { - farg = int(n) - } else { - farg = uint(n) - } - if c == 'i' || c == 'u' { - c = 'd' - } - } - fmts = append(fmts, c) - fmt.Fprintf(buf, string(fmts), farg) - fmts = nil - default: - return 0, "", fmt.Errorf("invalid format char: %c", c) - } - case c == '\\': - esc = true - case args != nil && c == '%': - // if args == nil, we are not doing format - // arguments - fmts = []rune{c} - default: - buf.WriteRune(c) - } - } - if len(fmts) > 0 { - return 0, "", fmt.Errorf("missing format char") - } - return initialArgs - len(args), buf.String(), nil -} - -func (r *Runner) fieldJoin(parts []fieldPart) string { - switch len(parts) { - case 0: - return "" - case 1: // short-cut without a string copy - return parts[0].val - } - buf := r.strBuilder() - for _, part := range parts { - buf.WriteString(part.val) - } - return buf.String() -} - -func (r *Runner) escapedGlobField(parts []fieldPart) (escaped string, glob bool) { - buf := r.strBuilder() - for _, part := range parts { - if part.quote > quoteNone { - buf.WriteString(syntax.QuotePattern(part.val)) - continue - } - buf.WriteString(part.val) - if syntax.HasPattern(part.val) { - glob = true - } - } - if glob { // only copy the string if it will be used - escaped = buf.String() - } - return escaped, glob -} - -func (r *Runner) Fields(ctx context.Context, words ...*syntax.Word) ([]string, error) { - if !r.didReset { - r.Reset() - } - return r.fields(ctx, words...), r.err -} - -func (r *Runner) fields(ctx context.Context, words ...*syntax.Word) []string { - fields := make([]string, 0, len(words)) - baseDir := syntax.QuotePattern(r.Dir) - for _, word := range words { - for _, expWord := range expand.Braces(word) { - for _, field := range r.wordFields(ctx, expWord.Parts) { - path, doGlob := r.escapedGlobField(field) - var matches []string - abs := filepath.IsAbs(path) - if doGlob && !r.opts[optNoGlob] { - if !abs { - path = filepath.Join(baseDir, path) - } - matches = glob(path, r.opts[optGlobStar]) - } - if len(matches) == 0 { - fields = append(fields, r.fieldJoin(field)) - continue - } - for _, match := range matches { - if !abs { - endSeparator := strings.HasSuffix(match, string(filepath.Separator)) - match, _ = filepath.Rel(r.Dir, match) - if endSeparator { - match += string(filepath.Separator) - } - } - fields = append(fields, match) - } - } - } - } - return fields -} - -func (r *Runner) loneWord(ctx context.Context, word *syntax.Word) string { - if word == nil { - return "" - } - field := r.wordField(ctx, word.Parts, quoteDouble) - return r.fieldJoin(field) -} - -func (r *Runner) lonePattern(ctx context.Context, word *syntax.Word) string { - field := r.wordField(ctx, word.Parts, quoteSingle) - buf := r.strBuilder() - for _, part := range field { - if part.quote > quoteNone { - buf.WriteString(syntax.QuotePattern(part.val)) - } else { - buf.WriteString(part.val) - } - } - return buf.String() -} - -func (r *Runner) expandAssigns(ctx context.Context, as *syntax.Assign) []*syntax.Assign { - // Convert "declare $x" into "declare value". - // Don't use syntax.Parser here, as we only want the basic - // splitting by '='. - if as.Name != nil { - return []*syntax.Assign{as} // nothing to do - } - var asgns []*syntax.Assign - for _, field := range r.fields(ctx, as.Value) { - as := &syntax.Assign{} - parts := strings.SplitN(field, "=", 2) - as.Name = &syntax.Lit{Value: parts[0]} - if len(parts) == 1 { - as.Naked = true - } else { - as.Value = &syntax.Word{Parts: []syntax.WordPart{ - &syntax.Lit{Value: parts[1]}, - }} - } - asgns = append(asgns, as) - } - return asgns -} - -type fieldPart struct { - val string - quote quoteLevel -} - -type quoteLevel uint - -const ( - quoteNone quoteLevel = iota - quoteDouble - quoteSingle -) - -func (r *Runner) wordField(ctx context.Context, wps []syntax.WordPart, ql quoteLevel) []fieldPart { - var field []fieldPart - for i, wp := range wps { - switch x := wp.(type) { - case *syntax.Lit: - s := x.Value - if i == 0 { - s = r.expandUser(s) - } - if ql == quoteDouble && strings.Contains(s, "\\") { - buf := r.strBuilder() - for i := 0; i < len(s); i++ { - b := s[i] - if b == '\\' && i+1 < len(s) { - switch s[i+1] { - case '\n': // remove \\\n - i++ - continue - case '"', '\\', '$', '`': // special chars - continue - } - } - buf.WriteByte(b) - } - s = buf.String() - } - field = append(field, fieldPart{val: s}) - case *syntax.SglQuoted: - fp := fieldPart{quote: quoteSingle, val: x.Value} - if x.Dollar { - _, fp.val, _ = r.expandFormat(fp.val, nil) - } - field = append(field, fp) - case *syntax.DblQuoted: - for _, part := range r.wordField(ctx, x.Parts, quoteDouble) { - part.quote = quoteDouble - field = append(field, part) - } - case *syntax.ParamExp: - field = append(field, fieldPart{val: r.paramExp(ctx, x)}) - case *syntax.CmdSubst: - field = append(field, fieldPart{val: r.cmdSubst(ctx, x)}) - case *syntax.ArithmExp: - field = append(field, fieldPart{ - val: strconv.Itoa(r.arithm(ctx, x.X)), - }) - default: - panic(fmt.Sprintf("unhandled word part: %T", x)) - } - } - return field -} - -func (r *Runner) cmdSubst(ctx context.Context, cs *syntax.CmdSubst) string { - r2 := r.sub() - buf := r.strBuilder() - r2.Stdout = buf - r2.stmts(ctx, cs.StmtList) - r.setErr(r2.err) - return strings.TrimRight(buf.String(), "\n") -} - -func (r *Runner) wordFields(ctx context.Context, wps []syntax.WordPart) [][]fieldPart { - fields := r.fieldsAlloc[:0] - curField := r.fieldAlloc[:0] - allowEmpty := false - flush := func() { - if len(curField) == 0 { - return - } - fields = append(fields, curField) - curField = nil - } - splitAdd := func(val string) { - for i, field := range strings.FieldsFunc(val, r.ifsRune) { - if i > 0 { - flush() - } - curField = append(curField, fieldPart{val: field}) - } - } - for i, wp := range wps { - switch x := wp.(type) { - case *syntax.Lit: - s := x.Value - if i == 0 { - s = r.expandUser(s) - } - if strings.Contains(s, "\\") { - buf := r.strBuilder() - for i := 0; i < len(s); i++ { - b := s[i] - if b == '\\' { - i++ - b = s[i] - } - buf.WriteByte(b) - } - s = buf.String() - } - curField = append(curField, fieldPart{val: s}) - case *syntax.SglQuoted: - allowEmpty = true - fp := fieldPart{quote: quoteSingle, val: x.Value} - if x.Dollar { - _, fp.val, _ = r.expandFormat(fp.val, nil) - } - curField = append(curField, fp) - case *syntax.DblQuoted: - allowEmpty = true - if len(x.Parts) == 1 { - pe, _ := x.Parts[0].(*syntax.ParamExp) - if elems := r.quotedElems(pe); elems != nil { - for i, elem := range elems { - if i > 0 { - flush() - } - curField = append(curField, fieldPart{ - quote: quoteDouble, - val: elem, - }) - } - continue - } - } - for _, part := range r.wordField(ctx, x.Parts, quoteDouble) { - part.quote = quoteDouble - curField = append(curField, part) - } - case *syntax.ParamExp: - splitAdd(r.paramExp(ctx, x)) - case *syntax.CmdSubst: - splitAdd(r.cmdSubst(ctx, x)) - case *syntax.ArithmExp: - curField = append(curField, fieldPart{ - val: strconv.Itoa(r.arithm(ctx, x.X)), - }) - default: - panic(fmt.Sprintf("unhandled word part: %T", x)) - } - } - flush() - if allowEmpty && len(fields) == 0 { - fields = append(fields, curField) - } - return fields -} - -func (r *Runner) expandUser(field string) string { - if len(field) == 0 || field[0] != '~' { - return field - } - name := field[1:] - rest := "" - if i := strings.Index(name, "/"); i >= 0 { - rest = name[i:] - name = name[:i] - } - if name == "" { - return r.getVar("HOME") + rest - } - u, err := user.Lookup(name) - if err != nil { - return field - } - return u.HomeDir + rest -} - -func match(pattern, name string) bool { - expr, err := syntax.TranslatePattern(pattern, true) - if err != nil { - return false - } - rx := regexp.MustCompile("^" + expr + "$") - return rx.MatchString(name) -} - -func findAllIndex(pattern, name string, n int) [][]int { - expr, err := syntax.TranslatePattern(pattern, true) - if err != nil { - return nil - } - rx := regexp.MustCompile(expr) - return rx.FindAllStringIndex(name, n) -} - -func hasGlob(path string) bool { - magicChars := `*?[` - if runtime.GOOS != "windows" { - magicChars = `*?[\` - } - return strings.ContainsAny(path, magicChars) -} - -var rxGlobStar = regexp.MustCompile(".*") - -func glob(pattern string, globStar bool) []string { - parts := strings.Split(pattern, string(filepath.Separator)) - matches := []string{"."} - if filepath.IsAbs(pattern) { - if parts[0] == "" { - // unix-like - matches[0] = string(filepath.Separator) - } else { - // windows (for some reason it won't work without the - // trailing separator) - matches[0] = parts[0] + string(filepath.Separator) - } - parts = parts[1:] - } - for _, part := range parts { - if part == "**" && globStar { - for i := range matches { - // "a/**" should match "a/ a/b a/b/c ..."; note - // how the zero-match case has a trailing - // separator. - matches[i] += string(filepath.Separator) - } - // expand all the possible levels of ** - latest := matches - for { - var newMatches []string - for _, dir := range latest { - newMatches = globDir(dir, rxGlobStar, newMatches) - } - if len(newMatches) == 0 { - // not another level of directories to - // try; stop - break - } - matches = append(matches, newMatches...) - latest = newMatches - } - continue - } - expr, err := syntax.TranslatePattern(part, true) - if err != nil { - return nil - } - rx := regexp.MustCompile("^" + expr + "$") - var newMatches []string - for _, dir := range matches { - newMatches = globDir(dir, rx, newMatches) - } - matches = newMatches - } - return matches -} - -func globDir(dir string, rx *regexp.Regexp, matches []string) []string { - d, err := os.Open(dir) - if err != nil { - return nil - } - defer d.Close() - - names, _ := d.Readdirnames(-1) - sort.Strings(names) - - for _, name := range names { - if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' { - continue - } - if rx.MatchString(name) { - matches = append(matches, filepath.Join(dir, name)) - } - } - return matches -} diff --git a/vendor/mvdan.cc/sh/interp/interp.go b/vendor/mvdan.cc/sh/interp/interp.go index b9edcbf..5fd53e8 100644 --- a/vendor/mvdan.cc/sh/interp/interp.go +++ b/vendor/mvdan.cc/sh/interp/interp.go @@ -13,6 +13,7 @@ import ( "os" "os/user" "path/filepath" + "regexp" "runtime" "strings" "sync" @@ -20,6 +21,7 @@ import ( "golang.org/x/sync/errgroup" + "mvdan.cc/sh/expand" "mvdan.cc/sh/syntax" ) @@ -46,10 +48,10 @@ func New(opts ...func(*Runner) error) (*Runner, error) { } } if r.Exec == nil { - Module(nil)(r) + Module(ModuleExec(nil))(r) } if r.Open == nil { - Module(nil)(r) + Module(ModuleOpen(nil))(r) } if r.Stdout == nil || r.Stderr == nil { StdIO(r.Stdin, r.Stdout, r.Stderr)(r) @@ -57,12 +59,127 @@ func New(opts ...func(*Runner) error) (*Runner, error) { return r, nil } -// Env sets the interpreter's environment. If nil, the current process's -// environment is used. -func Env(env Environ) func(*Runner) error { +func (r *Runner) fillExpandConfig(ctx context.Context) { + r.ectx = ctx + r.ecfg = &expand.Config{ + Env: expandEnv{r}, + CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error { + switch len(cs.Stmts) { + case 0: // nothing to do + return nil + case 1: // $( 0 { r.setErr(ExitStatus(r.exit)) @@ -564,6 +668,7 @@ func (r *Runner) sub() *Runner { // Keep in sync with the Runner type. Manually copy fields, to not copy // sensitive ones like errgroup.Group, and to do deep copies of slices. r2 := &Runner{ + Env: r.Env, Dir: r.Dir, Params: r.Params, Exec: r.Exec, @@ -576,19 +681,20 @@ func (r *Runner) sub() *Runner { filename: r.filename, opts: r.opts, } - // TODO: perhaps we could do a lazy copy here, or some sort of - // overlay to avoid copying all the time - r2.Env = r.Env.Copy() - r2.Vars = make(map[string]Variable, len(r.Vars)) + r2.Vars = make(map[string]expand.Variable, len(r.Vars)) for k, v := range r.Vars { r2.Vars[k] = v } + r2.funcVars = make(map[string]expand.Variable, len(r.funcVars)) + for k, v := range r.funcVars { + r2.funcVars[k] = v + } r2.cmdVars = make(map[string]string, len(r.cmdVars)) for k, v := range r.cmdVars { r2.cmdVars[k] = v } r2.dirStack = append([]string(nil), r.dirStack...) - r2.ifsUpdated() + r2.fillExpandConfig(r.ectx) r2.didReset = true return r2 } @@ -606,23 +712,19 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { r.exit = r2.exit r.setErr(r2.err) case *syntax.CallExpr: - fields := r.fields(ctx, x.Args...) + fields := r.fields(x.Args...) if len(fields) == 0 { for _, as := range x.Assigns { - vr, _ := r.lookupVar(as.Name.Value) - vr.Value = r.assignVal(ctx, as, "") - r.setVar(ctx, as.Name.Value, as.Index, vr) + vr := r.lookupVar(as.Name.Value) + vr.Value = r.assignVal(as, "") + r.setVar(as.Name.Value, as.Index, vr) } break } for _, as := range x.Assigns { - val := r.assignVal(ctx, as, "") + val := r.assignVal(as, "") // we know that inline vars must be strings - r.cmdVars[as.Name.Value] = string(val.(StringVal)) - if as.Name.Value == "IFS" { - r.ifsUpdated() - defer r.ifsUpdated() - } + r.cmdVars[as.Name.Value] = val.(string) } r.call(ctx, x.Args[0].Pos(), fields) // cmdVars can be nuked here, as they are never useful @@ -689,37 +791,41 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { switch y := x.Loop.(type) { case *syntax.WordIter: name := y.Name.Value - for _, field := range r.fields(ctx, y.Items...) { - r.setVarString(ctx, name, field) + items := r.Params // for i; do ... + if y.InPos.IsValid() { + items = r.fields(y.Items...) // for i in ...; do ... + } + for _, field := range items { + r.setVarString(name, field) if r.loopStmtsBroken(ctx, x.Do) { break } } case *syntax.CStyleLoop: - r.arithm(ctx, y.Init) - for r.arithm(ctx, y.Cond) != 0 { + r.arithm(y.Init) + for r.arithm(y.Cond) != 0 { if r.loopStmtsBroken(ctx, x.Do) { break } - r.arithm(ctx, y.Post) + r.arithm(y.Post) } } case *syntax.FuncDecl: r.setFunc(x.Name.Value, x.Body) case *syntax.ArithmCmd: - r.exit = oneIf(r.arithm(ctx, x.X) == 0) + r.exit = oneIf(r.arithm(x.X) == 0) case *syntax.LetClause: var val int for _, expr := range x.Exprs { - val = r.arithm(ctx, expr) + val = r.arithm(expr) } r.exit = oneIf(val == 0) case *syntax.CaseClause: - str := r.loneWord(ctx, x.Word) + str := r.literal(x.Word) for _, ci := range x.Items { for _, word := range ci.Patterns { - pat := r.lonePattern(ctx, word) - if match(pat, str) { + pattern := r.pattern(word) + if match(pattern, str) { r.stmts(ctx, ci.StmtList) return } @@ -732,13 +838,13 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { r.exit = 1 } case *syntax.DeclClause: - local := false + local, global := false, false var modes []string valType := "" switch x.Variant.Value { case "declare": - // When used in a function, "declare" acts as - // "local" unless the "-g" option is used. + // When used in a function, "declare" acts as "local" + // unless the "-g" option is used. local = r.inFunc case "local": if !r.inFunc { @@ -755,13 +861,13 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { modes = append(modes, "-n") } for _, opt := range x.Opts { - switch s := r.loneWord(ctx, opt); s { + switch s := r.literal(opt); s { case "-x", "-r", "-n": modes = append(modes, s) case "-a", "-A": valType = s case "-g": - local = false + global = true default: r.errf("declare: invalid option %q\n", s) r.exit = 2 @@ -769,11 +875,20 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { } } for _, as := range x.Assigns { - for _, as := range r.expandAssigns(ctx, as) { + for _, as := range r.flattenAssign(as) { name := as.Name.Value - vr, _ := r.lookupVar(as.Name.Value) - vr.Value = r.assignVal(ctx, as, valType) - vr.Local = local + if !syntax.ValidName(name) { + r.errf("declare: invalid name %q\n", name) + r.exit = 1 + return + } + vr := r.lookupVar(as.Name.Value) + vr.Value = r.assignVal(as, valType) + if global { + vr.Local = false + } else if local { + vr.Local = true + } for _, mode := range modes { switch mode { case "-x": @@ -784,7 +899,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { vr.NameRef = true } } - r.setVar(ctx, name, as.Index, vr) + r.setVar(name, as.Index, vr) } } case *syntax.TimeClause: @@ -808,6 +923,39 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) { } } +func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign { + // Convert "declare $x" into "declare value". + // Don't use syntax.Parser here, as we only want the basic + // splitting by '='. + if as.Name != nil { + return []*syntax.Assign{as} // nothing to do + } + var asgns []*syntax.Assign + for _, field := range r.fields(as.Value) { + as := &syntax.Assign{} + parts := strings.SplitN(field, "=", 2) + as.Name = &syntax.Lit{Value: parts[0]} + if len(parts) == 1 { + as.Naked = true + } else { + as.Value = &syntax.Word{Parts: []syntax.WordPart{ + &syntax.Lit{Value: parts[1]}, + }} + } + asgns = append(asgns, as) + } + return asgns +} + +func match(pattern, name string) bool { + expr, err := syntax.TranslatePattern(pattern, true) + if err != nil { + return false + } + rx := regexp.MustCompile("^" + expr + "$") + return rx.MatchString(name) +} + func elapsedString(d time.Duration, posix bool) string { if posix { return fmt.Sprintf("%.2f", d.Seconds()) @@ -823,10 +971,42 @@ func (r *Runner) stmts(ctx context.Context, sl syntax.StmtList) { } } +func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader { + if rd.Op != syntax.DashHdoc { + hdoc := r.document(rd.Hdoc) + return strings.NewReader(hdoc) + } + var buf bytes.Buffer + var cur []syntax.WordPart + flushLine := func() { + if buf.Len() > 0 { + buf.WriteByte('\n') + } + buf.WriteString(r.document(&syntax.Word{Parts: cur})) + cur = cur[:0] + } + for _, wp := range rd.Hdoc.Parts { + lit, ok := wp.(*syntax.Lit) + if !ok { + cur = append(cur, wp) + continue + } + for i, part := range strings.Split(lit.Value, "\n") { + if i > 0 { + flushLine() + cur = cur[:0] + } + part = strings.TrimLeft(part, "\t") + cur = append(cur, &syntax.Lit{Value: part}) + } + } + flushLine() + return &buf +} + func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) { if rd.Hdoc != nil { - hdoc := r.loneWord(ctx, rd.Hdoc) - r.Stdin = strings.NewReader(hdoc) + r.Stdin = r.hdocReader(rd) return nil, nil } orig := &r.Stdout @@ -837,7 +1017,7 @@ func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, err orig = &r.Stderr } } - arg := r.loneWord(ctx, rd.Word) + arg := r.literal(rd.Word) switch rd.Op { case syntax.WordHdoc: r.Stdin = strings.NewReader(arg + "\n") @@ -1006,6 +1186,10 @@ func (r *Runner) findExecutable(file string, exts []string) string { return "" } +func driveLetter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + // splitList is like filepath.SplitList, but always using the unix path // list separator ':'. On Windows, it also makes sure not to split // [A-Z]:[/\]. @@ -1022,8 +1206,7 @@ func splitList(path string) []string { for i := 0; i < len(list); i++ { s := list[i] switch { - case len(s) != 1, s[0] < 'A', s[0] > 'Z': - // not a disk name + case len(s) != 1, !driveLetter(s[0]): case i+1 >= len(list): // last element case strings.IndexAny(list[i+1], `/\`) != 0: @@ -1039,7 +1222,7 @@ func splitList(path string) []string { } func (r *Runner) lookPath(file string) string { - pathList := splitList(r.getVar("PATH")) + pathList := splitList(r.envGet("PATH")) chars := `/` if runtime.GOOS == "windows" { chars = `:\/` @@ -1070,7 +1253,7 @@ func (r *Runner) pathExts() []string { if runtime.GOOS != "windows" { return nil } - pathext := r.getVar("PATHEXT") + pathext := r.envGet("PATHEXT") if pathext == "" { return []string{".com", ".exe", ".bat", ".cmd"} } diff --git a/vendor/mvdan.cc/sh/interp/module.go b/vendor/mvdan.cc/sh/interp/module.go index 21771f4..9e288bb 100644 --- a/vendor/mvdan.cc/sh/interp/module.go +++ b/vendor/mvdan.cc/sh/interp/module.go @@ -13,6 +13,8 @@ import ( "strings" "syscall" "time" + + "mvdan.cc/sh/expand" ) // FromModuleContext returns the ModuleCtx value stored in ctx, if any. @@ -27,7 +29,7 @@ type moduleCtxKey struct{} // It contains some of the current state of the Runner, as well as some fields // necessary to implement some of the modules. type ModuleCtx struct { - Env Environ + Env expand.Environ Dir string Stdin io.Reader Stdout io.Writer diff --git a/vendor/mvdan.cc/sh/interp/test.go b/vendor/mvdan.cc/sh/interp/test.go index 7fd867e..96cc311 100644 --- a/vendor/mvdan.cc/sh/interp/test.go +++ b/vendor/mvdan.cc/sh/interp/test.go @@ -19,22 +19,22 @@ import ( func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string { switch x := expr.(type) { case *syntax.Word: - return r.loneWord(ctx, x) + return r.document(x) case *syntax.ParenTest: return r.bashTest(ctx, x.X, classic) case *syntax.BinaryTest: switch x.Op { case syntax.TsMatch, syntax.TsNoMatch: - str := r.loneWord(ctx, x.X.(*syntax.Word)) + str := r.literal(x.X.(*syntax.Word)) yw := x.Y.(*syntax.Word) if classic { // test, [ - lit := r.loneWord(ctx, yw) + lit := r.literal(yw) if (str == lit) == (x.Op == syntax.TsMatch) { return "1" } } else { // [[ - pat := r.lonePattern(ctx, yw) - if match(pat, str) == (x.Op == syntax.TsMatch) { + pattern := r.pattern(yw) + if match(pattern, str) == (x.Op == syntax.TsMatch) { return "1" } } @@ -173,11 +173,9 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string) } return false case syntax.TsVarSet: - _, e := r.lookupVar(x) - return e + return r.lookupVar(x).IsSet() case syntax.TsRefVar: - v, _ := r.lookupVar(x) - return v.NameRef + return r.lookupVar(x).NameRef case syntax.TsNot: return x == "" default: diff --git a/vendor/mvdan.cc/sh/interp/vars.go b/vendor/mvdan.cc/sh/interp/vars.go index 4547325..f0b2640 100644 --- a/vendor/mvdan.cc/sh/interp/vars.go +++ b/vendor/mvdan.cc/sh/interp/vars.go @@ -4,259 +4,137 @@ package interp import ( - "context" - "fmt" + "os" "runtime" - "sort" + "strconv" "strings" + "mvdan.cc/sh/expand" "mvdan.cc/sh/syntax" ) -type Environ interface { - Get(name string) (value string, exists bool) - Set(name, value string) - Delete(name string) - Names() []string - Copy() Environ +type overlayEnviron struct { + parent expand.Environ + values map[string]expand.Variable } -type mapEnviron struct { - names []string - values map[string]string +func (o overlayEnviron) Get(name string) expand.Variable { + if vr, ok := o.values[name]; ok { + return vr + } + return o.parent.Get(name) } -func (m *mapEnviron) Get(name string) (string, bool) { - val, ok := m.values[name] - return val, ok +func (o overlayEnviron) Set(name string, vr expand.Variable) { + o.values[name] = vr } -func (m *mapEnviron) Set(name, value string) { - _, ok := m.values[name] - if !ok { - m.names = append(m.names, name) - sort.Strings(m.names) - } - m.values[name] = value -} - -func (m *mapEnviron) Delete(name string) { - if _, ok := m.values[name]; !ok { - return - } - delete(m.values, name) - for i, iname := range m.names { - if iname == name { - m.names = append(m.names[:i], m.names[i+1:]...) +func (o overlayEnviron) Each(f func(name string, vr expand.Variable) bool) { + o.parent.Each(f) + for name, vr := range o.values { + if !f(name, vr) { return } } } -func (m *mapEnviron) Names() []string { - return m.names -} - -func (m *mapEnviron) Copy() Environ { - m2 := &mapEnviron{ - names: make([]string, len(m.names)), - values: make(map[string]string, len(m.values)), - } - copy(m2.names, m.names) - for name, val := range m.values { - m2.values[name] = val - } - return m2 -} - -func execEnv(env Environ) []string { - names := env.Names() - list := make([]string, len(names)) - for i, name := range names { - val, _ := env.Get(name) - list[i] = name + "=" + val - } +func execEnv(env expand.Environ) []string { + list := make([]string, 0, 32) + env.Each(func(name string, vr expand.Variable) bool { + if vr.Exported { + list = append(list, name+"="+vr.String()) + } + return true + }) return list } -func EnvFromList(list []string) (Environ, error) { - m := mapEnviron{ - names: make([]string, 0, len(list)), - values: make(map[string]string, len(list)), - } - for _, kv := range list { - i := strings.IndexByte(kv, '=') - if i < 0 { - return nil, fmt.Errorf("env not in the form key=value: %q", kv) +func (r *Runner) lookupVar(name string) expand.Variable { + if name == "" { + panic("variable name must not be empty") + } + var value interface{} + switch name { + case "#": + value = strconv.Itoa(len(r.Params)) + case "@", "*": + value = r.Params + case "?": + value = strconv.Itoa(r.exit) + case "$": + value = strconv.Itoa(os.Getpid()) + case "PPID": + value = strconv.Itoa(os.Getppid()) + case "DIRSTACK": + value = r.dirStack + case "0": + if r.filename != "" { + value = r.filename + } else { + value = "gosh" } - name, val := kv[:i], kv[i+1:] - if runtime.GOOS == "windows" { - name = strings.ToUpper(name) + case "1", "2", "3", "4", "5", "6", "7", "8", "9": + i := int(name[0] - '1') + if i < len(r.Params) { + value = r.Params[i] + } else { + value = "" } - m.names = append(m.names, name) - m.values[name] = val } - sort.Strings(m.names) - return &m, nil -} - -type FuncEnviron func(string) string - -func (f FuncEnviron) Get(name string) (string, bool) { - val := f(name) - return val, val != "" -} - -func (f FuncEnviron) Set(name, value string) {} -func (f FuncEnviron) Delete(name string) {} -func (f FuncEnviron) Names() []string { return nil } -func (f FuncEnviron) Copy() Environ { return f } - -type Variable struct { - Local bool - Exported bool - ReadOnly bool - NameRef bool - Value VarValue -} - -// VarValue is one of: -// -// StringVal -// IndexArray -// AssocArray -type VarValue interface{} - -type StringVal string - -type IndexArray []string - -type AssocArray map[string]string - -func (r *Runner) lookupVar(name string) (Variable, bool) { - if name == "" { - panic("variable name must not be empty") + if value != nil { + return expand.Variable{Value: value} } - if val, e := r.cmdVars[name]; e { - return Variable{Value: StringVal(val)}, true + if value, e := r.cmdVars[name]; e { + return expand.Variable{Value: value} } if vr, e := r.funcVars[name]; e { - return vr, true + vr.Local = true + return vr } if vr, e := r.Vars[name]; e { - return vr, true + return vr } - if str, e := r.Env.Get(name); e { - return Variable{Value: StringVal(str)}, true + if vr := r.Env.Get(name); vr.IsSet() { + return vr } if runtime.GOOS == "windows" { upper := strings.ToUpper(name) - if str, e := r.Env.Get(upper); e { - return Variable{Value: StringVal(str)}, true + if vr := r.Env.Get(upper); vr.IsSet() { + return vr } } if r.opts[optNoUnset] { r.errf("%s: unbound variable\n", name) r.setErr(ShellExitStatus(1)) } - return Variable{}, false + return expand.Variable{} } -func (r *Runner) getVar(name string) string { - val, _ := r.lookupVar(name) - return r.varStr(val, 0) +func (r *Runner) envGet(name string) string { + return r.lookupVar(name).String() } func (r *Runner) delVar(name string) { - val, _ := r.lookupVar(name) - if val.ReadOnly { + vr := r.lookupVar(name) + if vr.ReadOnly { r.errf("%s: readonly variable\n", name) r.exit = 1 return } - delete(r.Vars, name) - delete(r.funcVars, name) - delete(r.cmdVars, name) - r.Env.Delete(name) -} - -// maxNameRefDepth defines the maximum number of times to follow -// references when expanding a variable. Otherwise, simple name -// reference loops could crash the interpreter quite easily. -const maxNameRefDepth = 100 - -func (r *Runner) varStr(vr Variable, depth int) string { - if depth > maxNameRefDepth { - return "" - } - switch x := vr.Value.(type) { - case StringVal: - if vr.NameRef { - vr, _ = r.lookupVar(string(x)) - return r.varStr(vr, depth+1) - } - return string(x) - case IndexArray: - if len(x) > 0 { - return x[0] - } - case AssocArray: - // nothing to do - } - return "" -} - -func (r *Runner) varInd(ctx context.Context, vr Variable, e syntax.ArithmExpr, depth int) string { - if depth > maxNameRefDepth { - return "" - } - switch x := vr.Value.(type) { - case StringVal: - if vr.NameRef { - vr, _ = r.lookupVar(string(x)) - return r.varInd(ctx, vr, e, depth+1) - } - if r.arithm(ctx, e) == 0 { - return string(x) - } - case IndexArray: - switch anyOfLit(e, "@", "*") { - case "@": - return strings.Join(x, " ") - case "*": - return strings.Join(x, r.ifsJoin) - } - i := r.arithm(ctx, e) - if len(x) > 0 { - return x[i] - } - case AssocArray: - if lit := anyOfLit(e, "@", "*"); lit != "" { - var strs IndexArray - keys := make([]string, 0, len(x)) - for k := range x { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - strs = append(strs, x[k]) - } - if lit == "*" { - return strings.Join(strs, r.ifsJoin) - } - return strings.Join(strs, " ") - } - return x[r.loneWord(ctx, e.(*syntax.Word))] + if vr.Local { + // don't overwrite a non-local var with the same name + r.funcVars[name] = expand.Variable{} + } else { + r.Vars[name] = expand.Variable{} // to not query r.Env } - return "" } -func (r *Runner) setVarString(ctx context.Context, name, val string) { - r.setVar(ctx, name, nil, Variable{Value: StringVal(val)}) +func (r *Runner) setVarString(name, value string) { + r.setVar(name, nil, expand.Variable{Value: value}) } -func (r *Runner) setVarInternal(name string, vr Variable) { - if _, ok := vr.Value.(StringVal); ok { +func (r *Runner) setVarInternal(name string, vr expand.Variable) { + if _, ok := vr.Value.(string); ok { if r.opts[optAllExport] { vr.Exported = true } @@ -265,28 +143,31 @@ func (r *Runner) setVarInternal(name string, vr Variable) { } if vr.Local { if r.funcVars == nil { - r.funcVars = make(map[string]Variable) + r.funcVars = make(map[string]expand.Variable) } r.funcVars[name] = vr } else { r.Vars[name] = vr } - if name == "IFS" { - r.ifsUpdated() - } } -func (r *Runner) setVar(ctx context.Context, name string, index syntax.ArithmExpr, vr Variable) { - cur, _ := r.lookupVar(name) +func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable) { + cur := r.lookupVar(name) if cur.ReadOnly { r.errf("%s: readonly variable\n", name) r.exit = 1 return } - _, isIndexArray := cur.Value.(IndexArray) - _, isAssocArray := cur.Value.(AssocArray) + if name2, var2 := cur.Resolve(r.Env); name2 != "" { + name = name2 + cur = var2 + vr.NameRef = false + cur.NameRef = false + } + _, isIndexArray := cur.Value.([]string) + _, isAssocArray := cur.Value.(map[string]string) - if _, ok := vr.Value.(StringVal); ok && index == nil { + if _, ok := vr.Value.(string); ok && index == nil { // When assigning a string to an array, fall back to the // zero value for the index. if isIndexArray { @@ -304,33 +185,33 @@ func (r *Runner) setVar(ctx context.Context, name string, index syntax.ArithmExp return } - // from the syntax package, we know that val must be a string if - // index is non-nil; nested arrays are forbidden. - valStr := string(vr.Value.(StringVal)) + // from the syntax package, we know that value must be a string if index + // is non-nil; nested arrays are forbidden. + valStr := vr.Value.(string) // if the existing variable is already an AssocArray, try our best // to convert the key to a string if isAssocArray { - amap := cur.Value.(AssocArray) + amap := cur.Value.(map[string]string) w, ok := index.(*syntax.Word) if !ok { return } - k := r.loneWord(ctx, w) + k := r.literal(w) amap[k] = valStr cur.Value = amap r.setVarInternal(name, cur) return } - var list IndexArray + var list []string switch x := cur.Value.(type) { - case StringVal: - list = append(list, string(x)) - case IndexArray: + case string: + list = append(list, x) + case []string: list = x - case AssocArray: // done above + case map[string]string: // done above } - k := r.arithm(ctx, index) + k := r.arithm(index) for len(list) < k+1 { list = append(list, "") } @@ -358,32 +239,33 @@ func stringIndex(index syntax.ArithmExpr) bool { return false } -func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType string) VarValue { - prev, prevOk := r.lookupVar(as.Name.Value) +func (r *Runner) assignVal(as *syntax.Assign, valType string) interface{} { + prev := r.lookupVar(as.Name.Value) if as.Naked { return prev.Value } if as.Value != nil { - s := r.loneWord(ctx, as.Value) - if !as.Append || !prevOk { - return StringVal(s) + s := r.literal(as.Value) + if !as.Append || !prev.IsSet() { + return s } switch x := prev.Value.(type) { - case StringVal: - return x + StringVal(s) - case IndexArray: + case string: + return x + s + case []string: if len(x) == 0 { x = append(x, "") } x[0] += s return x - case AssocArray: + case map[string]string: // TODO } - return StringVal(s) + return s } if as.Array == nil { - return nil + // don't return nil, as that's an unset variable + return "" } elems := as.Array.Elems if valType == "" { @@ -395,12 +277,12 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin } if valType == "-A" { // associative array - amap := AssocArray(make(map[string]string, len(elems))) + amap := make(map[string]string, len(elems)) for _, elem := range elems { - k := r.loneWord(ctx, elem.Index.(*syntax.Word)) - amap[k] = r.loneWord(ctx, elem.Value) + k := r.literal(elem.Index.(*syntax.Word)) + amap[k] = r.literal(elem.Value) } - if !as.Append || !prevOk { + if !as.Append || !prev.IsSet() { return amap } // TODO @@ -414,7 +296,7 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin indexes[i] = i continue } - k := r.arithm(ctx, elem.Index) + k := r.arithm(elem.Index) indexes[i] = k if k > maxIndex { maxIndex = k @@ -422,50 +304,18 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin } strs := make([]string, maxIndex+1) for i, elem := range elems { - strs[indexes[i]] = r.loneWord(ctx, elem.Value) + strs[indexes[i]] = r.literal(elem.Value) } - if !as.Append || !prevOk { - return IndexArray(strs) + if !as.Append || !prev.IsSet() { + return strs } switch x := prev.Value.(type) { - case StringVal: - prevList := IndexArray([]string{string(x)}) - return append(prevList, strs...) - case IndexArray: + case string: + return append([]string{x}, strs...) + case []string: return append(x, strs...) - case AssocArray: + case map[string]string: // TODO } - return IndexArray(strs) -} - -func (r *Runner) ifsUpdated() { - runes := r.getVar("IFS") - r.ifsJoin = "" - if len(runes) > 0 { - r.ifsJoin = runes[:1] - } - r.ifsRune = func(r rune) bool { - for _, r2 := range runes { - if r == r2 { - return true - } - } - return false - } -} - -func (r *Runner) namesByPrefix(prefix string) []string { - var names []string - for _, name := range r.Env.Names() { - if strings.HasPrefix(name, prefix) { - names = append(names, name) - } - } - for name := range r.Vars { - if strings.HasPrefix(name, prefix) { - names = append(names, name) - } - } - return names + return strs } diff --git a/vendor/mvdan.cc/sh/syntax/expand.go b/vendor/mvdan.cc/sh/syntax/expand.go index 65af1ab..9316dce 100644 --- a/vendor/mvdan.cc/sh/syntax/expand.go +++ b/vendor/mvdan.cc/sh/syntax/expand.go @@ -5,7 +5,8 @@ package syntax import "strconv" -// TODO: consider making these special syntax nodes +// TODO(v3): Consider making these special syntax nodes. +// Among other things, we can make use of Word.Lit. type brace struct { seq bool // {x..y[..incr]} instead of {x,y[,...]} @@ -265,6 +266,8 @@ func expandRec(bw *braceWord) []*Word { return []*Word{{Parts: left}} } +// TODO(v3): remove + // ExpandBraces performs Bash brace expansion on a word. For example, // passing it a single-literal word "foo{bar,baz}" will return two // single-literal words, "foobar" and "foobaz". diff --git a/vendor/mvdan.cc/sh/syntax/lexer.go b/vendor/mvdan.cc/sh/syntax/lexer.go index 2402dc0..d2c1990 100644 --- a/vendor/mvdan.cc/sh/syntax/lexer.go +++ b/vendor/mvdan.cc/sh/syntax/lexer.go @@ -60,10 +60,9 @@ func (p *Parser) rune() rune { // p.r instead of b so that newline // character positions don't have col 0. p.npos.line++ - p.npos.col = 1 - } else { - p.npos.col += p.w + p.npos.col = 0 } + p.npos.col += p.w bquotes := 0 retry: if p.bsp < len(p.bs) { @@ -87,9 +86,8 @@ retry: p.w, p.r = 1, rune(b) return p.r } - if p.bsp+utf8.UTFMax >= len(p.bs) { - // we might need up to 4 bytes to read a full - // non-ascii rune + if !utf8.FullRune(p.bs[p.bsp:]) { + // we need more bytes to read a full non-ascii rune p.fill() } var w int @@ -122,14 +120,18 @@ func (p *Parser) fill() { p.offs += p.bsp left := len(p.bs) - p.bsp copy(p.readBuf[:left], p.readBuf[p.bsp:]) +readAgain: n, err := 0, p.readErr if err == nil { n, err = p.src.Read(p.readBuf[left:]) p.readErr = err } if n == 0 { + if err == nil { + goto readAgain + } // don't use p.errPass as we don't want to overwrite p.tok - if err != nil && err != io.EOF { + if err != io.EOF { p.err = err } if left > 0 { @@ -296,7 +298,6 @@ changedState: if !p.rxFirstPart && p.spaced { p.quote = noState goto changedState - return } p.rxFirstPart = false switch r { @@ -908,7 +909,6 @@ func (p *Parser) advanceLitHdoc(r rune) { p.newLit(r) if p.quote == hdocBodyTabs { for r == '\t' { - p.discardLit(1) r = p.rune() } } @@ -943,7 +943,6 @@ func (p *Parser) advanceLitHdoc(r rune) { if p.quote == hdocBodyTabs { for p.peekByte('\t') { p.rune() - p.discardLit(1) } } lStart = len(p.litBs) @@ -951,7 +950,7 @@ func (p *Parser) advanceLitHdoc(r rune) { } } -func (p *Parser) hdocLitWord() *Word { +func (p *Parser) quotedHdocWord() *Word { r := p.r p.newLit(r) pos := p.getPos() @@ -961,7 +960,6 @@ func (p *Parser) hdocLitWord() *Word { } if p.quote == hdocBodyTabs { for r == '\t' { - p.discardLit(1) r = p.rune() } } diff --git a/vendor/mvdan.cc/sh/syntax/nodes.go b/vendor/mvdan.cc/sh/syntax/nodes.go index 94a30a6..a4f473c 100644 --- a/vendor/mvdan.cc/sh/syntax/nodes.go +++ b/vendor/mvdan.cc/sh/syntax/nodes.go @@ -3,7 +3,10 @@ package syntax -import "fmt" +import ( + "fmt" + "strings" +) // Node represents a syntax tree node. type Node interface { @@ -243,7 +246,12 @@ func (r *Redirect) Pos() Pos { } return r.OpPos } -func (r *Redirect) End() Pos { return r.Word.End() } +func (r *Redirect) End() Pos { + if r.Hdoc != nil { + return r.Hdoc.End() + } + return r.Word.End() +} // CallExpr represents a command execution or function call, otherwise known as // a "simple command". @@ -289,6 +297,10 @@ type Block struct { func (b *Block) Pos() Pos { return b.Lbrace } func (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) } +// TODO(v3): Refactor and simplify elif/else. For example, we could likely make +// Else an *IfClause, remove ElsePos, make IfPos also do opening "else" +// positions, and join the comment slices as Last []Comment. + // IfClause represents an if statement. type IfClause struct { Elif bool // whether this IfClause begins with "elif" @@ -302,6 +314,7 @@ type IfClause struct { Else StmtList ElseComments []Comment // comments on the "else" + FiComments []Comment // comments on the "fi" } func (c *IfClause) Pos() Pos { return c.IfPos } @@ -363,14 +376,21 @@ func (*WordIter) loopNode() {} func (*CStyleLoop) loopNode() {} // WordIter represents the iteration of a variable over a series of words in a -// for clause. +// for clause. If InPos is an invalid position, the "in" token was missing, so +// the iteration is over the shell's positional parameters. type WordIter struct { Name *Lit + InPos Pos // position of "in" Items []*Word } func (w *WordIter) Pos() Pos { return w.Name.Pos() } -func (w *WordIter) End() Pos { return posMax(w.Name.End(), wordLastEnd(w.Items)) } +func (w *WordIter) End() Pos { + if len(w.Items) > 0 { + return wordLastEnd(w.Items) + } + return posMax(w.Name.End(), posAddCol(w.InPos, 2)) +} // CStyleLoop represents the behaviour of a for clause similar to the C // language. @@ -415,6 +435,28 @@ type Word struct { func (w *Word) Pos() Pos { return w.Parts[0].Pos() } func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() } +// Lit returns the word as a literal value, if the word consists of *syntax.Lit +// nodes only. An empty string is returned otherwise. Words with multiple +// literals, which can appear in some edge cases, are handled properly. +// +// For example, the word "foo" will return "foo", but the word "foo${bar}" will +// return "". +func (w *Word) Lit() string { + // In the usual case, we'll have either a single part that's a literal, + // or one of the parts being a non-literal. Using strings.Join instead + // of a strings.Builder avoids extra work in these cases, since a single + // part is a shortcut, and many parts don't incur string copies. + lits := make([]string, 0, 1) + for _, part := range w.Parts { + lit, ok := part.(*Lit) + if !ok { + return "" + } + lits = append(lits, lit.Value) + } + return strings.Join(lits, "") +} + // WordPart represents all nodes that can form part of a word. // // These are *Lit, *SglQuoted, *DblQuoted, *ParamExp, *CmdSubst, *ArithmExp, @@ -746,8 +788,12 @@ func (a *ArrayExpr) Pos() Pos { return a.Lparen } func (a *ArrayExpr) End() Pos { return posAddCol(a.Rparen, 1) } // ArrayElem represents a Bash array element. +// +// Index can be nil; for example, declare -a x=(value). +// Value can be nil; for example, declare -A x=([index]=). +// Finally, neither can be nil; for example, declare -A x=([index]=value) type ArrayElem struct { - Index ArithmExpr // [i]=, ["k"]= + Index ArithmExpr Value *Word Comments []Comment } @@ -758,7 +804,12 @@ func (a *ArrayElem) Pos() Pos { } return a.Value.Pos() } -func (a *ArrayElem) End() Pos { return a.Value.End() } +func (a *ArrayElem) End() Pos { + if a.Value != nil { + return a.Value.End() + } + return posAddCol(a.Index.Pos(), 1) +} // ExtGlob represents a Bash extended globbing expression. Note that these are // parsed independently of whether shopt has been called or not. diff --git a/vendor/mvdan.cc/sh/syntax/parser.go b/vendor/mvdan.cc/sh/syntax/parser.go index 122272c..1fca3c1 100644 --- a/vendor/mvdan.cc/sh/syntax/parser.go +++ b/vendor/mvdan.cc/sh/syntax/parser.go @@ -113,6 +113,83 @@ func (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error { return p.err } +type wrappedReader struct { + *Parser + io.Reader + + lastLine uint16 + accumulated []*Stmt + fn func([]*Stmt) bool +} + +func (w *wrappedReader) Read(p []byte) (n int, err error) { + // If we lexed a newline for the first time, we just finished a line, so + // we may need to give a callback for the edge cases below not covered + // by Parser.Stmts. + if w.r == '\n' && w.npos.line > w.lastLine { + if w.Incomplete() { + // Incomplete statement; call back to print "> ". + if !w.fn(w.accumulated) { + return 0, io.EOF + } + } else if len(w.accumulated) == 0 { + // Nothing was parsed; call back to print another "$ ". + if !w.fn(nil) { + return 0, io.EOF + } + } + w.lastLine = w.npos.line + } + return w.Reader.Read(p) +} + +// Interactive implements what is necessary to parse statements in an +// interactive shell. The parser will call the given function under two +// circumstances outlined below. +// +// If a line containing any number of statements is parsed, the function will be +// called with said statements. +// +// If a line ending in an incomplete statement is parsed, the function will be +// called with any fully parsed statents, and Parser.Incomplete will return +// true. +// +// One can imagine a simple interactive shell implementation as follows: +// +// fmt.Fprintf(os.Stdout, "$ ") +// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool { +// if parser.Incomplete() { +// fmt.Fprintf(os.Stdout, "> ") +// return true +// } +// run(stmts) +// fmt.Fprintf(os.Stdout, "$ ") +// return true +// } +// +// If the callback function returns false, parsing is stopped and the function +// is not called again. +func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error { + w := wrappedReader{Parser: p, Reader: r, fn: fn} + return p.Stmts(&w, func(stmt *Stmt) bool { + w.accumulated = append(w.accumulated, stmt) + // We finished parsing a statement and we're at a newline token, + // so we finished fully parsing a number of statements. Call + // back to run the statements and print "$ ". + if p.tok == _Newl { + if !fn(w.accumulated) { + return false + } + w.accumulated = w.accumulated[:0] + // The callback above would already print "$ ", so we + // don't want the subsequent wrappedReader.Read to cause + // another "$ " print thinking that nothing was parsed. + w.lastLine = w.npos.line + 1 + } + return true + }) +} + // Words reads and parses words one at a time, calling a function each time one // is parsed. If the function returns false, parsing is stopped and the function // is not called again. @@ -204,14 +281,17 @@ type Parser struct { hdocStop []byte parsingDoc bool - // openBquotes is how many levels of backquotes are open at the - // moment + // openStmts is how many entire statements we're currently parsing. A + // non-zero number means that we require certain tokens or words before + // reaching EOF. + openStmts int + // openBquotes is how many levels of backquotes are open at the moment. openBquotes int - // lastBquoteEsc is how many times the last backquote token was - // escaped + + // lastBquoteEsc is how many times the last backquote token was escaped lastBquoteEsc int - // buriedBquotes is like openBquotes, but saved for when the - // parser comes out of single quotes + // buriedBquotes is like openBquotes, but saved for when the parser + // comes out of single quotes buriedBquotes int rxOpenParens int @@ -234,6 +314,14 @@ type Parser struct { litBs []byte } +func (p *Parser) Incomplete() bool { + // If we're in a quote state other than noState, we're parsing a node + // such as a double-quoted string. + // If there are any open statements, we need to finish them. + // If we're constructing a literal, we need to finish it. + return p.quote != noState || p.openStmts > 0 || p.litBs != nil +} + const bufSize = 1 << 10 func (p *Parser) reset() { @@ -245,6 +333,7 @@ func (p *Parser) reset() { p.r, p.w = 0, 0 p.err, p.readErr = nil, nil p.quote, p.forbidNested = noState, false + p.openStmts = 0 p.heredocs, p.buriedHdocs = p.heredocs[:0], 0 p.parsingDoc = false p.openBquotes, p.buriedBquotes = 0, 0 @@ -428,7 +517,7 @@ func (p *Parser) doHeredocs() { p.rune() } if quoted { - r.Hdoc = p.hdocLitWord() + r.Hdoc = p.quotedHdocWord() } else { p.next() r.Hdoc = p.getWord() @@ -653,7 +742,9 @@ loop: if p.tok == _EOF { break } + p.openStmts++ s := p.getStmt(true, false, false) + p.openStmts-- if s == nil { p.invalidStmtStart() break @@ -675,7 +766,7 @@ func (p *Parser) stmtList(stops ...string) (sl StmtList) { } p.stmts(fn, stops...) split := len(p.accComs) - if p.tok == _LitWord && (p.val == "elif" || p.val == "else") { + if p.tok == _LitWord && (p.val == "elif" || p.val == "else" || p.val == "fi") { // Split the comments, so that any aligned with an opening token // get attached to it. For example: // @@ -912,6 +1003,11 @@ func (p *Parser) wordPart() WordPart { p.next() cs.StmtList = p.stmtList() + if p.tok == bckQuote && p.lastBquoteEsc < p.openBquotes-1 { + // e.g. found ` before the nested backquote \` was closed. + p.tok = _EOF + p.quoteErr(cs.Pos(), bckQuote) + } p.postNested(old) p.openBquotes-- cs.Right = p.pos @@ -1306,15 +1402,8 @@ func (p *Parser) paramExp() *ParamExp { default: pe.Exp = p.paramExpExp() } - case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn: - // if unset/null actions - switch pe.Param.Value { - case "#", "$", "?", "!": - p.curErr("$%s can never be unset or null", pe.Param.Value) - } - pe.Exp = p.paramExpExp() - case perc, dblPerc, hash, dblHash: - // pattern string manipulation + case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn, + perc, dblPerc, hash, dblHash: pe.Exp = p.paramExpExp() case _EOF: default: @@ -1389,6 +1478,9 @@ func (p *Parser) backquoteEnd() bool { // ValidName returns whether val is a valid name as per the POSIX spec. func ValidName(val string) bool { + if val == "" { + return false + } for i, r := range val { switch { case 'a' <= r && r <= 'z': @@ -1500,11 +1592,16 @@ func (p *Parser) getAssign(needEqual bool) *Assign { p.follow(left, `"[x]"`, assgn) } if ae.Value = p.getWord(); ae.Value == nil { - if p.tok == leftParen { + switch p.tok { + case leftParen: p.curErr("arrays cannot be nested") + return nil + case _Newl, rightParen, leftBrack: + // TODO: support [index]=[ + default: + p.curErr("array element values must be words") + break } - p.curErr("array element values must be words") - break } if len(p.accComs) > 0 { c := p.accComs[0] @@ -1853,6 +1950,8 @@ func (p *Parser) ifClause(s *Stmt) { curIf.ElsePos = elsePos curIf.Else = p.followStmts("else", curIf.ElsePos, "fi") } + curIf.FiComments = p.accComs + p.accComs = nil rif.FiPos = p.stmtEnd(rif, "if", "fi") curIf.FiPos = rif.FiPos s.Cmd = rif @@ -1923,7 +2022,8 @@ func (p *Parser) wordIter(ftok string, fpos Pos) *WordIter { return wi } p.got(_Newl) - if _, ok := p.gotRsrv("in"); ok { + if pos, ok := p.gotRsrv("in"); ok { + wi.InPos = pos for !stopToken(p.tok) { if w := p.getWord(); w == nil { p.curErr("word list can only contain words") @@ -2038,6 +2138,7 @@ func (p *Parser) testClause(s *Stmt) { } func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { + p.got(_Newl) var left TestExpr if pastAndOr { left = p.testExprBase(ftok, fpos) @@ -2047,6 +2148,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { if left == nil { return left } + p.got(_Newl) switch p.tok { case andAnd, orOr: case _LitWord: @@ -2071,10 +2173,12 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { Op: BinTestOperator(p.tok), X: left, } + // Save the previous quoteState, since we change it in TsReMatch. + oldQuote := p.quote + switch b.Op { case AndTest, OrTest: p.next() - p.got(_Newl) if b.Y = p.testExpr(token(b.Op), b.OpPos, false); b.Y == nil { p.followErrExp(b.OpPos, b.Op.String()) } @@ -2097,6 +2201,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr { p.next() b.Y = p.followWordTok(token(b.Op), b.OpPos) } + p.quote = oldQuote return b } @@ -2135,14 +2240,12 @@ func (p *Parser) testExprBase(ftok token, fpos Pos) TestExpr { case leftParen: pe := &ParenTest{Lparen: p.pos} p.next() - p.got(_Newl) if pe.X = p.testExpr(leftParen, pe.Lparen, false); pe.X == nil { p.followErrExp(pe.Lparen, "(") } pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) return pe default: - p.got(_Newl) return p.followWordTok(ftok, fpos) } } diff --git a/vendor/mvdan.cc/sh/syntax/printer.go b/vendor/mvdan.cc/sh/syntax/printer.go index 0ba108f..87e31c3 100644 --- a/vendor/mvdan.cc/sh/syntax/printer.go +++ b/vendor/mvdan.cc/sh/syntax/printer.go @@ -5,6 +5,8 @@ package syntax import ( "bufio" + "bytes" + "fmt" "io" "strings" "unicode" @@ -63,8 +65,9 @@ func NewPrinter(options ...func(*Printer)) *Printer { // Print "pretty-prints" the given syntax tree node to the given writer. Writes // to w are buffered. // -// The node types supported at the moment are *File, *Stmt, *Word, and any -// Command node. A trailing newline will only be printed when a *File is used. +// The node types supported at the moment are *File, *Stmt, *Word, any Command +// node, and any WordPart node. A trailing newline will only be printed when a +// *File is used. func (p *Printer) Print(w io.Writer, node Node) error { p.reset() p.bufWriter.Reset(w) @@ -74,10 +77,15 @@ func (p *Printer) Print(w io.Writer, node Node) error { p.newline(x.End()) case *Stmt: p.stmtList(StmtList{Stmts: []*Stmt{x}}) - case *Word: - p.word(x) case Command: + p.line = x.Pos().Line() p.command(x, nil) + case *Word: + p.word(x) + case WordPart: + p.wordPart(x, nil) + default: + return fmt.Errorf("unsupported node type: %T", x) } p.flushHeredocs() p.flushComments() @@ -85,8 +93,9 @@ func (p *Printer) Print(w io.Writer, node Node) error { } type bufWriter interface { - WriteByte(byte) error + Write([]byte) (int, error) WriteString(string) (int, error) + WriteByte(byte) error Reset(io.Writer) Flush() error } @@ -340,9 +349,9 @@ func (p *Printer) flushHeredocs() { !p.minify && p.tabsPrinter != nil { if r.Hdoc != nil { extra := extraIndenter{ - bufWriter: p.bufWriter, - afterNewl: true, - level: p.level + 1, + bufWriter: p.bufWriter, + baseIndent: int(p.level + 1), + firstIndent: -1, } *p.tabsPrinter = Printer{ bufWriter: &extra, @@ -407,13 +416,6 @@ func (p *Printer) semiRsrv(s string, pos Pos) { p.wantSpace = true } -func (p *Printer) comment(c Comment) { - if p.minify { - return - } - p.pendingComments = append(p.pendingComments, c) -} - func (p *Printer) flushComments() { for i, c := range p.pendingComments { p.firstLine = false @@ -445,11 +447,11 @@ func (p *Printer) flushComments() { p.pendingComments = nil } -func (p *Printer) comments(cs []Comment) { +func (p *Printer) comments(comments ...Comment) { if p.minify { return } - p.pendingComments = append(p.pendingComments, cs...) + p.pendingComments = append(p.pendingComments, comments...) } func (p *Printer) wordParts(wps []WordPart) { @@ -498,7 +500,7 @@ func (p *Printer) wordPart(wp, next WordPart) { } case *ParamExp: litCont := ";" - if nextLit, ok := next.(*Lit); ok { + if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" { litCont = nextLit.Value[:1] } name := x.Param.Value @@ -618,7 +620,7 @@ func (p *Printer) loop(loop Loop) { switch x := loop.(type) { case *WordIter: p.WriteString(x.Name.Value) - if len(x.Items) > 0 { + if x.InPos.IsValid() { p.spacedString(" in", Pos{}) p.wordJoin(x.Items) } @@ -769,13 +771,13 @@ func (p *Printer) casePatternJoin(pats []*Word) { func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { p.incLevel() for _, el := range elems { - var left *Comment + var left []Comment for _, c := range el.Comments { if c.Pos().After(el.Pos()) { - left = &c + left = append(left, c) break } - p.comment(c) + p.comments(c) } if el.Pos().Line() > p.line { p.newline(el.Pos()) @@ -786,13 +788,13 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) { if p.wroteIndex(el.Index) { p.WriteByte('=') } - p.word(el.Value) - if left != nil { - p.comment(*left) + if el.Value != nil { + p.word(el.Value) } + p.comments(left...) } if len(last) > 0 { - p.comments(last) + p.comments(last...) p.flushComments() } p.decLevel() @@ -935,14 +937,14 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.wantSpace = false p.newline(Pos{}) p.indent() - p.comments(x.Y.Comments) + p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } } else { p.spacedToken(x.Op.String(), x.OpPos) p.line = x.OpPos.Line() - p.comments(x.Y.Comments) + p.comments(x.Y.Comments...) p.newline(Pos{}) p.indent() } @@ -963,7 +965,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { p.space() } p.line = x.Body.Pos().Line() - p.comments(x.Body.Comments) + p.comments(x.Body.Comments...) p.stmt(x.Body) case *CaseClause: p.WriteString("case ") @@ -979,7 +981,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { last = ci.Comments[i:] break } - p.comment(c) + p.comments(c) } p.newlines(ci.Pos()) p.casePatternJoin(ci.Patterns) @@ -998,11 +1000,11 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { // avoid ; directly after tokens like ;; p.wroteSemi = true } - p.comments(last) + p.comments(last...) p.flushComments() p.level-- } - p.comments(x.Last) + p.comments(x.Last...) if p.swtCaseIndent { p.flushComments() p.decLevel() @@ -1059,20 +1061,30 @@ func (p *Printer) ifClause(ic *IfClause, elif bool) { p.nestedStmts(ic.Cond, Pos{}) p.semiOrNewl("then", ic.ThenPos) p.nestedStmts(ic.Then, ic.bodyEndPos()) - p.comments(ic.ElseComments) + + var left []Comment + for _, c := range ic.ElseComments { + if c.Pos().After(ic.ElsePos) { + left = append(left, c) + break + } + p.comments(c) + } if ic.FollowedByElif() { s := ic.Else.Stmts[0] - p.comments(s.Comments) + p.comments(s.Comments...) p.semiRsrv("elif", ic.ElsePos) p.ifClause(s.Cmd.(*IfClause), true) return } if !ic.Else.empty() { p.semiRsrv("else", ic.ElsePos) + p.comments(left...) p.nestedStmts(ic.Else, ic.FiPos) } else if ic.ElsePos.IsValid() { p.line = ic.ElsePos.Line() } + p.comments(ic.FiComments...) p.semiRsrv("fi", ic.FiPos) } @@ -1102,18 +1114,17 @@ func (p *Printer) stmtList(sl StmtList) { lastIndentedLine := uint(0) for i, s := range sl.Stmts { pos := s.Pos() - var endCom *Comment - var midComs []Comment + var midComs, endComs []Comment for _, c := range s.Comments { if c.End().After(s.End()) { - endCom = &c + endComs = append(endComs, c) break } if c.Pos().After(s.Pos()) { midComs = append(midComs, c) continue } - p.comment(c) + p.comments(c) } if !p.minify || p.wantSpace { p.newlines(pos) @@ -1122,12 +1133,12 @@ func (p *Printer) stmtList(sl StmtList) { if !p.hasInline(s) { inlineIndent = 0 p.commentPadding = 0 - p.comments(midComs) + p.comments(midComs...) p.stmt(s) p.wantNewline = true continue } - p.comments(midComs) + p.comments(midComs...) p.stmt(s) if s.Pos().Line() > lastIndentedLine+1 { inlineIndent = 0 @@ -1148,15 +1159,13 @@ func (p *Printer) stmtList(sl StmtList) { } lastIndentedLine = p.line } - if endCom != nil { - p.comment(*endCom) - } + p.comments(endComs...) p.wantNewline = true } if len(sl.Stmts) == 1 && !sep { p.wantNewline = false } - p.comments(sl.Last) + p.comments(sl.Last...) } type byteCounter int @@ -1171,6 +1180,9 @@ func (c *byteCounter) WriteByte(b byte) error { } return nil } +func (c *byteCounter) Write(p []byte) (int, error) { + return c.WriteString(string(p)) +} func (c *byteCounter) WriteString(s string) (int, error) { switch { case *c < 0: @@ -1184,20 +1196,41 @@ func (c *byteCounter) WriteString(s string) (int, error) { func (c *byteCounter) Reset(io.Writer) { *c = 0 } func (c *byteCounter) Flush() error { return nil } +// extraIndenter ensures that all lines in a '<<-' heredoc body have at least +// baseIndent leading tabs. Those that had more tab indentation than the first +// heredoc line will keep that relative indentation. type extraIndenter struct { bufWriter - afterNewl bool - level uint + baseIndent int + + firstIndent int + firstChange int + curLine []byte } func (e *extraIndenter) WriteByte(b byte) error { - if e.afterNewl { - for i := uint(0); i < e.level; i++ { - e.bufWriter.WriteByte('\t') + e.curLine = append(e.curLine, b) + if b != '\n' { + return nil + } + trimmed := bytes.TrimLeft(e.curLine, "\t") + lineIndent := len(e.curLine) - len(trimmed) + if e.firstIndent < 0 { + e.firstIndent = lineIndent + e.firstChange = e.baseIndent - lineIndent + lineIndent = e.baseIndent + } else { + if lineIndent < e.firstIndent { + lineIndent = e.firstIndent + } else { + lineIndent += e.firstChange } } - e.bufWriter.WriteByte(b) - e.afterNewl = b == '\n' + for i := 0; i < lineIndent; i++ { + e.bufWriter.WriteByte('\t') + } + e.bufWriter.Write(trimmed) + e.curLine = e.curLine[:0] return nil } @@ -1231,7 +1264,7 @@ func (p *Printer) nestedStmts(sl StmtList, closing Pos) { // { stmt; stmt; } p.wantNewline = true case closing.Line() > p.line && len(sl.Stmts) > 0 && - sl.end().Line() <= p.line: + sl.end().Line() < closing.Line(): // Force a newline if we find: // { stmt // } diff --git a/vendor/mvdan.cc/sh/syntax/simplify.go b/vendor/mvdan.cc/sh/syntax/simplify.go index 4b45241..a764471 100644 --- a/vendor/mvdan.cc/sh/syntax/simplify.go +++ b/vendor/mvdan.cc/sh/syntax/simplify.go @@ -16,9 +16,6 @@ import "bytes" // Remove redundant quotes [[ "$var" == str ]] // Merge negations with unary operators [[ ! -n $var ]] // Use single quotes to shorten literals "\$foo" -// -// This function is EXPERIMENTAL; it may change or disappear at any -// point until this notice is removed. func Simplify(n Node) bool { s := simplifier{} Walk(n, s.visit) diff --git a/vendor/mvdan.cc/sh/syntax/walk.go b/vendor/mvdan.cc/sh/syntax/walk.go index c1c0d27..9ff07da 100644 --- a/vendor/mvdan.cc/sh/syntax/walk.go +++ b/vendor/mvdan.cc/sh/syntax/walk.go @@ -39,7 +39,7 @@ func Walk(node Node, f func(Node) bool) { case *Comment: case *Stmt: for _, c := range x.Comments { - if c.Pos().After(x.Pos()) { + if !x.End().After(c.Pos()) { defer Walk(&c, f) break } @@ -199,7 +199,9 @@ func Walk(node Node, f func(Node) bool) { if x.Index != nil { Walk(x.Index, f) } - Walk(x.Value, f) + if x.Value != nil { + Walk(x.Value, f) + } case *ExtGlob: Walk(x.Pattern, f) case *ProcSubst: