From 6bc27baa96848cab2e4b5c4c64e4cc5235cca5eb Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Sat, 22 Apr 2017 15:46:29 -0300 Subject: [PATCH 1/3] Migrate from os/exec.Cmd to a native Go sh interpreter github.com/mvdan/sh Closes #23 --- execext/exec.go | 53 ++++++++++++++++++++++++------- execext/exec_other.go | 14 --------- execext/exec_win.go | 21 ------------- task.go | 73 ++++++++++++++++++++++++++----------------- variable_handling.go | 34 ++++++++++---------- 5 files changed, 103 insertions(+), 92 deletions(-) delete mode 100644 execext/exec_other.go delete mode 100644 execext/exec_win.go diff --git a/execext/exec.go b/execext/exec.go index 86328f55..0150e8da 100644 --- a/execext/exec.go +++ b/execext/exec.go @@ -2,22 +2,51 @@ package execext import ( "context" - "os/exec" + "errors" + "io" + "strings" + + "github.com/mvdan/sh/interp" + "github.com/mvdan/sh/syntax" ) +type RunCommandOptions struct { + Context context.Context + Command string + Dir string + Env []string + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer +} + var ( - // ShPath is path to "sh" command - ShPath string - // ShExists is true if "sh" command is available on the system - ShExists bool + // ErrNilOptions is returned when a nil options is given + ErrNilOptions = errors.New("execext: nil options given") ) -func init() { - var err error - ShPath, err = exec.LookPath("sh") - ShExists = err == nil -} +// RunCommand runs a shell command +func RunCommand(opts *RunCommandOptions) error { + if opts == nil { + return ErrNilOptions + } -func newShCommand(ctx context.Context, c string) *exec.Cmd { - return exec.CommandContext(ctx, ShPath, "-c", c) + p, err := syntax.Parse(strings.NewReader(opts.Command), "", 0) + if err != nil { + return err + } + + r := interp.Runner{ + Context: opts.Context, + File: p, + Dir: opts.Dir, + Env: opts.Env, + Stdin: opts.Stdin, + Stdout: opts.Stdout, + Stderr: opts.Stderr, + } + if err = r.Run(); err != nil { + return err + } + return nil } diff --git a/execext/exec_other.go b/execext/exec_other.go deleted file mode 100644 index 5f36715c..00000000 --- a/execext/exec_other.go +++ /dev/null @@ -1,14 +0,0 @@ -// +build !windows - -package execext - -import ( - "context" - "os/exec" -) - -// NewCommand returns a new command that runs on "sh" is available or on "cmd" -// otherwise on Windows -func NewCommand(ctx context.Context, c string) *exec.Cmd { - return newShCommand(ctx, c) -} diff --git a/execext/exec_win.go b/execext/exec_win.go deleted file mode 100644 index 64ec3194..00000000 --- a/execext/exec_win.go +++ /dev/null @@ -1,21 +0,0 @@ -// +build windows - -package execext - -import ( - "context" - "os/exec" -) - -// NewCommand returns a new command that runs on "sh" is available or on "cmd" -// otherwise on Windows -func NewCommand(ctx context.Context, c string) *exec.Cmd { - if ShExists { - return newShCommand(ctx, c) - } - return newCmdCommand(ctx, c) -} - -func newCmdCommand(ctx context.Context, c string) *exec.Cmd { - return exec.CommandContext(ctx, "cmd", "/C", c) -} diff --git a/task.go b/task.go index 5fae5c4e..a7298ded 100644 --- a/task.go +++ b/task.go @@ -1,6 +1,7 @@ package task import ( + "bytes" "context" "fmt" "log" @@ -177,38 +178,54 @@ func (t *Task) runCommand(ctx context.Context, i int) error { if err != nil { return err } - cmd := execext.NewCommand(ctx, c) - if dir != "" { - cmd.Dir = dir + + envs, err := t.getEnviron(vars) + if err != nil { + return err } - if t.Env != nil { - cmd.Env = os.Environ() - for key, value := range t.Env { - replacedValue, err := ReplaceVariables(value, vars) - if err != nil { - return err - } - replacedKey, err := ReplaceVariables(key, vars) - if err != nil { - return err - } - cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", replacedKey, replacedValue)) - } + opts := &execext.RunCommandOptions{ + Context: ctx, + Command: c, + Dir: dir, + Env: envs, + Stdin: os.Stdin, + Stderr: os.Stderr, } - cmd.Stdin = os.Stdin - cmd.Stderr = os.Stderr - if t.Set != "" { - bytes, err := cmd.Output() - if err != nil { + + if t.Set == "" { + log.Println(c) + opts.Stdout = os.Stdout + if err = execext.RunCommand(opts); err != nil { return err } - os.Setenv(t.Set, strings.TrimSpace(string(bytes))) - return nil - } - cmd.Stdout = os.Stdout - log.Println(c) - if err = cmd.Run(); err != nil { - return err + } else { + buff := bytes.NewBuffer(nil) + opts.Stdout = buff + if err = execext.RunCommand(opts); err != nil { + return err + } + os.Setenv(t.Set, strings.TrimSpace(buff.String())) } return nil } + +func (t *Task) getEnviron(vars map[string]string) ([]string, error) { + if t.Env == nil { + return nil, nil + } + + envs := os.Environ() + + for k, v := range t.Env { + replacedValue, err := ReplaceVariables(v, vars) + if err != nil { + return nil, err + } + replacedKey, err := ReplaceVariables(k, vars) + if err != nil { + return nil, err + } + envs = append(envs, fmt.Sprintf("%s=%s", replacedKey, replacedValue)) + } + return envs, nil +} diff --git a/variable_handling.go b/variable_handling.go index 1f4829bc..22f8f45d 100644 --- a/variable_handling.go +++ b/variable_handling.go @@ -2,7 +2,6 @@ package task import ( "bytes" - "context" "encoding/json" "errors" "io/ioutil" @@ -35,19 +34,25 @@ func handleDynamicVariableContent(value string) (string, error) { if result, ok := varCmds[value]; ok { return result, nil } - cmd := execext.NewCommand(context.Background(), value[1:]) - cmd.Stderr = os.Stderr - b, err := cmd.Output() - if err != nil { + + buff := bytes.NewBuffer(nil) + + opts := &execext.RunCommandOptions{ + Command: strings.TrimPrefix(value, "$"), + Stdout: buff, + Stderr: os.Stderr, + } + if err := execext.RunCommand(opts); err != nil { return "", err } - if b[len(b)-1] == '\n' { - b = b[:len(b)-1] - } - if bytes.ContainsRune(b, '\n') { + + result := buff.String() + result = strings.TrimSuffix(result, "\n") + if strings.ContainsRune(result, '\n') { return "", ErrMultilineResultCmd } - result := strings.TrimSpace(string(b)) + + result = strings.TrimSpace(result) varCmds[value] = result return result, nil } @@ -84,17 +89,12 @@ func init() { taskFuncs := template.FuncMap{ "OS": func() string { return runtime.GOOS }, "ARCH": func() string { return runtime.GOARCH }, - "IsSH": func() bool { return execext.ShExists }, + // historical reasons + "IsSH": func() bool { return true }, "FromSlash": func(path string) string { - if execext.ShExists { - return path - } return filepath.FromSlash(path) }, "ToSlash": func(path string) string { - if execext.ShExists { - return path - } return filepath.ToSlash(path) }, } From 25134279f4c86c12d981452cc45ce7c7e35cce0a Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Mon, 24 Apr 2017 09:47:10 -0300 Subject: [PATCH 2/3] Vendor github.com/mvdan/sh --- vendor/github.com/mvdan/sh/LICENSE | 27 + vendor/github.com/mvdan/sh/interp/arith.go | 171 ++ vendor/github.com/mvdan/sh/interp/builtin.go | 179 ++ vendor/github.com/mvdan/sh/interp/doc.go | 9 + vendor/github.com/mvdan/sh/interp/interp.go | 653 ++++++ vendor/github.com/mvdan/sh/interp/param.go | 198 ++ vendor/github.com/mvdan/sh/interp/test.go | 160 ++ .../github.com/mvdan/sh/syntax/canonical.sh | 37 + vendor/github.com/mvdan/sh/syntax/doc.go | 6 + vendor/github.com/mvdan/sh/syntax/lexer.go | 1005 ++++++++++ vendor/github.com/mvdan/sh/syntax/nodes.go | 731 +++++++ vendor/github.com/mvdan/sh/syntax/parser.go | 1768 +++++++++++++++++ vendor/github.com/mvdan/sh/syntax/printer.go | 904 +++++++++ .../mvdan/sh/syntax/token_string.go | 16 + vendor/github.com/mvdan/sh/syntax/tokens.go | 340 ++++ vendor/github.com/mvdan/sh/syntax/walk.go | 184 ++ vendor/vendor.json | 12 + 17 files changed, 6400 insertions(+) create mode 100644 vendor/github.com/mvdan/sh/LICENSE create mode 100644 vendor/github.com/mvdan/sh/interp/arith.go create mode 100644 vendor/github.com/mvdan/sh/interp/builtin.go create mode 100644 vendor/github.com/mvdan/sh/interp/doc.go create mode 100644 vendor/github.com/mvdan/sh/interp/interp.go create mode 100644 vendor/github.com/mvdan/sh/interp/param.go create mode 100644 vendor/github.com/mvdan/sh/interp/test.go create mode 100644 vendor/github.com/mvdan/sh/syntax/canonical.sh create mode 100644 vendor/github.com/mvdan/sh/syntax/doc.go create mode 100644 vendor/github.com/mvdan/sh/syntax/lexer.go create mode 100644 vendor/github.com/mvdan/sh/syntax/nodes.go create mode 100644 vendor/github.com/mvdan/sh/syntax/parser.go create mode 100644 vendor/github.com/mvdan/sh/syntax/printer.go create mode 100644 vendor/github.com/mvdan/sh/syntax/token_string.go create mode 100644 vendor/github.com/mvdan/sh/syntax/tokens.go create mode 100644 vendor/github.com/mvdan/sh/syntax/walk.go diff --git a/vendor/github.com/mvdan/sh/LICENSE b/vendor/github.com/mvdan/sh/LICENSE new file mode 100644 index 00000000..2a5268e5 --- /dev/null +++ b/vendor/github.com/mvdan/sh/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2016, Daniel Martí. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/mvdan/sh/interp/arith.go b/vendor/github.com/mvdan/sh/interp/arith.go new file mode 100644 index 00000000..36c1d127 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/arith.go @@ -0,0 +1,171 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package interp + +import ( + "strconv" + + "github.com/mvdan/sh/syntax" +) + +func (r *Runner) arithm(expr syntax.ArithmExpr) int { + switch x := expr.(type) { + case *syntax.Word: + str := r.loneWord(x) + // recursively fetch vars + for { + val := r.getVar(str) + if val == "" { + break + } + str = val + } + // default to 0 + return atoi(str) + case *syntax.ParenArithm: + return r.arithm(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)) + val := old + if x.Op == syntax.Inc { + val++ + } else { + val-- + } + r.setVar(name, strconv.Itoa(val)) + if x.Post { + return old + } + return val + } + val := r.arithm(x.X) + switch x.Op { + case syntax.Not: + return oneIf(val == 0) + case syntax.Plus: + return val + default: // syntax.Minus + return -val + } + case *syntax.BinaryArithm: + switch x.Op { + case syntax.Assgn, syntax.AddAssgn, syntax.SubAssgn, + syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn, + syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn, + syntax.ShlAssgn, syntax.ShrAssgn: + return r.assgnArit(x) + case syntax.Quest: // Colon can't happen here + cond := r.arithm(x.X) + b2 := x.Y.(*syntax.BinaryArithm) // must have Op==Colon + if cond == 1 { + return r.arithm(b2.X) + } + return r.arithm(b2.Y) + } + return binArit(x.Op, r.arithm(x.X), r.arithm(x.Y)) + default: + r.errf("unexpected arithm expr: %T", x) + 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) assgnArit(b *syntax.BinaryArithm) int { + name := b.X.(*syntax.Word).Parts[0].(*syntax.Lit).Value + val := atoi(r.getVar(name)) + arg := r.arithm(b.Y) + switch b.Op { + case syntax.Assgn: + val = arg + case syntax.AddAssgn: + val += arg + case syntax.SubAssgn: + val -= arg + case syntax.MulAssgn: + val *= arg + case syntax.QuoAssgn: + val /= arg + case syntax.RemAssgn: + val %= arg + case syntax.AndAssgn: + val &= arg + case syntax.OrAssgn: + val |= arg + case syntax.XorAssgn: + val ^= arg + case syntax.ShlAssgn: + val <<= uint(arg) + default: // syntax.ShrAssgn + val >>= uint(arg) + } + r.setVar(name, strconv.Itoa(val)) + return val +} + +func intPow(a, b int) int { + p := 1 + for b > 0 { + if b&1 != 0 { + p *= a + } + b >>= 1 + a *= a + } + return p +} + +func binArit(op syntax.BinAritOperator, x, y int) int { + switch op { + case syntax.Add: + return x + y + case syntax.Sub: + return x - y + case syntax.Mul: + return x * y + case syntax.Quo: + return x / y + case syntax.Rem: + return x % y + case syntax.Pow: + return intPow(x, y) + case syntax.Eql: + return oneIf(x == y) + case syntax.Gtr: + return oneIf(x > y) + case syntax.Lss: + return oneIf(x < y) + case syntax.Neq: + return oneIf(x != y) + case syntax.Leq: + return oneIf(x <= y) + case syntax.Geq: + return oneIf(x >= y) + case syntax.And: + return x & y + case syntax.Or: + return x | y + case syntax.Xor: + return x ^ y + case syntax.Shr: + return x >> uint(y) + case syntax.Shl: + return x << uint(y) + case syntax.AndArit: + return oneIf(x != 0 && y != 0) + case syntax.OrArit: + return oneIf(x != 0 || y != 0) + default: // syntax.Comma + // x is executed but its result discarded + return y + } +} diff --git a/vendor/github.com/mvdan/sh/interp/builtin.go b/vendor/github.com/mvdan/sh/interp/builtin.go new file mode 100644 index 00000000..1fa235b5 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/builtin.go @@ -0,0 +1,179 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package interp + +import ( + "os" + "path/filepath" + "strconv" + + "github.com/mvdan/sh/syntax" +) + +func (r *Runner) builtin(pos syntax.Pos, name string, args []string) bool { + exit := 0 + switch name { + case "true", ":": + case "false": + exit = 1 + case "exit": + switch len(args) { + case 0: + r.lastExit() + case 1: + if n, err := strconv.Atoi(args[0]); err != nil { + r.runErr(pos, "invalid exit code: %q", args[0]) + } else { + exit = n + r.err = ExitCode(n) + } + default: + r.runErr(pos, "exit cannot take multiple arguments") + } + case "set": + r.args = args + case "shift": + n := 1 + switch len(args) { + case 0: + case 1: + if n2, err := strconv.Atoi(args[0]); err == nil { + n = n2 + break + } + fallthrough + default: + r.errf("usage: shift [n]\n") + exit = 2 + } + if len(r.args) < n { + n = len(r.args) + } + r.args = r.args[n:] + case "unset": + for _, arg := range args { + r.delVar(arg) + } + case "echo": + newline := true + opts: + for len(args) > 0 { + switch args[0] { + case "-n": + newline = false + case "-e", "-E": + // TODO: what should be our default? + // exactly what is the difference in + // what we write? + default: + break opts + } + args = args[1:] + } + for i, arg := range args { + if i > 0 { + r.outf(" ") + } + r.outf("%s", arg) + } + if newline { + r.outf("\n") + } + case "printf": + if len(args) == 0 { + r.errf("usage: printf format [arguments]\n") + exit = 2 + break + } + var a []interface{} + for _, arg := range args[1:] { + a = append(a, arg) + } + r.outf(args[0], a...) + case "break": + if !r.inLoop { + r.errf("break is only useful in a loop") + 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") + exit = 2 + } + case "continue": + if !r.inLoop { + r.errf("continue is only useful in a loop") + break + } + switch len(args) { + case 0: + r.contnEnclosing = 1 + case 1: + if n, err := strconv.Atoi(args[0]); err == nil { + r.contnEnclosing = n + break + } + fallthrough + default: + r.errf("usage: continue [n]\n") + exit = 2 + } + case "pwd": + r.outf("%s\n", r.getVar("PWD")) + case "cd": + if len(args) > 1 { + r.errf("usage: cd [dir]\n") + exit = 2 + break + } + var dir string + if len(args) == 0 { + dir = r.getVar("HOME") + } else { + dir = args[0] + } + if !filepath.IsAbs(dir) { + dir = filepath.Join(r.Dir, dir) + } + _, err := os.Stat(dir) + if err != nil { + exit = 1 + break + } + r.Dir = dir + case "wait": + if len(args) > 0 { + r.errf("wait with args not handled yet") + break + } + r.bgShells.Wait() + case "builtin": + if len(args) < 1 { + break + } + // TODO: pos + if !r.builtin(0, args[0], args[1:]) { + exit = 1 + } + case "trap", "type", "source", "command", "pushd", "popd", + "umask", "alias", "unalias", "fg", "bg", "getopts": + r.errf("unhandled builtin: %s", name) + // TODO(mvdan): we rely on the binary versions of these, we + // should eventually implement them as builtins like Bash for + // portability + // case "[", "test": + default: + return false + } + r.exit = exit + return true +} diff --git a/vendor/github.com/mvdan/sh/interp/doc.go b/vendor/github.com/mvdan/sh/interp/doc.go new file mode 100644 index 00000000..04f77d75 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/doc.go @@ -0,0 +1,9 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +// Package interp implements an interpreter that executes shell +// programs. It aims to support POSIX. +// +// This package is a work in progress and EXPERIMENTAL; its API is not +// subject to the 1.x backwards compatibility guarantee. +package interp diff --git a/vendor/github.com/mvdan/sh/interp/interp.go b/vendor/github.com/mvdan/sh/interp/interp.go new file mode 100644 index 00000000..d2472aa9 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/interp.go @@ -0,0 +1,653 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package interp + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "os/user" + "path" + "strconv" + "strings" + "sync" + "syscall" + + "github.com/mvdan/sh/syntax" +) + +// A Runner interprets shell programs. It cannot be reused once a +// program has been interpreted. +// +// Note that writes to Stdout and Stderr may not be sequential. If +// you plan on using an io.Writer implementation that isn't safe for +// concurrent use, consider a workaround like hiding writes behind a +// mutex. +type Runner struct { + // TODO: syntax.Node instead of *syntax.File? + File *syntax.File + + // Env specifies the environment of the interpreter. + // If Env is nil, Run uses the current process's environment. + Env []string + + // envMap is just Env as a map, to simplify and speed up its use + envMap map[string]string + + // Dir specifies the working directory of the command. If Dir is + // the empty string, Run runs the command in the calling + // process's current directory. + Dir string + + // Separate maps, note that bash allows a name to be both a var + // and a func simultaneously + vars map[string]varValue + funcs map[string]*syntax.Stmt + + // like vars, but local to a cmd i.e. "foo=bar prog args..." + cmdVars map[string]varValue + + // Current arguments, if executing a function + args []string + + // >0 to break or continue out of N enclosing loops + breakEnclosing, contnEnclosing int + + inLoop bool + + err error // current fatal error + exit int // current (last) exit code + + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + bgShells sync.WaitGroup + + // Context can be used to cancel the interpreter before it finishes + Context context.Context +} + +// varValue can hold a string, an indexed array ([]string) or an +// associative array (map[string]string) +// TODO: implement associative arrays +type varValue interface{} + +func varStr(v varValue) string { + switch x := v.(type) { + case string: + return x + case []string: + if len(x) > 0 { + return x[0] + } + } + return "" +} + +func (r *Runner) varInd(v varValue, e syntax.ArithmExpr) string { + switch x := v.(type) { + case string: + i := r.arithm(e) + if i == 0 { + return x + } + case []string: + // TODO: @ between double quotes + if w, ok := e.(*syntax.Word); ok { + if lit, ok := w.Parts[0].(*syntax.Lit); ok { + switch lit.Value { + case "@", "*": + return strings.Join(x, " ") + } + } + } + i := r.arithm(e) + if len(x) > 0 { + return x[i] + } + } + return "" +} + +type ExitCode uint8 + +func (e ExitCode) Error() string { return fmt.Sprintf("exit status %d", e) } + +type RunError struct { + syntax.Position + Text string +} + +func (e RunError) Error() string { + return fmt.Sprintf("%s: %s", e.Position.String(), e.Text) +} + +func (r *Runner) runErr(pos syntax.Pos, format string, a ...interface{}) { + if r.err == nil { + r.err = RunError{ + Position: r.File.Position(pos), + Text: fmt.Sprintf(format, a...), + } + } +} + +func (r *Runner) lastExit() { + if r.err == nil { + r.err = ExitCode(r.exit) + } +} + +func (r *Runner) setVar(name string, val varValue) { + if r.vars == nil { + r.vars = make(map[string]varValue, 4) + } + r.vars[name] = val +} + +func (r *Runner) lookupVar(name string) (varValue, bool) { + switch name { + case "PWD": + return r.Dir, true + case "HOME": + u, _ := user.Current() + return u.HomeDir, true + } + if val, e := r.cmdVars[name]; e { + return val, true + } + if val, e := r.vars[name]; e { + return val, true + } + str, e := r.envMap[name] + return str, e +} + +func (r *Runner) getVar(name string) string { + val, _ := r.lookupVar(name) + return varStr(val) +} + +func (r *Runner) delVar(name string) { + delete(r.vars, name) + delete(r.envMap, name) +} + +func (r *Runner) setFunc(name string, body *syntax.Stmt) { + if r.funcs == nil { + r.funcs = make(map[string]*syntax.Stmt, 4) + } + r.funcs[name] = body +} + +// Run starts the interpreter and returns any error. +func (r *Runner) Run() error { + if r.Context == nil { + r.Context = context.Background() + } + if r.Env == nil { + r.Env = os.Environ() + } + r.envMap = make(map[string]string, len(r.Env)) + for _, kv := range r.Env { + i := strings.IndexByte(kv, '=') + if i < 0 { + return fmt.Errorf("env not in the form key=value: %q", kv) + } + name, val := kv[:i], kv[i+1:] + r.envMap[name] = val + } + if r.Dir == "" { + dir, err := os.Getwd() + if err != nil { + return fmt.Errorf("could not get current dir: %v", err) + } + r.Dir = dir + } + r.stmts(r.File.Stmts) + r.lastExit() + if r.err == ExitCode(0) { + r.err = nil + } + return r.err +} + +func (r *Runner) outf(format string, a ...interface{}) { + fmt.Fprintf(r.Stdout, format, a...) +} + +func (r *Runner) errf(format string, a ...interface{}) { + fmt.Fprintf(r.Stderr, format, a...) +} + +func (r *Runner) fields(words []*syntax.Word) []string { + fields := make([]string, 0, len(words)) + for _, word := range words { + fields = append(fields, r.wordParts(word.Parts, false)...) + } + return fields +} + +func (r *Runner) loneWord(word *syntax.Word) string { + return strings.Join(r.wordParts(word.Parts, false), "") +} + +func (r *Runner) stop() bool { + if r.err != nil { + return true + } + if err := r.Context.Err(); err != nil { + r.err = err + return true + } + return false +} + +func (r *Runner) stmt(st *syntax.Stmt) { + if r.stop() { + return + } + if st.Background { + r.bgShells.Add(1) + r2 := *r + r2.bgShells = sync.WaitGroup{} + go func() { + r2.stmtSync(st) + r.bgShells.Done() + }() + } else { + r.stmtSync(st) + } +} + +func (r *Runner) assignValue(word *syntax.Word) varValue { + if word == nil { + return nil + } + ae, ok := word.Parts[0].(*syntax.ArrayExpr) + if !ok { + return r.loneWord(word) + } + strs := make([]string, len(ae.List)) + for i, w := range ae.List { + strs[i] = r.loneWord(w) + } + return strs +} + +func (r *Runner) stmtSync(st *syntax.Stmt) { + oldVars := r.cmdVars + for _, as := range st.Assigns { + name := as.Name.Value + val := r.assignValue(as.Value) + if st.Cmd == nil { + r.setVar(name, val) + continue + } + if r.cmdVars == nil { + r.cmdVars = make(map[string]varValue, len(st.Assigns)) + } + r.cmdVars[name] = val + } + oldIn, oldOut, oldErr := r.Stdin, r.Stdout, r.Stderr + for _, rd := range st.Redirs { + cls, err := r.redir(rd) + if err != nil { + r.exit = 1 + return + } + if cls != nil { + defer cls.Close() + } + } + if st.Cmd == nil { + r.exit = 0 + } else { + r.cmd(st.Cmd) + } + if st.Negated { + r.exit = oneIf(r.exit == 0) + } + r.cmdVars = oldVars + r.Stdin, r.Stdout, r.Stderr = oldIn, oldOut, oldErr +} + +func oneIf(b bool) int { + if b { + return 1 + } + return 0 +} + +func (r *Runner) cmd(cm syntax.Command) { + if r.stop() { + return + } + switch x := cm.(type) { + case *syntax.Block: + r.stmts(x.Stmts) + case *syntax.Subshell: + r2 := *r + r2.stmts(x.Stmts) + r.exit = r2.exit + case *syntax.CallExpr: + fields := r.fields(x.Args) + r.call(x.Args[0].Pos(), fields[0], fields[1:]) + case *syntax.BinaryCmd: + switch x.Op { + case syntax.AndStmt: + r.stmt(x.X) + if r.exit == 0 { + r.stmt(x.Y) + } + case syntax.OrStmt: + r.stmt(x.X) + if r.exit != 0 { + r.stmt(x.Y) + } + case syntax.Pipe, syntax.PipeAll: + pr, pw := io.Pipe() + r2 := *r + r2.Stdin = r.Stdin + r2.Stdout = pw + if x.Op == syntax.PipeAll { + r2.Stderr = pw + } else { + r2.Stderr = r.Stderr + } + r.Stdin = pr + go func() { + r2.stmt(x.X) + pw.Close() + }() + r.stmt(x.Y) + pr.Close() + } + case *syntax.IfClause: + r.stmts(x.CondStmts) + if r.exit == 0 { + r.stmts(x.ThenStmts) + return + } + for _, el := range x.Elifs { + r.stmts(el.CondStmts) + if r.exit == 0 { + r.stmts(el.ThenStmts) + return + } + } + r.stmts(x.ElseStmts) + if len(x.Elifs)+len(x.ElseStmts) == 0 { + r.exit = 0 + } + case *syntax.WhileClause: + for r.err == nil { + r.stmts(x.CondStmts) + if r.exit != 0 { + r.exit = 0 + break + } + if r.loopStmtsBroken(x.DoStmts) { + break + } + } + case *syntax.UntilClause: + for r.err == nil { + r.stmts(x.CondStmts) + if r.exit == 0 { + break + } + r.exit = 0 + if r.loopStmtsBroken(x.DoStmts) { + break + } + } + case *syntax.ForClause: + switch y := x.Loop.(type) { + case *syntax.WordIter: + name := y.Name.Value + for _, field := range r.fields(y.List) { + r.setVar(name, field) + if r.loopStmtsBroken(x.DoStmts) { + break + } + } + case *syntax.CStyleLoop: + r.arithm(y.Init) + for r.arithm(y.Cond) != 0 { + if r.loopStmtsBroken(x.DoStmts) { + break + } + r.arithm(y.Post) + } + } + case *syntax.FuncDecl: + r.setFunc(x.Name.Value, x.Body) + case *syntax.ArithmCmd: + if r.arithm(x.X) == 0 { + r.exit = 1 + } + case *syntax.LetClause: + var val int + for _, expr := range x.Exprs { + val = r.arithm(expr) + } + if val == 0 { + r.exit = 1 + } + case *syntax.CaseClause: + str := r.loneWord(x.Word) + for _, pl := range x.List { + for _, word := range pl.Patterns { + pat := r.loneWord(word) + // TODO: error? + matched, _ := path.Match(pat, str) + if matched { + r.stmts(pl.Stmts) + return + } + } + } + case *syntax.TestClause: + if r.bashTest(x.X) == "" && r.exit == 0 { + r.exit = 1 + } + default: + r.errf("unhandled command node: %T", x) + } +} + +func (r *Runner) stmts(stmts []*syntax.Stmt) { + for _, stmt := range stmts { + r.stmt(stmt) + } +} + +func (r *Runner) redir(rd *syntax.Redirect) (io.Closer, error) { + if rd.Hdoc != nil { + hdoc := r.loneWord(rd.Hdoc) + r.Stdin = strings.NewReader(hdoc) + return nil, nil + } + orig := &r.Stdout + if rd.N != nil { + switch rd.N.Value { + case "1": + case "2": + orig = &r.Stderr + } + } + arg := r.loneWord(rd.Word) + switch rd.Op { + case syntax.WordHdoc: + r.Stdin = strings.NewReader(arg + "\n") + return nil, nil + case syntax.DplOut: + switch arg { + case "1": + *orig = r.Stdout + case "2": + *orig = r.Stderr + } + return nil, nil + case syntax.DplIn: + r.errf("unhandled redirect op: %v", rd.Op) + } + mode := os.O_RDONLY + switch rd.Op { + case syntax.AppOut, syntax.AppAll: + mode = os.O_RDWR | os.O_CREATE | os.O_APPEND + case syntax.RdrOut, syntax.RdrAll: + mode = os.O_RDWR | os.O_CREATE | os.O_TRUNC + } + f, err := os.OpenFile(arg, mode, 0644) + if err != nil { + // TODO: print to stderr? + return nil, err + } + switch rd.Op { + case syntax.RdrIn: + r.Stdin = f + case syntax.RdrOut, syntax.AppOut: + *orig = f + case syntax.RdrAll, syntax.AppAll: + r.Stdout = f + r.Stderr = f + default: + r.errf("unhandled redirect op: %v", rd.Op) + } + return f, nil +} + +func (r *Runner) loopStmtsBroken(stmts []*syntax.Stmt) bool { + r.inLoop = true + defer func() { r.inLoop = false }() + for _, stmt := range stmts { + r.stmt(stmt) + if r.contnEnclosing > 0 { + r.contnEnclosing-- + return r.contnEnclosing > 0 + } + if r.breakEnclosing > 0 { + r.breakEnclosing-- + return true + } + } + return false +} + +func (r *Runner) wordParts(wps []syntax.WordPart, quoted bool) []string { + var parts []string + var curBuf bytes.Buffer + flush := func() { + if curBuf.Len() == 0 { + return + } + parts = append(parts, curBuf.String()) + curBuf.Reset() + } + splitAdd := func(val string) { + // TODO: use IFS + for i, field := range strings.Fields(val) { + if i > 0 { + flush() + } + curBuf.WriteString(field) + } + } + for _, wp := range wps { + switch x := wp.(type) { + case *syntax.Lit: + curBuf.WriteString(x.Value) + case *syntax.SglQuoted: + curBuf.WriteString(x.Value) + case *syntax.DblQuoted: + // TODO: @ between double quotes but not alone + if len(x.Parts) == 1 { + pe, ok := x.Parts[0].(*syntax.ParamExp) + if ok && pe.Param.Value == "@" { + for i, arg := range r.args { + if i > 0 { + flush() + } + curBuf.WriteString(arg) + } + continue + } + } + for _, str := range r.wordParts(x.Parts, true) { + curBuf.WriteString(str) + } + case *syntax.ParamExp: + val := r.paramExp(x) + if quoted { + curBuf.WriteString(val) + } else { + splitAdd(val) + } + case *syntax.CmdSubst: + r2 := *r + var outBuf bytes.Buffer + r2.Stdout = &outBuf + r2.stmts(x.Stmts) + val := strings.TrimRight(outBuf.String(), "\n") + if quoted { + curBuf.WriteString(val) + } else { + splitAdd(val) + } + case *syntax.ArithmExp: + curBuf.WriteString(strconv.Itoa(r.arithm(x.X))) + default: + r.errf("unhandled word part: %T", x) + } + } + flush() + return parts +} + +func (r *Runner) call(pos syntax.Pos, name string, args []string) { + if body := r.funcs[name]; body != nil { + // stack them to support nested func calls + oldArgs := r.args + r.args = args + r.stmt(body) + r.args = oldArgs + return + } + if r.builtin(pos, name, args) { + return + } + cmd := exec.CommandContext(r.Context, name, args...) + cmd.Env = r.Env + for name, val := range r.cmdVars { + cmd.Env = append(cmd.Env, name+"="+varStr(val)) + } + cmd.Dir = r.Dir + cmd.Stdin = r.Stdin + cmd.Stdout = r.Stdout + cmd.Stderr = r.Stderr + err := cmd.Run() + switch x := err.(type) { + case *exec.ExitError: + // started, but errored - default to 1 if OS + // doesn't have exit statuses + if status, ok := x.Sys().(syscall.WaitStatus); ok { + r.exit = status.ExitStatus() + } else { + r.exit = 1 + } + case *exec.Error: + // did not start + // TODO: can this be anything other than + // "command not found"? + r.exit = 127 + // TODO: print something? + default: + r.exit = 0 + } +} diff --git a/vendor/github.com/mvdan/sh/interp/param.go b/vendor/github.com/mvdan/sh/interp/param.go new file mode 100644 index 00000000..a15d1f23 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/param.go @@ -0,0 +1,198 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package interp + +import ( + "path" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + "github.com/mvdan/sh/syntax" +) + +func (r *Runner) paramExp(pe *syntax.ParamExp) string { + name := pe.Param.Value + var val varValue + set := false + switch name { + case "#": + val = strconv.Itoa(len(r.args)) + case "*", "@": + val = strings.Join(r.args, " ") + case "?": + val = strconv.Itoa(r.exit) + default: + if n, err := strconv.Atoi(name); err == nil { + if i := n - 1; i < len(r.args) { + val, set = r.args[i], true + } + } else { + val, set = r.lookupVar(name) + } + } + str := varStr(val) + if pe.Ind != nil { + str = r.varInd(val, pe.Ind.Expr) + } + switch { + case pe.Length: + str = strconv.Itoa(utf8.RuneCountInString(str)) + case pe.Excl: + val, set = r.lookupVar(str) + str = varStr(val) + } + slicePos := func(expr syntax.ArithmExpr) int { + p := r.arithm(expr) + if p < 0 { + p = len(str) + p + if p < 0 { + p = len(str) + } + } else if p > len(str) { + p = len(str) + } + return p + } + if pe.Slice != nil { + if pe.Slice.Offset != nil { + offset := slicePos(pe.Slice.Offset) + str = str[offset:] + } + if pe.Slice.Length != nil { + length := slicePos(pe.Slice.Length) + str = str[:length] + } + } + if pe.Repl != nil { + orig := r.loneWord(pe.Repl.Orig) + with := r.loneWord(pe.Repl.With) + n := 1 + if pe.Repl.All { + n = -1 + } + str = strings.Replace(str, orig, with, n) + } + if pe.Exp != nil { + arg := r.loneWord(pe.Exp.Word) + switch pe.Exp.Op { + case syntax.SubstColPlus: + if str == "" { + break + } + fallthrough + case syntax.SubstPlus: + if set { + str = arg + } + case syntax.SubstMinus: + if set { + break + } + fallthrough + case syntax.SubstColMinus: + if str == "" { + str = arg + } + case syntax.SubstQuest: + if set { + break + } + fallthrough + case syntax.SubstColQuest: + if str == "" { + r.errf("%s", arg) + r.exit = 1 + r.lastExit() + } + case syntax.SubstAssgn: + if set { + break + } + fallthrough + case syntax.SubstColAssgn: + if str == "" { + r.setVar(name, arg) + str = arg + } + case syntax.RemSmallPrefix: + str = removePattern(str, arg, false, false) + case syntax.RemLargePrefix: + str = removePattern(str, arg, false, true) + case syntax.RemSmallSuffix: + str = removePattern(str, arg, true, false) + case syntax.RemLargeSuffix: + str = removePattern(str, arg, true, true) + case syntax.UpperFirst: + rs := []rune(str) + if len(rs) > 0 { + rs[0] = unicode.ToUpper(rs[0]) + } + str = string(rs) + case syntax.UpperAll: + str = strings.ToUpper(str) + case syntax.LowerFirst: + rs := []rune(str) + if len(rs) > 0 { + rs[0] = unicode.ToLower(rs[0]) + } + str = string(rs) + case syntax.LowerAll: + str = strings.ToLower(str) + default: // syntax.OtherParamOps + switch arg { + case "Q": + str = strconv.Quote(str) + case "E": + tail := str + var rns []rune + for tail != "" { + var rn rune + rn, _, tail, _ = strconv.UnquoteChar(tail, 0) + rns = append(rns, rn) + } + str = string(rns) + case "P", "A", "a": + r.errf("unhandled @%s param expansion", arg) + default: + r.errf("unexpected @%s param expansion", arg) + } + } + } + return str +} + +func removePattern(str, pattern string, fromEnd, longest bool) string { + // TODO: really slow to not re-implement path.Match. + last := str + s := str + i := len(str) + if fromEnd { + i = 0 + } + for { + if m, _ := path.Match(pattern, s); m { + last = str[i:] + if fromEnd { + last = str[:i] + } + if longest { + return last + } + } + if fromEnd { + if i++; i >= len(str) { + break + } + s = str[i:] + } else { + if i--; i < 1 { + break + } + s = str[:i] + } + } + return last +} diff --git a/vendor/github.com/mvdan/sh/interp/test.go b/vendor/github.com/mvdan/sh/interp/test.go new file mode 100644 index 00000000..1d1fd403 --- /dev/null +++ b/vendor/github.com/mvdan/sh/interp/test.go @@ -0,0 +1,160 @@ +// Copyright (c) 2017, Daniel Martí +// See LICENSE for licensing information + +package interp + +import ( + "os" + "os/exec" + "path" + "path/filepath" + "regexp" + + "github.com/mvdan/sh/syntax" +) + +// non-empty string is true, empty string is false +func (r *Runner) bashTest(expr syntax.TestExpr) string { + switch x := expr.(type) { + case *syntax.Word: + return r.loneWord(x) + case *syntax.ParenTest: + return r.bashTest(x.X) + case *syntax.BinaryTest: + if r.binTest(x.Op, r.bashTest(x.X), r.bashTest(x.Y)) { + return "1" + } + return "" + case *syntax.UnaryTest: + if r.unTest(x.Op, r.bashTest(x.X)) { + return "1" + } + return "" + } + return "" +} + +func (r *Runner) binTest(op syntax.BinTestOperator, x, y string) bool { + switch op { + case syntax.TsReMatch: + re, err := regexp.Compile(y) + if err != nil { + r.exit = 2 + return false + } + return re.MatchString(x) + case syntax.TsNewer: + i1, i2 := stat(x), stat(y) + if i1 == nil || i2 == nil { + return false + } + return i1.ModTime().After(i2.ModTime()) + case syntax.TsOlder: + i1, i2 := stat(x), stat(y) + if i1 == nil || i2 == nil { + return false + } + return i1.ModTime().Before(i2.ModTime()) + case syntax.TsDevIno: + i1, i2 := stat(x), stat(y) + return os.SameFile(i1, i2) + case syntax.TsEql: + return atoi(x) == atoi(y) + case syntax.TsNeq: + return atoi(x) != atoi(y) + case syntax.TsLeq: + return atoi(x) <= atoi(y) + case syntax.TsGeq: + return atoi(x) >= atoi(y) + case syntax.TsLss: + return atoi(x) < atoi(y) + case syntax.TsGtr: + return atoi(x) > atoi(y) + case syntax.AndTest: + return x != "" && y != "" + case syntax.OrTest: + return x != "" || y != "" + case syntax.TsEqual: + m, _ := path.Match(y, x) + return m + case syntax.TsNequal: + m, _ := path.Match(y, x) + return !m + case syntax.TsBefore: + return x < y + default: // syntax.TsAfter + return x > y + } +} + +func stat(name string) os.FileInfo { + info, _ := os.Stat(name) + return info +} + +func statMode(name string, mode os.FileMode) bool { + info := stat(name) + return info != nil && info.Mode()&mode != 0 +} + +func (r *Runner) unTest(op syntax.UnTestOperator, x string) bool { + switch op { + case syntax.TsExists: + return stat(x) != nil + case syntax.TsRegFile: + info := stat(x) + return info != nil && info.Mode().IsRegular() + case syntax.TsDirect: + return statMode(x, os.ModeDir) + //case syntax.TsCharSp: + //case syntax.TsBlckSp: + case syntax.TsNmPipe: + return statMode(x, os.ModeNamedPipe) + case syntax.TsSocket: + return statMode(x, os.ModeSocket) + case syntax.TsSmbLink: + info, _ := os.Lstat(x) + return info != nil && info.Mode()&os.ModeSymlink != 0 + case syntax.TsSticky: + return statMode(x, os.ModeSticky) + case syntax.TsUIDSet: + return statMode(x, os.ModeSetuid) + case syntax.TsGIDSet: + return statMode(x, os.ModeSetgid) + //case syntax.TsGrpOwn: + //case syntax.TsUsrOwn: + //case syntax.TsModif: + case syntax.TsRead: + f, err := os.OpenFile(x, os.O_RDONLY, 0) + if err == nil { + f.Close() + } + return err == nil + case syntax.TsWrite: + f, err := os.OpenFile(x, os.O_WRONLY, 0) + if err == nil { + f.Close() + } + return err == nil + case syntax.TsExec: + // use an absolute path to not use $PATH + _, err := exec.LookPath(filepath.Join(r.Dir, x)) + return err == nil + case syntax.TsNoEmpty: + info := stat(x) + return info != nil && info.Size() > 0 + //case syntax.TsFdTerm: + case syntax.TsEmpStr: + return x == "" + case syntax.TsNempStr: + return x != "" + //case syntax.TsOptSet: + //case syntax.TsVarSet: + //case syntax.TsRefVar: + case syntax.TsNot: + return x == "" + default: + r.errf("unhandled unary test op: %v", op) + return false + } +} diff --git a/vendor/github.com/mvdan/sh/syntax/canonical.sh b/vendor/github.com/mvdan/sh/syntax/canonical.sh new file mode 100644 index 00000000..e0acfc32 --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/canonical.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# separate comment + +! foo bar >a & + +foo() { bar; } + +{ + var1="some long value" # var1 comment + var2=short # var2 comment +} + +if foo; then bar; fi + +for foo in a b c; do + bar +done + +case $foo in + a) A ;; + b) + B + ;; +esac + +foo | bar +foo \ + && $(bar) \ + && (more) + +foo 2>&1 +foo < +// See LICENSE for licensing information + +// Package syntax implements parsing and formatting of shell programs. +// It supports both POSIX Shell and Bash. +package syntax diff --git a/vendor/github.com/mvdan/sh/syntax/lexer.go b/vendor/github.com/mvdan/sh/syntax/lexer.go new file mode 100644 index 00000000..a4f19e15 --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/lexer.go @@ -0,0 +1,1005 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +import ( + "bytes" + "io" + "unicode/utf8" +) + +// bytes that form or start a token +func regOps(r rune) bool { + switch r { + case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': + return true + } + return false +} + +// tokenize these inside parameter expansions +func paramOps(r rune) bool { + switch r { + case '}', '#', '!', ':', '-', '+', '=', '?', '%', '[', ']', '/', '^', + ',', '@': + return true + } + return false +} + +// tokenize these inside arithmetic expansions +func arithmOps(r rune) bool { + switch r { + case '+', '-', '!', '*', '/', '%', '(', ')', '^', '<', '>', ':', '=', + ',', '?', '|', '&', ']': + return true + } + return false +} + +func wordBreak(r rune) bool { + switch r { + case ' ', '\t', '\n', ';', '&', '>', '<', '|', '(', ')', '\r': + return true + } + return false +} + +func (p *parser) rune() rune { +retry: + if p.npos < len(p.bs) { + if b := p.bs[p.npos]; b < utf8.RuneSelf { + if p.npos++; b == '\n' { + p.f.lines = append(p.f.lines, p.getPos()) + } + if p.litBs != nil { + p.litBs = append(p.litBs, b) + } + r := rune(b) + p.r = r + return r + } + if p.npos+utf8.UTFMax >= len(p.bs) { + // we might need up to 4 bytes to read a full + // non-ascii rune + p.fill() + } + var w int + p.r, w = utf8.DecodeRune(p.bs[p.npos:]) + if p.litBs != nil { + p.litBs = append(p.litBs, p.bs[p.npos:p.npos+w]...) + } + p.npos += w + if p.r == utf8.RuneError && w == 1 { + p.posErr(p.getPos(), "invalid UTF-8 encoding") + } + } else { + if p.r == utf8.RuneSelf { + } else if p.fill(); p.bs == nil { + p.npos++ + p.r = utf8.RuneSelf + } else { + goto retry + } + } + return p.r +} + +func (p *parser) unrune(r rune) { + if p.r != utf8.RuneSelf { + p.npos -= utf8.RuneLen(p.r) + p.r = r + } +} + +// fill reads more bytes from the input src into readBuf. Any bytes that +// had not yet been used at the end of the buffer are slid into the +// beginning of the buffer. +func (p *parser) fill() { + left := len(p.bs) - p.npos + p.offs += p.npos + copy(p.readBuf[:left], p.readBuf[p.npos:]) + var n int + var err error + if p.readErr == nil { + n, err = p.src.Read(p.readBuf[left:]) + p.readErr = err + } else { + n, err = 0, p.readErr + } + if n == 0 { + // don't use p.errPass as we don't want to overwrite p.tok + if err != nil && err != io.EOF { + p.err = err + } + if left > 0 { + p.bs = p.readBuf[:left] + } else { + p.bs = nil + } + } else { + p.bs = p.readBuf[:left+n] + } + p.npos = 0 +} + +func (p *parser) nextKeepSpaces() { + r := p.r + if p.pos = p.getPos(); r > utf8.RuneSelf { + p.pos -= Pos(utf8.RuneLen(r) - 1) + } + switch p.quote { + case paramExpRepl: + switch r { + case '}', '/': + p.tok = p.paramToken(r) + case '`', '"', '$': + p.tok = p.dqToken(r) + default: + p.advanceLitOther(r) + } + case dblQuotes: + switch r { + case '`', '"', '$': + p.tok = p.dqToken(r) + default: + p.advanceLitDquote(r) + } + case hdocBody, hdocBodyTabs: + if r == '`' || r == '$' { + p.tok = p.dqToken(r) + } else if p.hdocStop == nil { + p.tok = illegalTok + } else { + p.advanceLitHdoc(r) + } + case paramExpExp: + switch r { + case '}': + p.rune() + p.tok = rightBrace + case '`', '"', '$': + p.tok = p.dqToken(r) + default: + p.advanceLitOther(r) + } + default: // sglQuotes + if r == '\'' { + p.rune() + p.tok = sglQuote + } else { + p.advanceLitOther(r) + } + } +} + +func (p *parser) next() { + if p.r == utf8.RuneSelf { + p.tok = _EOF + return + } + p.spaced, p.newLine = false, false + if p.quote&allKeepSpaces != 0 { + p.nextKeepSpaces() + return + } + r := p.r +skipSpace: + for { + switch r { + case utf8.RuneSelf: + p.tok = _EOF + return + case ' ', '\t', '\r': + p.spaced = true + r = p.rune() + case '\n': + if p.quote == arithmExprLet || p.quote == hdocWord { + p.tok = illegalTok + return + } + p.spaced, p.newLine = true, true + r = p.rune() + if len(p.heredocs) > p.buriedHdocs { + if p.doHeredocs(); p.tok == _EOF { + return + } + r = p.r + } + case '\\': + if !p.peekByte('\n') { + break skipSpace + } + p.rune() + r = p.rune() + default: + break skipSpace + } + } + if p.pos = p.getPos(); r > utf8.RuneSelf { + p.pos -= Pos(utf8.RuneLen(r) - 1) + } + switch { + case p.quote&allRegTokens != 0: + switch r { + case ';', '"', '\'', '(', ')', '$', '|', '&', '>', '<', '`': + p.tok = p.regToken(r) + case '#': + r = p.rune() + p.newLit(r) + for r != utf8.RuneSelf && r != '\n' { + r = p.rune() + } + if p.mode&ParseComments > 0 { + p.f.Comments = append(p.f.Comments, &Comment{ + Hash: p.pos, + Text: p.endLit(), + }) + } else { + p.litBs = nil + } + p.next() + case '?', '*', '+', '@', '!': + if p.peekByte('(') { + switch r { + case '?': + p.tok = globQuest + case '*': + p.tok = globStar + case '+': + p.tok = globPlus + case '@': + p.tok = globAt + default: // '!' + p.tok = globExcl + } + p.rune() + p.rune() + } else { + p.advanceLitNone(r) + } + default: + p.advanceLitNone(r) + } + case p.quote&allArithmExpr != 0 && arithmOps(r): + p.tok = p.arithmToken(r) + case p.quote&allParamExp != 0 && paramOps(r): + p.tok = p.paramToken(r) + case p.quote == testRegexp: + if regOps(r) && r != '(' { + p.tok = p.regToken(r) + } else { + p.advanceLitRe(r) + } + case regOps(r): + p.tok = p.regToken(r) + default: + p.advanceLitOther(r) + } + if p.err != nil && p.tok != _EOF { + p.tok = _EOF + } +} + +func (p *parser) peekByte(b byte) bool { + if p.npos == len(p.bs) && p.readErr == nil { + p.fill() + } + return p.npos < len(p.bs) && p.bs[p.npos] == b +} + +func (p *parser) regToken(r rune) token { + switch r { + case '\'': + p.rune() + return sglQuote + case '"': + p.rune() + return dblQuote + case '`': + p.rune() + return bckQuote + case '&': + switch p.rune() { + case '&': + p.rune() + return andAnd + case '>': + if !p.bash() { + break + } + if p.rune() == '>' { + p.rune() + return appAll + } + return rdrAll + } + return and + case '|': + switch p.rune() { + case '|': + p.rune() + return orOr + case '&': + if !p.bash() { + break + } + p.rune() + return pipeAll + } + return or + case '$': + switch p.rune() { + case '\'': + if !p.bash() { + break + } + p.rune() + return dollSglQuote + case '"': + if !p.bash() { + break + } + p.rune() + return dollDblQuote + case '{': + p.rune() + return dollBrace + case '[': + if !p.bash() { + break + } + p.rune() + return dollBrack + case '(': + if p.rune() == '(' { + p.rune() + return dollDblParen + } + return dollParen + } + return dollar + case '(': + if p.rune() == '(' && p.bash() { + p.rune() + return dblLeftParen + } + return leftParen + case ')': + p.rune() + return rightParen + case ';': + switch p.rune() { + case ';': + if p.rune() == '&' && p.bash() { + p.rune() + return dblSemiFall + } + return dblSemicolon + case '&': + if !p.bash() { + break + } + p.rune() + return semiFall + } + return semicolon + case '<': + switch p.rune() { + case '<': + if r = p.rune(); r == '-' { + p.rune() + return dashHdoc + } else if r == '<' && p.bash() { + p.rune() + return wordHdoc + } + return hdoc + case '>': + p.rune() + return rdrInOut + case '&': + p.rune() + return dplIn + case '(': + if !p.bash() { + break + } + p.rune() + return cmdIn + } + return rdrIn + default: // '>' + switch p.rune() { + case '>': + p.rune() + return appOut + case '&': + p.rune() + return dplOut + case '|': + p.rune() + return clbOut + case '(': + if !p.bash() { + break + } + p.rune() + return cmdOut + } + return rdrOut + } +} + +func (p *parser) dqToken(r rune) token { + switch r { + case '"': + p.rune() + return dblQuote + case '`': + p.rune() + return bckQuote + default: // '$' + switch p.rune() { + case '{': + p.rune() + return dollBrace + case '[': + if !p.bash() { + break + } + p.rune() + return dollBrack + case '(': + if p.rune() == '(' { + p.rune() + return dollDblParen + } + return dollParen + } + return dollar + } +} + +func (p *parser) paramToken(r rune) token { + switch r { + case '}': + p.rune() + return rightBrace + case ':': + switch p.rune() { + case '+': + p.rune() + return colPlus + case '-': + p.rune() + return colMinus + case '?': + p.rune() + return colQuest + case '=': + p.rune() + return colAssgn + } + return colon + case '+': + p.rune() + return plus + case '-': + p.rune() + return minus + case '?': + p.rune() + return quest + case '=': + p.rune() + return assgn + case '%': + if p.rune() == '%' { + p.rune() + return dblPerc + } + return perc + case '#': + if p.rune() == '#' { + p.rune() + return dblHash + } + return hash + case '!': + p.rune() + return exclMark + case '[': + p.rune() + return leftBrack + case '/': + if p.rune() == '/' { + p.rune() + return dblSlash + } + return slash + case '^': + if p.rune() == '^' { + p.rune() + return dblCaret + } + return caret + case ',': + if p.rune() == ',' { + p.rune() + return dblComma + } + return comma + default: // '@' + p.rune() + return at + } +} + +func (p *parser) arithmToken(r rune) token { + switch r { + case '!': + if p.rune() == '=' { + p.rune() + return nequal + } + return exclMark + case '=': + if p.rune() == '=' { + p.rune() + return equal + } + return assgn + case '(': + p.rune() + return leftParen + case ')': + p.rune() + return rightParen + case '&': + switch p.rune() { + case '&': + p.rune() + return andAnd + case '=': + p.rune() + return andAssgn + } + return and + case '|': + switch p.rune() { + case '|': + p.rune() + return orOr + case '=': + p.rune() + return orAssgn + } + return or + case '<': + switch p.rune() { + case '<': + if p.rune() == '=' { + p.rune() + return shlAssgn + } + return hdoc + case '=': + p.rune() + return lequal + } + return rdrIn + case '>': + switch p.rune() { + case '>': + if p.rune() == '=' { + p.rune() + return shrAssgn + } + return appOut + case '=': + p.rune() + return gequal + } + return rdrOut + case '+': + switch p.rune() { + case '+': + p.rune() + return addAdd + case '=': + p.rune() + return addAssgn + } + return plus + case '-': + switch p.rune() { + case '-': + p.rune() + return subSub + case '=': + p.rune() + return subAssgn + } + return minus + case '%': + if p.rune() == '=' { + p.rune() + return remAssgn + } + return perc + case '*': + switch p.rune() { + case '*': + p.rune() + return power + case '=': + p.rune() + return mulAssgn + } + return star + case '/': + if p.rune() == '=' { + p.rune() + return quoAssgn + } + return slash + case '^': + if p.rune() == '=' { + p.rune() + return xorAssgn + } + return caret + case ']': + p.rune() + return rightBrack + case ',': + p.rune() + return comma + case '?': + p.rune() + return quest + default: // ':' + p.rune() + return colon + } +} + +func (p *parser) newLit(r rune) { + // don't let r == utf8.RuneSelf go to the second case as RuneLen + // would return -1 + if r <= utf8.RuneSelf { + p.litBs = p.litBuf[:1] + p.litBs[0] = byte(r) + } else if p.npos <= len(p.bs) { + w := utf8.RuneLen(r) + p.litBs = append(p.litBuf[:0], p.bs[p.npos-w:p.npos]...) + } +} + +func (p *parser) discardLit(n int) { p.litBs = p.litBs[:len(p.litBs)-n] } + +func (p *parser) endLit() (s string) { + if p.r == utf8.RuneSelf { + s = string(p.litBs) + } else if len(p.litBs) > 0 { + s = string(p.litBs[:len(p.litBs)-1]) + } + p.litBs = nil + return +} + +func (p *parser) advanceLitOther(r rune) { + tok := _LitWord +loop: + for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { + switch r { + case '\\': // escaped byte follows + if r = p.rune(); r == '\n' { + p.discardLit(2) + } + case '\n': + switch p.quote { + case sglQuotes, paramExpRepl, paramExpExp: + default: + break loop + } + case '\'': + switch p.quote { + case paramExpExp, paramExpRepl: + default: + break loop + } + case '"', '`', '$': + if p.quote != sglQuotes { + tok = _Lit + break loop + } + case '}': + if p.quote&allParamExp != 0 { + break loop + } + case '/': + if p.quote&allParamExp != 0 && p.quote != paramExpExp { + break loop + } + case ']': + if p.quote&allRbrack != 0 { + break loop + } + case '!', '*': + if p.quote&allArithmExpr != 0 { + break loop + } + case ':', '=', '%', '?', '^', ',': + if p.quote&allArithmExpr != 0 || p.quote&allParamReg != 0 { + break loop + } + case '#', '[', '@': + if p.quote&allParamReg != 0 { + break loop + } + case '+', '-': + switch p.quote { + case paramExpInd, paramExpLen, paramExpOff, + paramExpExp, paramExpRepl, sglQuotes: + default: + break loop + } + case ' ', '\t', ';', '&', '>', '<', '|', '(', ')', '\r': + switch p.quote { + case paramExpExp, paramExpRepl, sglQuotes: + default: + break loop + } + } + } + p.tok, p.val = tok, p.endLit() +} + +func (p *parser) advanceLitNone(r rune) { + p.asPos = 0 + tok := _LitWord +loop: + for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { + switch r { + case ' ', '\t', '\n', '\r', '&', '|', ';', '(', ')': + break loop + case '\\': // escaped byte follows + r = p.rune() + switch r { + case '\n': + p.discardLit(2) + case '\\': + // TODO: also treat escaped ` and $ + // differently in backquotes + if p.quote == subCmdBckquo { + p.discardLit(1) + if r = p.rune(); r == '\\' { + p.discardLit(1) + r = p.rune() + } + } + } + case '>', '<': + if p.peekByte('(') { + tok = _Lit + } else { + tok = _LitRedir + } + break loop + case '`': + if p.quote != subCmdBckquo { + tok = _Lit + } + break loop + case '"', '\'', '$': + tok = _Lit + break loop + case '?', '*', '+', '@', '!': + if p.peekByte('(') { + tok = _Lit + break loop + } + case '=': + p.asPos = len(p.litBs) - 1 + } + } + p.tok, p.val = tok, p.endLit() +} + +func (p *parser) advanceLitDquote(r rune) { + tok := _LitWord +loop: + for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { + switch r { + case '"': + break loop + case '\\': // escaped byte follows + p.rune() + case '`', '$': + tok = _Lit + break loop + } + } + p.tok, p.val = tok, p.endLit() +} + +func (p *parser) advanceLitHdoc(r rune) { + p.tok = _Lit + p.newLit(r) + if p.quote == hdocBodyTabs { + for r == '\t' { + r = p.rune() + } + } + lStart := len(p.litBs) - 1 +loop: + for ; r != utf8.RuneSelf; r = p.rune() { + switch r { + case '`', '$': + break loop + case '\\': // escaped byte follows + p.rune() + case '\n': + if bytes.Equal(p.litBs[lStart:len(p.litBs)-1], p.hdocStop) { + p.val = p.endLit()[:lStart] + p.hdocStop = nil + return + } + if p.quote == hdocBodyTabs { + for p.peekByte('\t') { + p.rune() + } + } + lStart = len(p.litBs) + } + } + if bytes.Equal(p.litBs[lStart:], p.hdocStop) { + p.val = p.endLit()[:lStart] + p.hdocStop = nil + } else { + p.val = p.endLit() + } +} + +func (p *parser) hdocLitWord() *Word { + r := p.r + p.newLit(r) + pos, val := p.getPos(), "" + for { + if r == utf8.RuneSelf { + val = p.endLit() + break + } + if p.quote == hdocBodyTabs { + for r == '\t' { + r = p.rune() + } + } + lStart := len(p.litBs) - 1 + for r != utf8.RuneSelf && r != '\n' { + r = p.rune() + } + lEnd := len(p.litBs) + if r != utf8.RuneSelf { + lEnd-- + } + if bytes.Equal(p.litBs[lStart:lEnd], p.hdocStop) { + val = p.endLit()[:lStart] + break + } + r = p.rune() + } + l := p.lit(pos, val) + return p.word(p.singleWps(l)) +} + +func (p *parser) advanceLitRe(r rune) { + lparens := 0 +loop: + for p.newLit(r); r != utf8.RuneSelf; r = p.rune() { + switch r { + case '\\': + p.rune() + case '(': + lparens++ + case ')': + lparens-- + case ' ', '\t', '\r', '\n', ';': + if lparens == 0 { + break loop + } + } + } + p.tok, p.val = _LitWord, p.endLit() +} + +func testUnaryOp(val string) token { + switch val { + case "!": + return exclMark + case "-e", "-a": + return tsExists + case "-f": + return tsRegFile + case "-d": + return tsDirect + case "-c": + return tsCharSp + case "-b": + return tsBlckSp + case "-p": + return tsNmPipe + case "-S": + return tsSocket + case "-L", "-h": + return tsSmbLink + case "-k": + return tsSticky + case "-g": + return tsGIDSet + case "-u": + return tsUIDSet + case "-G": + return tsGrpOwn + case "-O": + return tsUsrOwn + case "-N": + return tsModif + case "-r": + return tsRead + case "-w": + return tsWrite + case "-x": + return tsExec + case "-s": + return tsNoEmpty + case "-t": + return tsFdTerm + case "-z": + return tsEmpStr + case "-n": + return tsNempStr + case "-o": + return tsOptSet + case "-v": + return tsVarSet + case "-R": + return tsRefVar + default: + return illegalTok + } +} + +func testBinaryOp(val string) token { + switch val { + case "==", "=": + return equal + case "!=": + return nequal + case "=~": + return tsReMatch + case "-nt": + return tsNewer + case "-ot": + return tsOlder + case "-ef": + return tsDevIno + case "-eq": + return tsEql + case "-ne": + return tsNeq + case "-le": + return tsLeq + case "-ge": + return tsGeq + case "-lt": + return tsLss + case "-gt": + return tsGtr + default: + return illegalTok + } +} diff --git a/vendor/github.com/mvdan/sh/syntax/nodes.go b/vendor/github.com/mvdan/sh/syntax/nodes.go new file mode 100644 index 00000000..66e5e1ab --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/nodes.go @@ -0,0 +1,731 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +import "fmt" + +// Node represents an AST node. +type Node interface { + // Pos returns the first character of the node + Pos() Pos + // End returns the character immediately after the node + End() Pos +} + +// File is a shell program. +type File struct { + Name string + + Stmts []*Stmt + Comments []*Comment + + lines []Pos +} + +// Pos is the internal representation of a position within a source +// file. +type Pos uint32 + +// IsValid reports whether the position is valid. All positions in nodes +// returned by Parse are valid. +func (p Pos) IsValid() bool { return p > 0 } + +const maxPos = Pos(^uint32(0)) + +// Position describes a position within a source file including the line +// and column location. A Position is valid if the line number is > 0. +type Position struct { + Filename string // if any + Offset int // byte offset, starting at 0 + Line int // line number, starting at 1 + Column int // column number, starting at 1 (in bytes) +} + +// IsValid reports whether the position is valid. All positions in nodes +// returned by Parse are valid. +func (p Position) IsValid() bool { return p.Line > 0 } + +// String returns the position in the "file:line:column" form, or +// "line:column" if there is no filename available. +func (p Position) String() string { + prefix := "" + if p.Filename != "" { + prefix = p.Filename + ":" + } + return fmt.Sprintf("%s%d:%d", prefix, p.Line, p.Column) +} + +func (f *File) Pos() Pos { + if len(f.Stmts) == 0 { + return 0 + } + return f.Stmts[0].Pos() +} + +func (f *File) End() Pos { + if len(f.Stmts) == 0 { + return 0 + } + return f.Stmts[len(f.Stmts)-1].End() +} + +func (f *File) Position(p Pos) (pos Position) { + pos.Filename = f.Name + pos.Offset = int(p) - 1 + if i := searchPos(f.lines, p); i >= 0 { + pos.Line, pos.Column = i+1, int(p-f.lines[i]) + } + return +} + +func searchPos(a []Pos, x Pos) int { + i, j := 0, len(a) + for i < j { + h := i + (j-i)/2 + if a[h] <= x { + i = h + 1 + } else { + j = h + } + } + return i - 1 +} + +func posMax(p1, p2 Pos) Pos { + if p2 > p1 { + return p2 + } + return p1 +} + +// Comment represents a single comment on a single line. +type Comment struct { + Hash Pos + Text string +} + +func (c *Comment) Pos() Pos { return c.Hash } +func (c *Comment) End() Pos { return c.Hash + Pos(len(c.Text)) } + +// Stmt represents a statement, otherwise known as a compound command. +// It is compromised of a command and other components that may come +// before or after it. +type Stmt struct { + Cmd Command + Position Pos + Semicolon Pos + Negated bool + Background bool + Assigns []*Assign + Redirs []*Redirect +} + +func (s *Stmt) Pos() Pos { return s.Position } +func (s *Stmt) End() Pos { + if s.Semicolon.IsValid() { + return s.Semicolon + 1 + } + end := s.Position + if s.Negated { + end++ + } + if s.Cmd != nil { + end = s.Cmd.End() + } + if len(s.Assigns) > 0 { + end = posMax(end, s.Assigns[len(s.Assigns)-1].End()) + } + if len(s.Redirs) > 0 { + end = posMax(end, s.Redirs[len(s.Redirs)-1].End()) + } + return end +} + +// Command represents all nodes that are simple commands, which are +// directly placed in a Stmt. +type Command interface { + Node + commandNode() +} + +func (*CallExpr) commandNode() {} +func (*IfClause) commandNode() {} +func (*WhileClause) commandNode() {} +func (*UntilClause) commandNode() {} +func (*ForClause) commandNode() {} +func (*CaseClause) commandNode() {} +func (*Block) commandNode() {} +func (*Subshell) commandNode() {} +func (*BinaryCmd) commandNode() {} +func (*FuncDecl) commandNode() {} +func (*ArithmCmd) commandNode() {} +func (*TestClause) commandNode() {} +func (*DeclClause) commandNode() {} +func (*EvalClause) commandNode() {} +func (*LetClause) commandNode() {} +func (*CoprocClause) commandNode() {} + +// Assign represents an assignment to a variable. +type Assign struct { + Append bool + Name *Lit + Value *Word +} + +func (a *Assign) Pos() Pos { + if a.Name != nil { + return a.Name.Pos() + } + return a.Value.Pos() +} + +func (a *Assign) End() Pos { + if a.Value != nil { + return a.Value.End() + } + return a.Name.End() + 1 +} + +// Redirect represents an input/output redirection. +type Redirect struct { + OpPos Pos + Op RedirOperator + N *Lit + Word, Hdoc *Word +} + +func (r *Redirect) Pos() Pos { + if r.N != nil { + return r.N.Pos() + } + return r.OpPos +} +func (r *Redirect) End() Pos { return r.Word.End() } + +// CallExpr represents a command execution or function call. +type CallExpr struct { + Args []*Word +} + +func (c *CallExpr) Pos() Pos { return c.Args[0].Pos() } +func (c *CallExpr) End() Pos { return c.Args[len(c.Args)-1].End() } + +// Subshell represents a series of commands that should be executed in a +// nested shell environment. +type Subshell struct { + Lparen, Rparen Pos + Stmts []*Stmt +} + +func (s *Subshell) Pos() Pos { return s.Lparen } +func (s *Subshell) End() Pos { return s.Rparen + 1 } + +// Block represents a series of commands that should be executed in a +// nested scope. +type Block struct { + Lbrace, Rbrace Pos + Stmts []*Stmt +} + +func (b *Block) Pos() Pos { return b.Rbrace } +func (b *Block) End() Pos { return b.Rbrace + 1 } + +// IfClause represents an if statement. +type IfClause struct { + If, Then, Else, Fi Pos + CondStmts []*Stmt + ThenStmts []*Stmt + Elifs []*Elif + ElseStmts []*Stmt +} + +func (c *IfClause) Pos() Pos { return c.If } +func (c *IfClause) End() Pos { return c.Fi + 2 } + +// Elif represents an "else if" case in an if clause. +type Elif struct { + Elif, Then Pos + CondStmts []*Stmt + ThenStmts []*Stmt +} + +// WhileClause represents a while clause. +type WhileClause struct { + While, Do, Done Pos + CondStmts []*Stmt + DoStmts []*Stmt +} + +func (w *WhileClause) Pos() Pos { return w.While } +func (w *WhileClause) End() Pos { return w.Done + 4 } + +// UntilClause represents an until clause. +type UntilClause struct { + Until, Do, Done Pos + CondStmts []*Stmt + DoStmts []*Stmt +} + +func (u *UntilClause) Pos() Pos { return u.Until } +func (u *UntilClause) End() Pos { return u.Done + 4 } + +// ForClause represents a for clause. +type ForClause struct { + For, Do, Done Pos + Loop Loop + DoStmts []*Stmt +} + +func (f *ForClause) Pos() Pos { return f.For } +func (f *ForClause) End() Pos { return f.Done + 4 } + +// Loop represents all nodes that can be loops in a for clause. +type Loop interface { + Node + loopNode() +} + +func (*WordIter) loopNode() {} +func (*CStyleLoop) loopNode() {} + +// WordIter represents the iteration of a variable over a series of +// words in a for clause. +type WordIter struct { + Name *Lit + List []*Word +} + +func (w *WordIter) Pos() Pos { return w.Name.Pos() } +func (w *WordIter) End() Pos { return posMax(w.Name.End(), wordLastEnd(w.List)) } + +// CStyleLoop represents the behaviour of a for clause similar to the C +// language. +// +// This node will never appear when in PosixConformant mode. +type CStyleLoop struct { + Lparen, Rparen Pos + Init, Cond, Post ArithmExpr +} + +func (c *CStyleLoop) Pos() Pos { return c.Lparen } +func (c *CStyleLoop) End() Pos { return c.Rparen + 2 } + +// BinaryCmd represents a binary expression between two statements. +type BinaryCmd struct { + OpPos Pos + Op BinCmdOperator + X, Y *Stmt +} + +func (b *BinaryCmd) Pos() Pos { return b.X.Pos() } +func (b *BinaryCmd) End() Pos { return b.Y.End() } + +// FuncDecl represents the declaration of a function. +type FuncDecl struct { + Position Pos + BashStyle bool + Name *Lit + Body *Stmt +} + +func (f *FuncDecl) Pos() Pos { return f.Position } +func (f *FuncDecl) End() Pos { return f.Body.End() } + +// Word represents a non-empty list of nodes that are contiguous to each +// other. The word is delimeted by word boundaries. +type Word struct { + Parts []WordPart +} + +func (w *Word) Pos() Pos { return w.Parts[0].Pos() } +func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() } + +// WordPart represents all nodes that can form a word. +type WordPart interface { + Node + wordPartNode() +} + +func (*Lit) wordPartNode() {} +func (*SglQuoted) wordPartNode() {} +func (*DblQuoted) wordPartNode() {} +func (*ParamExp) wordPartNode() {} +func (*CmdSubst) wordPartNode() {} +func (*ArithmExp) wordPartNode() {} +func (*ProcSubst) wordPartNode() {} +func (*ArrayExpr) wordPartNode() {} +func (*ExtGlob) wordPartNode() {} + +// Lit represents an unquoted string consisting of characters that were +// not tokenized. +type Lit struct { + ValuePos, ValueEnd Pos + Value string +} + +func (l *Lit) Pos() Pos { return l.ValuePos } +func (l *Lit) End() Pos { return l.ValueEnd } + +// SglQuoted represents a string within single quotes. +type SglQuoted struct { + Position Pos + Dollar bool + Value string +} + +func (q *SglQuoted) Pos() Pos { return q.Position } +func (q *SglQuoted) End() Pos { + end := q.Position + 2 + Pos(len(q.Value)) + if q.Dollar { + end++ + } + return end +} + +// DblQuoted represents a list of nodes within double quotes. +type DblQuoted struct { + Position Pos + Dollar bool + Parts []WordPart +} + +func (q *DblQuoted) Pos() Pos { return q.Position } +func (q *DblQuoted) End() Pos { + if len(q.Parts) == 0 { + if q.Dollar { + return q.Position + 3 + } + return q.Position + 2 + } + return q.Parts[len(q.Parts)-1].End() + 1 +} + +// CmdSubst represents a command substitution. +type CmdSubst struct { + Left, Right Pos + Stmts []*Stmt +} + +func (c *CmdSubst) Pos() Pos { return c.Left } +func (c *CmdSubst) End() Pos { return c.Right + 1 } + +// ParamExp represents a parameter expansion. +type ParamExp struct { + Dollar, Rbrace Pos + Short bool + Length, Excl bool // TODO(mvdan): rename Excl in 2.0 (Indirect, etc) + Param *Lit + Ind *Index + Slice *Slice + Repl *Replace + Exp *Expansion +} + +func (p *ParamExp) Pos() Pos { return p.Dollar } +func (p *ParamExp) End() Pos { + if !p.Short { + return p.Rbrace + 1 + } + return p.Param.End() +} + +// Index represents access to an array via an index inside a ParamExp. +// +// This node will never appear when in PosixConformant mode. +type Index struct { + Expr ArithmExpr +} + +// Slice represents character slicing inside a ParamExp. +// +// This node will never appear when in PosixConformant mode. +type Slice struct { + Offset, Length ArithmExpr +} + +// Replace represents a search and replace inside a ParamExp. +type Replace struct { + All bool + Orig, With *Word +} + +// Expansion represents string manipulation in a ParamExp other than +// those covered by Replace. +type Expansion struct { + Op ParExpOperator + Word *Word +} + +// ArithmExp represents an arithmetic expansion. +type ArithmExp struct { + Left, Right Pos + Bracket bool + X ArithmExpr +} + +func (a *ArithmExp) Pos() Pos { return a.Left } +func (a *ArithmExp) End() Pos { + if a.Bracket { + return a.Right + 1 + } + return a.Right + 2 +} + +// ArithmCmd represents an arithmetic command. +// +// This node will never appear when in PosixConformant mode. +type ArithmCmd struct { + Left, Right Pos + X ArithmExpr +} + +func (a *ArithmCmd) Pos() Pos { return a.Left } +func (a *ArithmCmd) End() Pos { return a.Right + 2 } + +// ArithmExpr represents all nodes that form arithmetic expressions. +type ArithmExpr interface { + Node + arithmExprNode() +} + +func (*BinaryArithm) arithmExprNode() {} +func (*UnaryArithm) arithmExprNode() {} +func (*ParenArithm) arithmExprNode() {} +func (*Word) arithmExprNode() {} + +// BinaryArithm represents a binary expression between two arithmetic +// expression. +// +// If Op is any assign operator, X will be a *Word with a single *Lit +// whose value is a valid name. +// +// Ternary operators like "a ? b : c" are fit into this structure. Thus, +// if Op == Quest, Y will be a *BinaryArithm with Op == Colon. Op can +// only be Colon in that scenario. +// +// TODO(mvdan): we probably want to split up assigns in 2.0 (X would be +// a *Lit) to simplify the rules here. Perhaps reuse the Assign type? +type BinaryArithm struct { + OpPos Pos + Op BinAritOperator + X, Y ArithmExpr +} + +func (b *BinaryArithm) Pos() Pos { return b.X.Pos() } +func (b *BinaryArithm) End() Pos { return b.Y.End() } + +// UnaryArithm represents an unary expression over a node, either before +// or after it. +// +// If Op is Inc or Dec, X will be a *Word with a single *Lit whose value +// is a valid name. +// +// TODO(mvdan): consider splitting up Inc/Dec like the assigns above in +// 2.0. +type UnaryArithm struct { + OpPos Pos + Op UnAritOperator + Post bool + X ArithmExpr +} + +func (u *UnaryArithm) Pos() Pos { + if u.Post { + return u.X.Pos() + } + return u.OpPos +} + +func (u *UnaryArithm) End() Pos { + if u.Post { + return u.OpPos + 2 + } + return u.X.End() +} + +// ParenArithm represents an expression within parentheses inside an +// ArithmExp. +type ParenArithm struct { + Lparen, Rparen Pos + X ArithmExpr +} + +func (p *ParenArithm) Pos() Pos { return p.Lparen } +func (p *ParenArithm) End() Pos { return p.Rparen + 1 } + +// CaseClause represents a case (switch) clause. +type CaseClause struct { + Case, Esac Pos + Word *Word + List []*PatternList +} + +func (c *CaseClause) Pos() Pos { return c.Case } +func (c *CaseClause) End() Pos { return c.Esac + 4 } + +// PatternList represents a pattern list (case) within a CaseClause. +type PatternList struct { + Op CaseOperator + OpPos Pos + Patterns []*Word + Stmts []*Stmt +} + +// TestClause represents a Bash extended test clause. +// +// This node will never appear when in PosixConformant mode. +type TestClause struct { + Left, Right Pos + X TestExpr +} + +func (t *TestClause) Pos() Pos { return t.Left } +func (t *TestClause) End() Pos { return t.Right + 2 } + +// TestExpr represents all nodes that form arithmetic expressions. +type TestExpr interface { + Node + testExprNode() +} + +func (*BinaryTest) testExprNode() {} +func (*UnaryTest) testExprNode() {} +func (*ParenTest) testExprNode() {} +func (*Word) testExprNode() {} + +// BinaryTest represents a binary expression between two arithmetic +// expression. +type BinaryTest struct { + OpPos Pos + Op BinTestOperator + X, Y TestExpr +} + +func (b *BinaryTest) Pos() Pos { return b.X.Pos() } +func (b *BinaryTest) End() Pos { return b.Y.End() } + +// UnaryTest represents an unary expression over a node, either before +// or after it. +type UnaryTest struct { + OpPos Pos + Op UnTestOperator + X TestExpr +} + +func (u *UnaryTest) Pos() Pos { return u.OpPos } +func (u *UnaryTest) End() Pos { return u.X.End() } + +// ParenTest represents an expression within parentheses inside an +// TestExp. +type ParenTest struct { + Lparen, Rparen Pos + X TestExpr +} + +func (p *ParenTest) Pos() Pos { return p.Lparen } +func (p *ParenTest) End() Pos { return p.Rparen + 1 } + +// DeclClause represents a Bash declare clause. +// +// This node will never appear when in PosixConformant mode. +type DeclClause struct { + Position Pos + Variant string + Opts []*Word + Assigns []*Assign +} + +func (d *DeclClause) Pos() Pos { return d.Position } +func (d *DeclClause) End() Pos { + if len(d.Assigns) > 0 { + return d.Assigns[len(d.Assigns)-1].End() + } + return wordLastEnd(d.Opts) +} + +// ArrayExpr represents a Bash array expression. +// +// This node will never appear when in PosixConformant mode. +type ArrayExpr struct { + Lparen, Rparen Pos + List []*Word +} + +func (a *ArrayExpr) Pos() Pos { return a.Lparen } +func (a *ArrayExpr) End() Pos { return a.Rparen + 1 } + +// ExtGlob represents a Bash extended globbing expression. Note that +// these are parsed independently of whether shopt has been called or +// not. +// +// This node will never appear when in PosixConformant mode. +type ExtGlob struct { + OpPos Pos + Op GlobOperator + Pattern *Lit +} + +func (e *ExtGlob) Pos() Pos { return e.OpPos } +func (e *ExtGlob) End() Pos { return e.Pattern.End() + 1 } + +// ProcSubst represents a Bash process substitution. +// +// This node will never appear when in PosixConformant mode. +type ProcSubst struct { + OpPos, Rparen Pos + Op ProcOperator + Stmts []*Stmt +} + +func (s *ProcSubst) Pos() Pos { return s.OpPos } +func (s *ProcSubst) End() Pos { return s.Rparen + 1 } + +// EvalClause represents a Bash eval clause. +// +// This node will never appear when in PosixConformant mode. +// +// TODO(mvdan): EvalClause is actually pointless, as any non-trivial use +// of eval will involve parsing the program at run-time. Remove in 2.0. +type EvalClause struct { + Eval Pos + Stmt *Stmt +} + +func (e *EvalClause) Pos() Pos { return e.Eval } +func (e *EvalClause) End() Pos { + if e.Stmt != nil { + return e.Stmt.End() + } + return e.Eval + 4 +} + +// CoprocClause represents a Bash coproc clause. +// +// This node will never appear when in PosixConformant mode. +type CoprocClause struct { + Coproc Pos + Name *Lit + Stmt *Stmt +} + +func (c *CoprocClause) Pos() Pos { return c.Coproc } +func (c *CoprocClause) End() Pos { return c.Stmt.End() } + +// LetClause represents a Bash let clause. +// +// This node will never appear when in PosixConformant mode. +type LetClause struct { + Let Pos + Exprs []ArithmExpr +} + +func (l *LetClause) Pos() Pos { return l.Let } +func (l *LetClause) End() Pos { return l.Exprs[len(l.Exprs)-1].End() } + +func wordLastEnd(ws []*Word) Pos { + if len(ws) == 0 { + return 0 + } + return ws[len(ws)-1].End() +} diff --git a/vendor/github.com/mvdan/sh/syntax/parser.go b/vendor/github.com/mvdan/sh/syntax/parser.go new file mode 100644 index 00000000..d97847a0 --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/parser.go @@ -0,0 +1,1768 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +import ( + "bytes" + "fmt" + "io" + "strconv" + "sync" + "unicode/utf8" +) + +// ParseMode controls the parser behaviour via a set of flags. +type ParseMode uint + +const ( + ParseComments ParseMode = 1 << iota // add comments to the AST + PosixConformant // match the POSIX standard where it differs from bash +) + +var parserFree = sync.Pool{ + New: func() interface{} { + return &parser{helperBuf: new(bytes.Buffer)} + }, +} + +// Parse reads and parses a shell program with an optional name. It +// returns the parsed program if no issues were encountered. Otherwise, +// an error is returned. +func Parse(src io.Reader, name string, mode ParseMode) (*File, error) { + p := parserFree.Get().(*parser) + p.reset() + alloc := &struct { + f File + l [32]Pos + }{} + p.f = &alloc.f + p.f.Name = name + p.f.lines = alloc.l[:1] + p.src, p.mode = src, mode + p.rune() + p.next() + p.f.Stmts = p.stmts() + if p.err == nil { + // EOF immediately after heredoc word so no newline to + // trigger it + p.doHeredocs() + } + f, err := p.f, p.err + parserFree.Put(p) + return f, err +} + +type parser struct { + src io.Reader + bs []byte // current chunk of read bytes + r rune + + f *File + mode ParseMode + + spaced bool // whether tok has whitespace on its left + newLine bool // whether tok is on a new line + + err error // lexer/parser error + readErr error // got a read error, but bytes left + + tok token // current token + val string // current value (valid if tok is _Lit*) + + pos Pos // position of tok + offs int // chunk offset + npos int // pos within chunk for the next rune + + quote quoteState // current lexer state + asPos int // position of '=' in a literal + + forbidNested bool + + // list of pending heredoc bodies + buriedHdocs int + heredocs []*Redirect + hdocStop []byte + + helperBuf *bytes.Buffer + + litBatch []Lit + wordBatch []Word + wpsBatch []WordPart + stmtBatch []Stmt + stListBatch []*Stmt + callBatch []callAlloc + + readBuf [bufSize]byte + litBuf [bufSize]byte + litBs []byte +} + +const bufSize = 1 << 10 + +func (p *parser) reset() { + p.bs = nil + p.offs, p.npos = 0, 0 + p.r, p.err, p.readErr = 0, nil, nil + p.quote, p.forbidNested = noState, false + p.heredocs, p.buriedHdocs = p.heredocs[:0], 0 +} + +func (p *parser) getPos() Pos { return Pos(p.offs + p.npos) } + +func (p *parser) lit(pos Pos, val string) *Lit { + if len(p.litBatch) == 0 { + p.litBatch = make([]Lit, 64) + } + l := &p.litBatch[0] + p.litBatch = p.litBatch[1:] + l.ValuePos = pos + l.ValueEnd = p.getPos() + l.Value = val + return l +} + +func (p *parser) word(parts []WordPart) *Word { + if len(p.wordBatch) == 0 { + p.wordBatch = make([]Word, 32) + } + w := &p.wordBatch[0] + p.wordBatch = p.wordBatch[1:] + w.Parts = parts + return w +} + +func (p *parser) singleWps(wp WordPart) []WordPart { + if len(p.wpsBatch) == 0 { + p.wpsBatch = make([]WordPart, 64) + } + wps := p.wpsBatch[:1:1] + p.wpsBatch = p.wpsBatch[1:] + wps[0] = wp + return wps +} + +func (p *parser) wps() []WordPart { + if len(p.wpsBatch) < 4 { + p.wpsBatch = make([]WordPart, 64) + } + wps := p.wpsBatch[:0:4] + p.wpsBatch = p.wpsBatch[4:] + return wps +} + +func (p *parser) stmt(pos Pos) *Stmt { + if len(p.stmtBatch) == 0 { + p.stmtBatch = make([]Stmt, 16) + } + s := &p.stmtBatch[0] + p.stmtBatch = p.stmtBatch[1:] + s.Position = pos + return s +} + +func (p *parser) stList() []*Stmt { + if len(p.stListBatch) == 0 { + p.stListBatch = make([]*Stmt, 128) + } + stmts := p.stListBatch[:0:4] + p.stListBatch = p.stListBatch[4:] + return stmts +} + +type callAlloc struct { + ce CallExpr + ws [4]*Word +} + +func (p *parser) call(w *Word) *CallExpr { + if len(p.callBatch) == 0 { + p.callBatch = make([]callAlloc, 32) + } + alloc := &p.callBatch[0] + p.callBatch = p.callBatch[1:] + ce := &alloc.ce + ce.Args = alloc.ws[:1] + ce.Args[0] = w + return ce +} + +type quoteState uint + +const ( + noState quoteState = 1 << iota + subCmd + subCmdBckquo + sglQuotes + dblQuotes + hdocWord + hdocBody + hdocBodyTabs + arithmExpr + arithmExprLet + arithmExprCmd + arithmExprBrack + testRegexp + switchCase + paramExpName + paramExpInd + paramExpOff + paramExpLen + paramExpRepl + paramExpExp + + allKeepSpaces = paramExpRepl | dblQuotes | hdocBody | + hdocBodyTabs | paramExpExp | sglQuotes + allRegTokens = noState | subCmd | subCmdBckquo | hdocWord | switchCase + allArithmExpr = arithmExpr | arithmExprLet | arithmExprCmd | + arithmExprBrack | allParamArith + allRbrack = arithmExprBrack | paramExpInd + allParamArith = paramExpInd | paramExpOff | paramExpLen + allParamReg = paramExpName | allParamArith + allParamExp = allParamReg | paramExpRepl | paramExpExp +) + +func (p *parser) bash() bool { return p.mode&PosixConformant == 0 } + +type saveState struct { + quote quoteState + buriedHdocs int +} + +func (p *parser) preNested(quote quoteState) (s saveState) { + s.quote, s.buriedHdocs = p.quote, p.buriedHdocs + p.buriedHdocs, p.quote = len(p.heredocs), quote + return +} + +func (p *parser) postNested(s saveState) { + p.quote, p.buriedHdocs = s.quote, s.buriedHdocs +} + +func (p *parser) unquotedWordBytes(w *Word) ([]byte, bool) { + p.helperBuf.Reset() + didUnquote := false + for _, wp := range w.Parts { + if p.unquotedWordPart(p.helperBuf, wp, false) { + didUnquote = true + } + } + return p.helperBuf.Bytes(), didUnquote +} + +func (p *parser) unquotedWordPart(buf *bytes.Buffer, wp WordPart, quotes bool) (quoted bool) { + switch x := wp.(type) { + case *Lit: + for i := 0; i < len(x.Value); i++ { + if b := x.Value[i]; b == '\\' && !quotes { + if i++; i < len(x.Value) { + buf.WriteByte(x.Value[i]) + } + quoted = true + } else { + buf.WriteByte(b) + } + } + case *SglQuoted: + buf.WriteString(x.Value) + quoted = true + case *DblQuoted: + for _, wp2 := range x.Parts { + p.unquotedWordPart(buf, wp2, true) + } + quoted = true + } + return +} + +func (p *parser) doHeredocs() { + old := p.quote + hdocs := p.heredocs[p.buriedHdocs:] + p.heredocs = p.heredocs[:p.buriedHdocs] + for i, r := range hdocs { + if p.err != nil { + break + } + if r.Op == DashHdoc { + p.quote = hdocBodyTabs + } else { + p.quote = hdocBody + } + var quoted bool + p.hdocStop, quoted = p.unquotedWordBytes(r.Word) + if i > 0 && p.r == '\n' { + p.rune() + } + if quoted { + r.Hdoc = p.hdocLitWord() + } else { + p.next() + r.Hdoc = p.getWordOrEmpty() + } + } + p.quote = old +} + +func (p *parser) got(tok token) bool { + if p.tok == tok { + p.next() + return true + } + return false +} + +func (p *parser) gotRsrv(val string) bool { + if p.tok == _LitWord && p.val == val { + p.next() + return true + } + return false +} + +func (p *parser) gotSameLine(tok token) bool { + if !p.newLine && p.tok == tok { + p.next() + return true + } + return false +} + +func readableStr(s string) string { + // don't quote tokens like & or } + if s != "" && s[0] >= 'a' && s[0] <= 'z' { + return strconv.Quote(s) + } + return s +} + +func (p *parser) followErr(pos Pos, left, right string) { + leftStr := readableStr(left) + p.posErr(pos, "%s must be followed by %s", leftStr, right) +} + +func (p *parser) followErrExp(pos Pos, left string) { + p.followErr(pos, left, "an expression") +} + +func (p *parser) follow(lpos Pos, left string, tok token) Pos { + pos := p.pos + if !p.got(tok) { + p.followErr(lpos, left, tok.String()) + } + return pos +} + +func (p *parser) followRsrv(lpos Pos, left, val string) Pos { + pos := p.pos + if !p.gotRsrv(val) { + p.followErr(lpos, left, fmt.Sprintf("%q", val)) + } + return pos +} + +func (p *parser) followStmts(left string, lpos Pos, stops ...string) []*Stmt { + if p.gotSameLine(semicolon) { + return nil + } + sts := p.stmts(stops...) + if len(sts) < 1 && !p.newLine { + p.followErr(lpos, left, "a statement list") + } + return sts +} + +func (p *parser) followWordTok(tok token, pos Pos) *Word { + w := p.getWord() + if w == nil { + p.followErr(pos, tok.String(), "a word") + } + return w +} + +func (p *parser) followWord(s string, pos Pos) *Word { + w := p.getWord() + if w == nil { + p.followErr(pos, s, "a word") + } + return w +} + +func (p *parser) stmtEnd(n Node, start, end string) Pos { + pos := p.pos + if !p.gotRsrv(end) { + p.posErr(n.Pos(), "%s statement must end with %q", start, end) + } + return pos +} + +func (p *parser) quoteErr(lpos Pos, quote token) { + p.posErr(lpos, "reached %s without closing quote %s", + p.tok.String(), quote) +} + +func (p *parser) matchingErr(lpos Pos, left, right interface{}) { + p.posErr(lpos, "reached %s without matching %s with %s", + p.tok.String(), left, right) +} + +func (p *parser) matched(lpos Pos, left, right token) Pos { + pos := p.pos + if !p.got(right) { + p.matchingErr(lpos, left, right) + } + return pos +} + +func (p *parser) errPass(err error) { + if p.err == nil { + p.err = err + p.npos = len(p.bs) + 1 + p.r = utf8.RuneSelf + p.tok = _EOF + } +} + +// ParseError represents an error found when parsing a source file. +type ParseError struct { + Position + Text string +} + +func (e *ParseError) Error() string { + return fmt.Sprintf("%s: %s", e.Position.String(), e.Text) +} + +func (p *parser) posErr(pos Pos, format string, a ...interface{}) { + p.errPass(&ParseError{ + Position: p.f.Position(pos), + Text: fmt.Sprintf(format, a...), + }) +} + +func (p *parser) curErr(format string, a ...interface{}) { + p.posErr(p.pos, format, a...) +} + +func (p *parser) stmts(stops ...string) (sts []*Stmt) { + gotEnd := true + for p.tok != _EOF { + switch p.tok { + case _LitWord: + for _, stop := range stops { + if p.val == stop { + return + } + } + case rightParen: + if p.quote == subCmd { + return + } + case bckQuote: + if p.quote == subCmdBckquo { + return + } + case dblSemicolon, semiFall, dblSemiFall: + if p.quote == switchCase { + return + } + p.curErr("%s can only be used in a case clause", p.tok) + } + if !p.newLine && !gotEnd { + p.curErr("statements must be separated by &, ; or a newline") + } + if p.tok == _EOF { + break + } + if s, end := p.getStmt(true, false); s == nil { + p.invalidStmtStart() + } else { + if sts == nil { + sts = p.stList() + } + sts = append(sts, s) + gotEnd = end + } + } + return +} + +func (p *parser) invalidStmtStart() { + switch p.tok { + case semicolon, and, or, andAnd, orOr: + p.curErr("%s can only immediately follow a statement", p.tok) + case rightParen: + p.curErr("%s can only be used to close a subshell", p.tok) + default: + p.curErr("%s is not a valid start for a statement", p.tok) + } +} + +func (p *parser) getWord() *Word { + if p.tok == _LitWord { + w := p.word(p.singleWps(p.lit(p.pos, p.val))) + p.next() + return w + } + if parts := p.wordParts(); len(parts) > 0 { + return p.word(parts) + } + return nil +} + +func (p *parser) getWordOrEmpty() *Word { + parts := p.wordParts() + if len(parts) == 0 { + l := p.lit(p.pos, "") + l.ValueEnd = l.ValuePos // force Lit.Pos() == Lit.End() + return p.word(p.singleWps(l)) + } + return p.word(parts) +} + +func (p *parser) getLit() *Lit { + switch p.tok { + case _Lit, _LitWord, _LitRedir: + l := p.lit(p.pos, p.val) + p.next() + return l + } + return nil +} + +func (p *parser) wordParts() (wps []WordPart) { + for { + n := p.wordPart() + if n == nil { + return + } + if wps == nil { + wps = p.wps() + } + wps = append(wps, n) + if p.spaced { + return + } + } +} + +func (p *parser) ensureNoNested() { + if p.forbidNested { + p.curErr("expansions not allowed in heredoc words") + } +} + +func (p *parser) wordPart() WordPart { + switch p.tok { + case _Lit, _LitWord: + l := p.lit(p.pos, p.val) + p.next() + return l + case dollBrace: + p.ensureNoNested() + return p.paramExp() + case dollDblParen, dollBrack: + p.ensureNoNested() + left := p.tok + ar := &ArithmExp{Left: p.pos, Bracket: left == dollBrack} + var old saveState + if ar.Bracket { + old = p.preNested(arithmExprBrack) + } else { + old = p.preNested(arithmExpr) + } + p.next() + ar.X = p.arithmExpr(left, ar.Left, 0, false, false) + if ar.Bracket { + if p.tok != rightBrack { + p.matchingErr(ar.Left, dollBrack, rightBrack) + } + p.postNested(old) + ar.Right = p.pos + p.next() + } else { + ar.Right = p.arithmEnd(dollDblParen, ar.Left, old) + } + return ar + case dollParen: + p.ensureNoNested() + cs := &CmdSubst{Left: p.pos} + old := p.preNested(subCmd) + p.next() + cs.Stmts = p.stmts() + p.postNested(old) + cs.Right = p.matched(cs.Left, leftParen, rightParen) + return cs + case dollar: + r := p.r + if r == utf8.RuneSelf || wordBreak(r) || r == '"' || r == '\'' || r == '`' || r == '[' { + l := p.lit(p.pos, "$") + p.next() + return l + } + p.ensureNoNested() + pe := &ParamExp{Dollar: p.pos, Short: true} + p.pos++ + switch r { + case '@', '*', '#', '$', '?', '!', '0', '-': + p.rune() + p.tok, p.val = _LitWord, string(r) + default: + if p.quote&allRegTokens != 0 { + p.advanceLitNone(r) + } else { + p.advanceLitOther(r) + } + } + pe.Param = p.getLit() + return pe + case cmdIn, cmdOut: + p.ensureNoNested() + ps := &ProcSubst{Op: ProcOperator(p.tok), OpPos: p.pos} + old := p.preNested(subCmd) + p.next() + ps.Stmts = p.stmts() + p.postNested(old) + ps.Rparen = p.matched(ps.OpPos, token(ps.Op), rightParen) + return ps + case sglQuote: + sq := &SglQuoted{Position: p.pos} + r := p.r + loop: + for p.newLit(r); ; r = p.rune() { + switch r { + case utf8.RuneSelf, '\'': + sq.Value = p.endLit() + p.rune() + break loop + } + } + if r != '\'' { + p.posErr(sq.Pos(), "reached EOF without closing quote %s", sglQuote) + } + p.next() + return sq + case dollSglQuote: + sq := &SglQuoted{Position: p.pos, Dollar: true} + old := p.quote + p.quote = sglQuotes + p.next() + p.quote = old + if p.tok != sglQuote { + sq.Value = p.val + p.next() + } + if !p.got(sglQuote) { + p.quoteErr(sq.Pos(), sglQuote) + } + return sq + case dblQuote: + if p.quote == dblQuotes { + return nil + } + fallthrough + case dollDblQuote: + q := &DblQuoted{Position: p.pos, Dollar: p.tok == dollDblQuote} + old := p.quote + p.quote = dblQuotes + p.next() + if p.tok == _LitWord { + q.Parts = p.singleWps(p.lit(p.pos, p.val)) + p.next() + } else { + q.Parts = p.wordParts() + } + p.quote = old + if !p.got(dblQuote) { + p.quoteErr(q.Pos(), dblQuote) + } + return q + case bckQuote: + if p.quote == subCmdBckquo { + return nil + } + p.ensureNoNested() + cs := &CmdSubst{Left: p.pos} + old := p.preNested(subCmdBckquo) + p.next() + cs.Stmts = p.stmts() + p.postNested(old) + cs.Right = p.pos + if !p.got(bckQuote) { + p.quoteErr(cs.Pos(), bckQuote) + } + return cs + case globQuest, globStar, globPlus, globAt, globExcl: + if !p.bash() { + p.curErr("extended globs are a bash feature") + } + eg := &ExtGlob{Op: GlobOperator(p.tok), OpPos: p.pos} + lparens := 0 + r := p.r + globLoop: + for p.newLit(r); ; r = p.rune() { + switch r { + case utf8.RuneSelf: + break globLoop + case '(': + lparens++ + case ')': + if lparens--; lparens < 0 { + break globLoop + } + } + } + eg.Pattern = p.lit(eg.OpPos+2, p.endLit()) + p.rune() + p.next() + if lparens != -1 { + p.matchingErr(eg.OpPos, eg.Op, rightParen) + } + return eg + default: + return nil + } +} + +func arithmOpLevel(op BinAritOperator) int { + switch op { + case Comma: + return 0 + case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn, + OrAssgn, XorAssgn, ShlAssgn, ShrAssgn: + return 1 + case Assgn: + return 2 + case Quest, Colon: + return 3 + case AndArit, OrArit: + return 4 + case And, Or, Xor: + return 5 + case Eql, Neq: + return 6 + case Lss, Gtr, Leq, Geq: + return 7 + case Shl, Shr: + return 8 + case Add, Sub: + return 9 + case Mul, Quo, Rem: + return 10 + case Pow: + return 11 + } + return -1 +} + +func (p *parser) arithmExpr(ftok token, fpos Pos, level int, compact, tern bool) ArithmExpr { + if p.tok == _EOF || p.peekArithmEnd() { + return nil + } + var left ArithmExpr + if level > 11 { + left = p.arithmExprBase(compact) + } else { + left = p.arithmExpr(ftok, fpos, level+1, compact, false) + } + if compact && p.spaced { + return left + } + newLevel := arithmOpLevel(BinAritOperator(p.tok)) + if !tern && p.tok == colon && p.quote&allParamArith != 0 { + newLevel = -1 + } + if newLevel < 0 { + switch p.tok { + case _Lit, _LitWord: + p.curErr("not a valid arithmetic operator: %s", p.val) + return nil + case rightParen, _EOF: + default: + if p.quote == arithmExpr { + p.curErr("not a valid arithmetic operator: %v", p.tok) + return nil + } + } + } + if newLevel < level { + return left + } + if left == nil { + p.curErr("%s must follow an expression", p.tok.String()) + return nil + } + b := &BinaryArithm{ + OpPos: p.pos, + Op: BinAritOperator(p.tok), + X: left, + } + switch b.Op { + case Colon: + if !tern { + p.posErr(b.Pos(), "ternary operator missing ? before :") + } + case AddAssgn, SubAssgn, MulAssgn, QuoAssgn, RemAssgn, AndAssgn, + OrAssgn, XorAssgn, ShlAssgn, ShrAssgn, Assgn: + if w, ok := b.X.(*Word); !ok || !p.wordIdent(w) { + p.posErr(b.OpPos, "%s must follow a name", b.Op.String()) + } + } + if p.next(); compact && p.spaced { + p.followErrExp(b.OpPos, b.Op.String()) + } + b.Y = p.arithmExpr(token(b.Op), b.OpPos, newLevel, compact, b.Op == Quest) + if b.Y == nil { + p.followErrExp(b.OpPos, b.Op.String()) + } + if b.Op == Quest { + if b2, ok := b.Y.(*BinaryArithm); !ok || b2.Op != Colon { + p.posErr(b.Pos(), "ternary operator missing : after ?") + } + } + return b +} + +func (p *parser) wordIdent(w *Word) bool { + if len(w.Parts) != 1 { + return false + } + lit, ok := w.Parts[0].(*Lit) + return ok && validIdent(lit.Value, p.bash()) +} + +func (p *parser) arithmExprBase(compact bool) ArithmExpr { + var x ArithmExpr + switch p.tok { + case exclMark: + ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} + p.next() + if ue.X = p.arithmExprBase(compact); ue.X == nil { + p.followErrExp(ue.OpPos, ue.Op.String()) + } + return ue + case addAdd, subSub: + ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} + p.next() + ue.X = p.followWordTok(token(ue.Op), ue.OpPos) + return ue + case leftParen: + pe := &ParenArithm{Lparen: p.pos} + p.next() + pe.X = p.arithmExpr(leftParen, pe.Lparen, 0, false, false) + if pe.X == nil { + p.posErr(pe.Lparen, "parentheses must enclose an expression") + } + pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) + x = pe + case plus, minus: + ue := &UnaryArithm{OpPos: p.pos, Op: UnAritOperator(p.tok)} + if p.next(); compact && p.spaced { + p.followErrExp(ue.OpPos, ue.Op.String()) + } + ue.X = p.arithmExpr(token(ue.Op), ue.OpPos, 0, compact, false) + if ue.X == nil { + p.followErrExp(ue.OpPos, ue.Op.String()) + } + x = ue + case bckQuote: + if p.quote == arithmExprLet { + return nil + } + fallthrough + default: + if w := p.getWord(); w != nil { + // we want real nil, not (*Word)(nil) as that + // sets the type to non-nil and then x != nil + x = w + } + } + if compact && p.spaced { + return x + } + if p.tok == addAdd || p.tok == subSub { + if w, ok := x.(*Word); !ok || !p.wordIdent(w) { + p.curErr("%s must follow a name", p.tok.String()) + } + u := &UnaryArithm{ + Post: true, + OpPos: p.pos, + Op: UnAritOperator(p.tok), + X: x, + } + p.next() + return u + } + return x +} + +func (p *parser) paramExp() *ParamExp { + pe := &ParamExp{Dollar: p.pos} + old := p.quote + p.quote = paramExpName + p.next() + switch p.tok { + case at: + p.tok, p.val = _LitWord, "@" + case dblHash: + p.tok = hash + p.unrune('#') + fallthrough + case hash: + if p.r != '}' { + pe.Length = true + p.next() + } + case exclMark: + if p.r != '}' { + pe.Excl = true + p.next() + } + } + switch p.tok { + case _Lit, _LitWord: + pe.Param = p.lit(p.pos, p.val) + p.next() + case hash, exclMark: + pe.Param = p.lit(p.pos, p.tok.String()) + p.next() + case dollar, quest, minus: + op := p.tok + pe.Param = p.lit(p.pos, p.tok.String()) + p.next() + switch p.tok { + case _Lit, _LitWord: + p.curErr("%s cannot be followed by a word", op) + } + default: + if !pe.Length { + p.posErr(pe.Dollar, "parameter expansion requires a literal") + } + } + if p.tok == rightBrace { + pe.Rbrace = p.pos + p.quote = old + p.next() + return pe + } + if p.tok == leftBrack { + if !p.bash() { + p.curErr("arrays are a bash feature") + } + lpos := p.pos + p.quote = paramExpInd + p.next() + switch p.tok { + case star: + p.tok, p.val = _LitWord, "*" + case at: + p.tok, p.val = _LitWord, "@" + } + pe.Ind = &Index{ + Expr: p.arithmExpr(leftBrack, lpos, 0, false, false), + } + if pe.Ind.Expr == nil { + p.followErrExp(lpos, "[") + } + p.quote = paramExpName + p.matched(lpos, leftBrack, rightBrack) + } + switch p.tok { + case rightBrace: + pe.Rbrace = p.pos + p.quote = old + p.next() + return pe + case slash, dblSlash: + if !p.bash() { + p.curErr("search and replace is a bash feature") + } + pe.Repl = &Replace{All: p.tok == dblSlash} + p.quote = paramExpRepl + p.next() + pe.Repl.Orig = p.getWordOrEmpty() + switch p.tok { + case dblSlash: + p.unrune('/') + fallthrough + case slash: + p.quote = paramExpExp + p.next() + } + pe.Repl.With = p.getWordOrEmpty() + case colon: + if !p.bash() { + p.curErr("slicing is a bash feature") + } + pe.Slice = &Slice{} + colonPos := p.pos + p.quote = paramExpOff + if p.next(); p.tok != colon { + pe.Slice.Offset = p.arithmExpr(colon, colonPos, 0, false, false) + if pe.Slice.Offset == nil { + p.followErrExp(colonPos, ":") + } + } + colonPos = p.pos + p.quote = paramExpLen + if p.got(colon) { + pe.Slice.Length = p.arithmExpr(colon, colonPos, 0, false, false) + if pe.Slice.Length == nil { + p.followErrExp(colonPos, ":") + } + } + case caret, dblCaret, comma, dblComma, at: + if !p.bash() { + p.curErr("this expansion operator is a bash feature") + } + fallthrough + default: + pe.Exp = &Expansion{Op: ParExpOperator(p.tok)} + p.quote = paramExpExp + p.next() + pe.Exp.Word = p.getWordOrEmpty() + } + p.quote = old + pe.Rbrace = p.pos + p.matched(pe.Dollar, dollBrace, rightBrace) + return pe +} + +func (p *parser) peekArithmEnd() bool { + return p.tok == rightParen && p.r == ')' +} + +func (p *parser) arithmEnd(ltok token, lpos Pos, old saveState) Pos { + if p.peekArithmEnd() { + p.rune() + } else { + p.matchingErr(lpos, ltok, dblRightParen) + } + p.postNested(old) + pos := p.pos + p.next() + return pos +} + +func stopToken(tok token) bool { + switch tok { + case _EOF, semicolon, and, or, andAnd, orOr, pipeAll, dblSemicolon, + semiFall, dblSemiFall, rightParen: + return true + } + return false +} + +func validIdent(val string, bash bool) bool { + for i, c := range val { + switch { + case 'a' <= c && c <= 'z': + case 'A' <= c && c <= 'Z': + case c == '_': + case i > 0 && '0' <= c && c <= '9': + case i > 0 && (c == '[' || c == ']') && bash: + case c == '+' && i == len(val)-1 && bash: + default: + return false + } + } + return true +} + +func (p *parser) hasValidIdent() bool { + if p.asPos < 1 { + return false + } + return validIdent(p.val[:p.asPos], p.bash()) +} + +func (p *parser) getAssign() *Assign { + as := &Assign{} + nameEnd := p.asPos + if p.bash() && p.val[p.asPos-1] == '+' { + // a+=b + as.Append = true + nameEnd-- + } + as.Name = p.lit(p.pos, p.val[:nameEnd]) + // since we're not using the entire p.val + as.Name.ValueEnd = as.Name.ValuePos + Pos(nameEnd) + start := p.lit(p.pos+1, p.val[p.asPos+1:]) + if start.Value != "" { + start.ValuePos += Pos(p.asPos) + as.Value = p.word(p.singleWps(start)) + } + if p.next(); p.spaced { + return as + } + if start.Value == "" && p.tok == leftParen { + if !p.bash() { + p.curErr("arrays are a bash feature") + } + ae := &ArrayExpr{Lparen: p.pos} + p.next() + for p.tok != _EOF && p.tok != rightParen { + if w := p.getWord(); w == nil { + p.curErr("array elements must be words") + } else { + ae.List = append(ae.List, w) + } + } + ae.Rparen = p.matched(ae.Lparen, leftParen, rightParen) + as.Value = p.word(p.singleWps(ae)) + } else if !p.newLine && !stopToken(p.tok) { + if w := p.getWord(); w != nil { + if as.Value == nil { + as.Value = w + } else { + as.Value.Parts = append(as.Value.Parts, w.Parts...) + } + } + } + return as +} + +func (p *parser) peekRedir() bool { + switch p.tok { + case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, + hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: + return true + } + return false +} + +func (p *parser) doRedirect(s *Stmt) { + r := &Redirect{} + r.N = p.getLit() + r.Op, r.OpPos = RedirOperator(p.tok), p.pos + p.next() + if p.newLine { + p.curErr("redirect word must be on the same line") + } + switch r.Op { + case Hdoc, DashHdoc: + old := p.quote + p.quote, p.forbidNested = hdocWord, true + p.heredocs = append(p.heredocs, r) + r.Word = p.followWordTok(token(r.Op), r.OpPos) + p.quote, p.forbidNested = old, false + if p.tok == illegalTok { + p.next() + } + default: + r.Word = p.followWordTok(token(r.Op), r.OpPos) + } + s.Redirs = append(s.Redirs, r) +} + +func (p *parser) getStmt(readEnd, binCmd bool) (s *Stmt, gotEnd bool) { + s = p.stmt(p.pos) + if p.gotRsrv("!") { + s.Negated = true + } +preLoop: + for { + switch p.tok { + case _Lit, _LitWord: + if p.hasValidIdent() { + s.Assigns = append(s.Assigns, p.getAssign()) + } else { + break preLoop + } + case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, + hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: + p.doRedirect(s) + default: + break preLoop + } + switch { + case p.newLine, p.tok == _EOF: + return + case p.tok == semicolon: + if readEnd { + s.Semicolon = p.pos + p.next() + gotEnd = true + } + return + } + } + if s = p.gotStmtPipe(s); s == nil { + return + } + switch p.tok { + case andAnd, orOr: + // left associativity: in a list of BinaryCmds, the + // right recursion should only read a single element. + if binCmd { + return + } + // and instead of using recursion, iterate manually + for p.tok == andAnd || p.tok == orOr { + b := &BinaryCmd{ + OpPos: p.pos, + Op: BinCmdOperator(p.tok), + X: s, + } + p.next() + if b.Y, _ = p.getStmt(false, true); b.Y == nil { + p.followErr(b.OpPos, b.Op.String(), "a statement") + } + s = p.stmt(s.Position) + s.Cmd = b + } + if readEnd && p.gotSameLine(semicolon) { + gotEnd = true + } + case and: + p.next() + s.Background = true + gotEnd = true + case semicolon: + if !p.newLine && readEnd { + s.Semicolon = p.pos + p.next() + gotEnd = true + } + } + return +} + +func bashDeclareWord(s string) bool { + switch s { + case "declare", "local", "export", "readonly", "typeset", "nameref": + return true + } + return false +} + +func (p *parser) gotStmtPipe(s *Stmt) *Stmt { + switch p.tok { + case leftParen: + s.Cmd = p.subshell() + case dblLeftParen: + s.Cmd = p.arithmExpCmd() + case _LitWord: + switch { + case p.val == "}": + p.curErr("%s can only be used to close a block", p.val) + case p.val == "{": + s.Cmd = p.block() + case p.val == "if": + s.Cmd = p.ifClause() + case p.val == "while": + s.Cmd = p.whileClause() + case p.val == "until": + s.Cmd = p.untilClause() + case p.val == "for": + s.Cmd = p.forClause() + case p.val == "case": + s.Cmd = p.caseClause() + case p.bash() && p.val == "[[": + s.Cmd = p.testClause() + case p.bash() && bashDeclareWord(p.val): + s.Cmd = p.declClause() + case p.bash() && p.val == "eval": + s.Cmd = p.evalClause() + case p.bash() && p.val == "coproc": + s.Cmd = p.coprocClause() + case p.bash() && p.val == "let": + s.Cmd = p.letClause() + case p.bash() && p.val == "function": + s.Cmd = p.bashFuncDecl() + default: + name := p.lit(p.pos, p.val) + if p.next(); p.gotSameLine(leftParen) { + p.follow(name.ValuePos, "foo(", rightParen) + s.Cmd = p.funcDecl(name, name.ValuePos) + } else { + s.Cmd = p.callExpr(s, p.word(p.singleWps(name))) + } + } + case bckQuote: + if p.quote == subCmdBckquo { + return s + } + fallthrough + case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, + globQuest, globStar, globPlus, globAt, globExcl: + w := p.word(p.wordParts()) + if p.gotSameLine(leftParen) && p.err == nil { + p.posErr(w.Pos(), "invalid func name") + } + s.Cmd = p.callExpr(s, w) + } + for !p.newLine && p.peekRedir() { + p.doRedirect(s) + } + if s.Cmd == nil && len(s.Redirs) == 0 && !s.Negated && len(s.Assigns) == 0 { + return nil + } + if p.tok == or || p.tok == pipeAll { + b := &BinaryCmd{OpPos: p.pos, Op: BinCmdOperator(p.tok), X: s} + p.next() + if b.Y = p.gotStmtPipe(p.stmt(p.pos)); b.Y == nil { + p.followErr(b.OpPos, b.Op.String(), "a statement") + } + s = p.stmt(s.Position) + s.Cmd = b + } + return s +} + +func (p *parser) subshell() *Subshell { + s := &Subshell{Lparen: p.pos} + old := p.preNested(subCmd) + p.next() + s.Stmts = p.stmts() + p.postNested(old) + s.Rparen = p.matched(s.Lparen, leftParen, rightParen) + return s +} + +func (p *parser) arithmExpCmd() Command { + ar := &ArithmCmd{Left: p.pos} + old := p.preNested(arithmExprCmd) + p.next() + ar.X = p.arithmExpr(dblLeftParen, ar.Left, 0, false, false) + ar.Right = p.arithmEnd(dblLeftParen, ar.Left, old) + return ar +} + +func (p *parser) block() *Block { + b := &Block{Lbrace: p.pos} + p.next() + b.Stmts = p.stmts("}") + b.Rbrace = p.pos + if !p.gotRsrv("}") { + p.matchingErr(b.Lbrace, "{", "}") + } + return b +} + +func (p *parser) ifClause() *IfClause { + ic := &IfClause{If: p.pos} + p.next() + ic.CondStmts = p.followStmts("if", ic.If, "then") + ic.Then = p.followRsrv(ic.If, "if ", "then") + ic.ThenStmts = p.followStmts("then", ic.Then, "fi", "elif", "else") + for p.tok == _LitWord && p.val == "elif" { + elf := &Elif{Elif: p.pos} + p.next() + elf.CondStmts = p.followStmts("elif", elf.Elif, "then") + elf.Then = p.followRsrv(elf.Elif, "elif ", "then") + elf.ThenStmts = p.followStmts("then", elf.Then, "fi", "elif", "else") + ic.Elifs = append(ic.Elifs, elf) + } + if elsePos := p.pos; p.gotRsrv("else") { + ic.Else = elsePos + ic.ElseStmts = p.followStmts("else", ic.Else, "fi") + } + ic.Fi = p.stmtEnd(ic, "if", "fi") + return ic +} + +func (p *parser) whileClause() *WhileClause { + wc := &WhileClause{While: p.pos} + p.next() + wc.CondStmts = p.followStmts("while", wc.While, "do") + wc.Do = p.followRsrv(wc.While, "while ", "do") + wc.DoStmts = p.followStmts("do", wc.Do, "done") + wc.Done = p.stmtEnd(wc, "while", "done") + return wc +} + +func (p *parser) untilClause() *UntilClause { + uc := &UntilClause{Until: p.pos} + p.next() + uc.CondStmts = p.followStmts("until", uc.Until, "do") + uc.Do = p.followRsrv(uc.Until, "until ", "do") + uc.DoStmts = p.followStmts("do", uc.Do, "done") + uc.Done = p.stmtEnd(uc, "until", "done") + return uc +} + +func (p *parser) forClause() *ForClause { + fc := &ForClause{For: p.pos} + p.next() + fc.Loop = p.loop(fc.For) + fc.Do = p.followRsrv(fc.For, "for foo [in words]", "do") + fc.DoStmts = p.followStmts("do", fc.Do, "done") + fc.Done = p.stmtEnd(fc, "for", "done") + return fc +} + +func (p *parser) loop(forPos Pos) Loop { + if p.tok == dblLeftParen { + cl := &CStyleLoop{Lparen: p.pos} + old := p.preNested(arithmExprCmd) + if p.next(); p.tok == dblSemicolon { + p.unrune(';') + p.tok = semicolon + } + if p.tok != semicolon { + cl.Init = p.arithmExpr(dblLeftParen, cl.Lparen, 0, false, false) + } + scPos := p.pos + if p.tok == dblSemicolon { + p.unrune(';') + p.tok = semicolon + } + p.follow(p.pos, "expression", semicolon) + if p.tok != semicolon { + cl.Cond = p.arithmExpr(semicolon, scPos, 0, false, false) + } + scPos = p.pos + p.follow(p.pos, "expression", semicolon) + if p.tok != semicolon { + cl.Post = p.arithmExpr(semicolon, scPos, 0, false, false) + } + cl.Rparen = p.arithmEnd(dblLeftParen, cl.Lparen, old) + p.gotSameLine(semicolon) + return cl + } + wi := &WordIter{} + if wi.Name = p.getLit(); wi.Name == nil { + p.followErr(forPos, "for", "a literal") + } + if p.gotRsrv("in") { + for !p.newLine && p.tok != _EOF && p.tok != semicolon { + if w := p.getWord(); w == nil { + p.curErr("word list can only contain words") + } else { + wi.List = append(wi.List, w) + } + } + p.gotSameLine(semicolon) + } else if !p.newLine && !p.got(semicolon) { + p.followErr(forPos, "for foo", `"in", ; or a newline`) + } + return wi +} + +func (p *parser) caseClause() *CaseClause { + cc := &CaseClause{Case: p.pos} + p.next() + cc.Word = p.followWord("case", cc.Case) + p.followRsrv(cc.Case, "case x", "in") + cc.List = p.patLists() + cc.Esac = p.stmtEnd(cc, "case", "esac") + return cc +} + +func (p *parser) patLists() (pls []*PatternList) { + for p.tok != _EOF && !(p.tok == _LitWord && p.val == "esac") { + pl := &PatternList{} + p.got(leftParen) + for p.tok != _EOF { + if w := p.getWord(); w == nil { + p.curErr("case patterns must consist of words") + } else { + pl.Patterns = append(pl.Patterns, w) + } + if p.tok == rightParen { + break + } + if !p.got(or) { + p.curErr("case patterns must be separated with |") + } + } + old := p.preNested(switchCase) + p.next() + pl.Stmts = p.stmts("esac") + p.postNested(old) + pl.OpPos = p.pos + if p.tok != dblSemicolon && p.tok != semiFall && p.tok != dblSemiFall { + pl.Op = DblSemicolon + pls = append(pls, pl) + break + } + pl.Op = CaseOperator(p.tok) + p.next() + pls = append(pls, pl) + } + return +} + +func (p *parser) testClause() *TestClause { + tc := &TestClause{Left: p.pos} + if p.next(); p.tok == _EOF || p.gotRsrv("]]") { + p.posErr(tc.Left, "test clause requires at least one expression") + } + tc.X = p.testExpr(illegalTok, tc.Left, 0) + tc.Right = p.pos + if !p.gotRsrv("]]") { + p.matchingErr(tc.Left, "[[", "]]") + } + return tc +} + +func (p *parser) testExpr(ftok token, fpos Pos, level int) TestExpr { + var left TestExpr + if level > 1 { + left = p.testExprBase(ftok, fpos) + } else { + left = p.testExpr(ftok, fpos, level+1) + } + if left == nil { + return left + } + var newLevel int + switch p.tok { + case andAnd, orOr: + case _LitWord: + if p.val == "]]" { + return left + } + fallthrough + case rdrIn, rdrOut: + newLevel = 1 + case _EOF, rightParen: + return left + case _Lit: + p.curErr("not a valid test operator: %s", p.val) + default: + p.curErr("not a valid test operator: %v", p.tok) + } + if newLevel < level { + return left + } + if p.tok == _LitWord { + if p.tok = testBinaryOp(p.val); p.tok == illegalTok { + p.curErr("not a valid test operator: %s", p.val) + } + } + b := &BinaryTest{ + OpPos: p.pos, + Op: BinTestOperator(p.tok), + X: left, + } + switch b.Op { + case AndTest, OrTest: + p.next() + if b.Y = p.testExpr(token(b.Op), b.OpPos, newLevel); b.Y == nil { + p.followErrExp(b.OpPos, b.Op.String()) + } + case TsReMatch: + old := p.preNested(testRegexp) + defer p.postNested(old) + fallthrough + default: + if _, ok := b.X.(*Word); !ok { + p.posErr(b.OpPos, "expected %s, %s or %s after complex expr", + AndTest, OrTest, "]]") + } + p.next() + b.Y = p.followWordTok(token(b.Op), b.OpPos) + } + return b +} + +func (p *parser) testExprBase(ftok token, fpos Pos) TestExpr { + switch p.tok { + case _EOF: + return nil + case _LitWord: + if op := testUnaryOp(p.val); op != illegalTok { + p.tok = op + } + } + switch p.tok { + case exclMark: + u := &UnaryTest{OpPos: p.pos, Op: TsNot} + p.next() + u.X = p.testExpr(token(u.Op), u.OpPos, 0) + return u + case tsExists, tsRegFile, tsDirect, tsCharSp, tsBlckSp, tsNmPipe, + tsSocket, tsSmbLink, tsSticky, tsGIDSet, tsUIDSet, tsGrpOwn, + tsUsrOwn, tsModif, tsRead, tsWrite, tsExec, tsNoEmpty, + tsFdTerm, tsEmpStr, tsNempStr, tsOptSet, tsVarSet, tsRefVar: + u := &UnaryTest{OpPos: p.pos, Op: UnTestOperator(p.tok)} + p.next() + u.X = p.followWordTok(ftok, fpos) + return u + case leftParen: + pe := &ParenTest{Lparen: p.pos} + p.next() + if pe.X = p.testExpr(leftParen, pe.Lparen, 0); pe.X == nil { + p.posErr(pe.Lparen, "parentheses must enclose an expression") + } + pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen) + return pe + case rightParen: + return nil + default: + // since we don't have [[ as a token + fstr := "[[" + if ftok != illegalTok { + fstr = ftok.String() + } + return p.followWord(fstr, fpos) + } +} + +func (p *parser) declClause() *DeclClause { + name := p.val + ds := &DeclClause{Position: p.pos} + switch name { + case "declare", "typeset": // typeset is an obsolete synonym + default: + ds.Variant = name + } + p.next() + for p.tok == _LitWord && p.val[0] == '-' { + ds.Opts = append(ds.Opts, p.getWord()) + } + for !p.newLine && !stopToken(p.tok) && !p.peekRedir() { + if (p.tok == _Lit || p.tok == _LitWord) && p.hasValidIdent() { + ds.Assigns = append(ds.Assigns, p.getAssign()) + } else if w := p.getWord(); w == nil { + p.followErr(p.pos, name, "words") + } else { + ds.Assigns = append(ds.Assigns, &Assign{Value: w}) + } + } + return ds +} + +func (p *parser) evalClause() *EvalClause { + ec := &EvalClause{Eval: p.pos} + p.next() + ec.Stmt, _ = p.getStmt(false, false) + return ec +} + +func isBashCompoundCommand(tok token, val string) bool { + switch tok { + case leftParen, dblLeftParen: + return true + case _LitWord: + switch val { + case "{", "if", "while", "until", "for", "case", "[[", "eval", + "coproc", "let", "function": + return true + } + if bashDeclareWord(val) { + return true + } + } + return false +} + +func (p *parser) coprocClause() *CoprocClause { + cc := &CoprocClause{Coproc: p.pos} + if p.next(); isBashCompoundCommand(p.tok, p.val) { + // has no name + cc.Stmt, _ = p.getStmt(false, false) + return cc + } + if p.newLine { + p.posErr(cc.Coproc, "coproc clause requires a command") + } + cc.Name = p.getLit() + cc.Stmt, _ = p.getStmt(false, false) + if cc.Stmt == nil { + if cc.Name == nil { + p.posErr(cc.Coproc, "coproc clause requires a command") + return nil + } + // name was in fact the stmt + cc.Stmt = p.stmt(cc.Name.ValuePos) + cc.Stmt.Cmd = p.call(p.word(p.singleWps(cc.Name))) + cc.Name = nil + } else if cc.Name != nil { + if call, ok := cc.Stmt.Cmd.(*CallExpr); ok { + // name was in fact the start of a call + call.Args = append([]*Word{p.word(p.singleWps(cc.Name))}, + call.Args...) + cc.Name = nil + } + } + return cc +} + +func (p *parser) letClause() *LetClause { + lc := &LetClause{Let: p.pos} + old := p.preNested(arithmExprLet) + p.next() + for !p.newLine && !stopToken(p.tok) && !p.peekRedir() { + x := p.arithmExpr(illegalTok, lc.Let, 0, true, false) + if x == nil { + break + } + lc.Exprs = append(lc.Exprs, x) + } + if len(lc.Exprs) == 0 { + p.posErr(lc.Let, "let clause requires at least one expression") + } + p.postNested(old) + if p.tok == illegalTok { + p.next() + } + return lc +} + +func (p *parser) bashFuncDecl() *FuncDecl { + fpos := p.pos + p.next() + if p.tok != _LitWord { + if w := p.followWord("function", fpos); p.err == nil { + p.posErr(w.Pos(), "invalid func name") + } + } + name := p.lit(p.pos, p.val) + if p.next(); p.gotSameLine(leftParen) { + p.follow(name.ValuePos, "foo(", rightParen) + } + return p.funcDecl(name, fpos) +} + +func (p *parser) callExpr(s *Stmt, w *Word) *CallExpr { + ce := p.call(w) + for !p.newLine { + switch p.tok { + case _EOF, semicolon, and, or, andAnd, orOr, pipeAll, + dblSemicolon, semiFall, dblSemiFall: + return ce + case _LitWord: + ce.Args = append(ce.Args, p.word( + p.singleWps(p.lit(p.pos, p.val)), + )) + p.next() + case bckQuote: + if p.quote == subCmdBckquo { + return ce + } + fallthrough + case _Lit, dollBrace, dollDblParen, dollParen, dollar, cmdIn, cmdOut, + sglQuote, dollSglQuote, dblQuote, dollDblQuote, dollBrack, + globQuest, globStar, globPlus, globAt, globExcl: + ce.Args = append(ce.Args, p.word(p.wordParts())) + case rdrOut, appOut, rdrIn, dplIn, dplOut, clbOut, rdrInOut, + hdoc, dashHdoc, wordHdoc, rdrAll, appAll, _LitRedir: + p.doRedirect(s) + case dblLeftParen: + p.curErr("%s can only be used to open an arithmetic cmd", p.tok) + case rightParen: + if p.quote == subCmd { + return ce + } + fallthrough + default: + p.curErr("a command can only contain words and redirects") + } + } + return ce +} + +func (p *parser) funcDecl(name *Lit, pos Pos) *FuncDecl { + fd := &FuncDecl{ + Position: pos, + BashStyle: pos != name.ValuePos, + Name: name, + } + if fd.Body, _ = p.getStmt(false, false); fd.Body == nil { + p.followErr(fd.Pos(), "foo()", "a statement") + } + return fd +} diff --git a/vendor/github.com/mvdan/sh/syntax/printer.go b/vendor/github.com/mvdan/sh/syntax/printer.go new file mode 100644 index 00000000..dac6242f --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/printer.go @@ -0,0 +1,904 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +import ( + "bufio" + "io" + "sync" +) + +// PrintConfig controls how the printing of an AST node will behave. +type PrintConfig struct { + Spaces int // 0 (default) for tabs, >0 for number of spaces +} + +var printerFree = sync.Pool{ + New: func() interface{} { + return &printer{ + bufWriter: bufio.NewWriter(nil), + lenPrinter: new(printer), + } + }, +} + +// Fprint "pretty-prints" the given AST file to the given writer. +func (c PrintConfig) Fprint(w io.Writer, f *File) error { + p := printerFree.Get().(*printer) + p.reset() + p.PrintConfig = c + p.lines, p.comments = f.lines, f.Comments + p.bufWriter.Reset(w) + p.stmts(f.Stmts) + p.commentsUpTo(0) + p.newline(0) + var err error + if flusher, ok := p.bufWriter.(interface { + Flush() error + }); ok { + err = flusher.Flush() + } + printerFree.Put(p) + return err +} + +// Fprint "pretty-prints" the given AST file to the given writer. It +// calls PrintConfig.Fprint with its default settings. +func Fprint(w io.Writer, f *File) error { + return PrintConfig{}.Fprint(w, f) +} + +type bufWriter interface { + WriteByte(byte) error + WriteString(string) (int, error) + Reset(io.Writer) +} + +type printer struct { + bufWriter + + PrintConfig + lines []Pos + + wantSpace bool + wantNewline bool + wroteSemi bool + + commentPadding int + + // nline is the position of the next newline + nline Pos + nlineIndex int + + // lastLevel is the last level of indentation that was used. + lastLevel int + // level is the current level of indentation. + level int + // levelIncs records which indentation level increments actually + // took place, to revert them once their section ends. + levelIncs []bool + + nestedBinary bool + + // comments is the list of pending comments to write. + comments []*Comment + + // pendingHdocs is the list of pending heredocs to write. + pendingHdocs []*Redirect + + // used in stmtLen to align comments + lenPrinter *printer + lenCounter byteCounter +} + +func (p *printer) reset() { + p.wantSpace, p.wantNewline = false, false + p.commentPadding = 0 + p.nline, p.nlineIndex = 0, 0 + p.lastLevel, p.level = 0, 0 + p.levelIncs = p.levelIncs[:0] + p.nestedBinary = false + p.pendingHdocs = p.pendingHdocs[:0] +} + +func (p *printer) incLine() { + if p.nlineIndex++; p.nlineIndex >= len(p.lines) { + p.nline = maxPos + } else { + p.nline = p.lines[p.nlineIndex] + } +} + +func (p *printer) incLines(pos Pos) { + for p.nline < pos { + p.incLine() + } +} + +func (p *printer) spaces(n int) { + for i := 0; i < n; i++ { + p.WriteByte(' ') + } +} + +func (p *printer) bslashNewl() { + if p.wantSpace { + p.WriteByte(' ') + } + p.WriteString("\\\n") + p.wantSpace = false + p.incLine() +} + +func (p *printer) spacedString(s string) { + if p.wantSpace { + p.WriteByte(' ') + } + p.WriteString(s) + p.wantSpace = true +} + +func (p *printer) semiOrNewl(s string, pos Pos) { + if p.wantNewline { + p.newline(pos) + p.indent() + } else { + if !p.wroteSemi { + p.WriteByte(';') + } + p.WriteByte(' ') + p.incLines(pos) + } + p.WriteString(s) + p.wantSpace = true +} + +func (p *printer) incLevel() { + inc := false + if p.level <= p.lastLevel || len(p.levelIncs) == 0 { + p.level++ + inc = true + } else if last := &p.levelIncs[len(p.levelIncs)-1]; *last { + *last = false + inc = true + } + p.levelIncs = append(p.levelIncs, inc) +} + +func (p *printer) decLevel() { + if p.levelIncs[len(p.levelIncs)-1] { + p.level-- + } + p.levelIncs = p.levelIncs[:len(p.levelIncs)-1] +} + +func (p *printer) indent() { + p.lastLevel = p.level + switch { + case p.level == 0: + case p.Spaces == 0: + for i := 0; i < p.level; i++ { + p.WriteByte('\t') + } + case p.Spaces > 0: + p.spaces(p.Spaces * p.level) + } +} + +func (p *printer) newline(pos Pos) { + p.wantNewline, p.wantSpace = false, false + p.WriteByte('\n') + if pos > p.nline { + p.incLine() + } + hdocs := p.pendingHdocs + p.pendingHdocs = p.pendingHdocs[:0] + for _, r := range hdocs { + p.word(r.Hdoc) + p.incLines(r.Hdoc.End()) + p.unquotedWord(r.Word) + p.WriteByte('\n') + p.incLine() + p.wantSpace = false + } +} + +func (p *printer) newlines(pos Pos) { + p.newline(pos) + if pos > p.nline { + // preserve single empty lines + p.WriteByte('\n') + p.incLine() + } + p.indent() +} + +func (p *printer) commentsAndSeparate(pos Pos) { + p.commentsUpTo(pos) + if p.wantNewline || pos > p.nline { + p.newlines(pos) + } +} + +func (p *printer) sepTok(s string, pos Pos) { + p.level++ + p.commentsUpTo(pos) + p.level-- + if p.wantNewline || pos > p.nline { + p.newlines(pos) + } + p.WriteString(s) + p.wantSpace = true +} + +func (p *printer) semiRsrv(s string, pos Pos, fallback bool) { + p.level++ + p.commentsUpTo(pos) + p.level-- + if p.wantNewline || pos > p.nline { + p.newlines(pos) + } else if fallback { + if !p.wroteSemi { + p.WriteByte(';') + } + p.WriteByte(' ') + } else if p.wantSpace { + p.WriteByte(' ') + } + p.WriteString(s) + p.wantSpace = true +} + +func (p *printer) anyCommentsBefore(pos Pos) bool { + if !pos.IsValid() || len(p.comments) < 1 { + return false + } + return p.comments[0].Hash < pos +} + +func (p *printer) commentsUpTo(pos Pos) { + if len(p.comments) < 1 { + return + } + c := p.comments[0] + if pos.IsValid() && c.Hash >= pos { + return + } + p.comments = p.comments[1:] + switch { + case p.nlineIndex == 0: + case c.Hash > p.nline: + p.newlines(c.Hash) + case p.wantSpace: + p.spaces(p.commentPadding + 1) + } + p.incLines(c.Hash) + p.WriteByte('#') + p.WriteString(c.Text) + p.commentsUpTo(pos) +} + +func (p *printer) wordPart(wp WordPart) { + switch x := wp.(type) { + case *Lit: + p.WriteString(x.Value) + case *SglQuoted: + if x.Dollar { + p.WriteByte('$') + } + p.WriteByte('\'') + p.WriteString(x.Value) + p.WriteByte('\'') + p.incLines(x.End()) + case *DblQuoted: + if x.Dollar { + p.WriteByte('$') + } + p.WriteByte('"') + for i, n := range x.Parts { + p.wordPart(n) + if i == len(x.Parts)-1 { + p.incLines(n.End()) + } + } + p.WriteByte('"') + case *CmdSubst: + p.incLines(x.Pos()) + p.WriteString("$(") + p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) + p.nestedStmts(x.Stmts, x.Right) + p.sepTok(")", x.Right) + case *ParamExp: + if x.Short { + p.WriteByte('$') + p.WriteString(x.Param.Value) + break + } + p.WriteString("${") + switch { + case x.Length: + p.WriteByte('#') + case x.Excl: + p.WriteByte('!') + } + if x.Param != nil { + p.WriteString(x.Param.Value) + } + if x.Ind != nil { + p.WriteByte('[') + p.arithmExpr(x.Ind.Expr, false) + p.WriteByte(']') + } + if x.Slice != nil { + p.WriteByte(':') + if un, ok := x.Slice.Offset.(*UnaryArithm); ok { + if un.Op == Plus || un.Op == Minus { + // to avoid :+ and :- + p.WriteByte(' ') + } + } + p.arithmExpr(x.Slice.Offset, true) + if x.Slice.Length != nil { + p.WriteByte(':') + p.arithmExpr(x.Slice.Length, true) + } + } else if x.Repl != nil { + if x.Repl.All { + p.WriteByte('/') + } + p.WriteByte('/') + p.word(x.Repl.Orig) + p.WriteByte('/') + p.word(x.Repl.With) + } else if x.Exp != nil { + p.WriteString(x.Exp.Op.String()) + p.word(x.Exp.Word) + } + p.WriteByte('}') + case *ArithmExp: + p.WriteString("$((") + p.arithmExpr(x.X, false) + p.WriteString("))") + case *ArrayExpr: + p.wantSpace = false + p.WriteByte('(') + p.wordJoin(x.List, false) + p.sepTok(")", x.Rparen) + case *ExtGlob: + p.WriteString(x.Op.String()) + p.WriteString(x.Pattern.Value) + p.WriteByte(')') + case *ProcSubst: + // avoid conflict with << and others + if p.wantSpace { + p.WriteByte(' ') + p.wantSpace = false + } + p.WriteString(x.Op.String()) + p.nestedStmts(x.Stmts, 0) + p.WriteByte(')') + } +} + +func (p *printer) loop(loop Loop) { + switch x := loop.(type) { + case *WordIter: + p.WriteString(x.Name.Value) + if len(x.List) > 0 { + p.spacedString(" in") + p.wordJoin(x.List, true) + } + case *CStyleLoop: + p.WriteString("((") + if x.Init == nil { + p.WriteByte(' ') + } + p.arithmExpr(x.Init, false) + p.WriteString("; ") + p.arithmExpr(x.Cond, false) + p.WriteString("; ") + p.arithmExpr(x.Post, false) + p.WriteString("))") + } +} + +func (p *printer) arithmExpr(expr ArithmExpr, compact bool) { + switch x := expr.(type) { + case *Word: + p.word(x) + case *BinaryArithm: + if compact { + p.arithmExpr(x.X, compact) + p.WriteString(x.Op.String()) + p.arithmExpr(x.Y, compact) + } else { + p.arithmExpr(x.X, compact) + if x.Op != Comma { + p.WriteByte(' ') + } + p.WriteString(x.Op.String()) + p.WriteByte(' ') + p.arithmExpr(x.Y, compact) + } + case *UnaryArithm: + if x.Post { + p.arithmExpr(x.X, compact) + p.WriteString(x.Op.String()) + } else { + p.WriteString(x.Op.String()) + p.arithmExpr(x.X, compact) + } + case *ParenArithm: + p.WriteByte('(') + p.arithmExpr(x.X, false) + p.WriteByte(')') + } +} + +func (p *printer) testExpr(expr TestExpr) { + switch x := expr.(type) { + case *Word: + p.word(x) + case *BinaryTest: + p.testExpr(x.X) + p.WriteByte(' ') + p.WriteString(x.Op.String()) + p.WriteByte(' ') + p.testExpr(x.Y) + case *UnaryTest: + p.WriteString(x.Op.String()) + p.WriteByte(' ') + p.testExpr(x.X) + case *ParenTest: + p.WriteByte('(') + p.testExpr(x.X) + p.WriteByte(')') + } +} + +func (p *printer) word(w *Word) { + for _, n := range w.Parts { + p.wordPart(n) + } + p.wantSpace = true +} + +func (p *printer) unquotedWord(w *Word) { + for _, wp := range w.Parts { + switch x := wp.(type) { + case *SglQuoted: + p.WriteString(x.Value) + case *DblQuoted: + for _, qp := range x.Parts { + p.wordPart(qp) + } + case *Lit: + for i := 0; i < len(x.Value); i++ { + if b := x.Value[i]; b == '\\' { + if i++; i < len(x.Value) { + p.WriteByte(x.Value[i]) + } + } else { + p.WriteByte(b) + } + } + } + } +} + +func (p *printer) wordJoin(ws []*Word, backslash bool) { + anyNewline := false + for _, w := range ws { + if pos := w.Pos(); pos > p.nline { + p.commentsUpTo(pos) + if backslash { + p.bslashNewl() + } else { + p.WriteByte('\n') + p.incLine() + } + if !anyNewline { + p.incLevel() + anyNewline = true + } + p.indent() + } else if p.wantSpace { + p.WriteByte(' ') + p.wantSpace = false + } + p.word(w) + } + if anyNewline { + p.decLevel() + } +} + +func (p *printer) stmt(s *Stmt) { + if s.Negated { + p.spacedString("!") + } + p.assigns(s.Assigns) + var startRedirs int + if s.Cmd != nil { + startRedirs = p.command(s.Cmd, s.Redirs) + } + anyNewline := false + for _, r := range s.Redirs[startRedirs:] { + if r.OpPos > p.nline { + p.bslashNewl() + if !anyNewline { + p.incLevel() + anyNewline = true + } + p.indent() + } + p.commentsAndSeparate(r.OpPos) + if p.wantSpace { + p.WriteByte(' ') + } + if r.N != nil { + p.WriteString(r.N.Value) + } + p.WriteString(r.Op.String()) + p.word(r.Word) + if r.Op == Hdoc || r.Op == DashHdoc { + p.pendingHdocs = append(p.pendingHdocs, r) + } + } + p.wroteSemi = false + if s.Semicolon.IsValid() && s.Semicolon > p.nline { + p.incLevel() + p.bslashNewl() + p.indent() + p.decLevel() + p.WriteByte(';') + p.wroteSemi = true + } else if s.Background { + p.WriteString(" &") + } + if anyNewline { + p.decLevel() + } +} + +func (p *printer) command(cmd Command, redirs []*Redirect) (startRedirs int) { + if p.wantSpace { + p.WriteByte(' ') + p.wantSpace = false + } + switch x := cmd.(type) { + case *CallExpr: + if len(x.Args) <= 1 { + p.wordJoin(x.Args, true) + return 0 + } + p.wordJoin(x.Args[:1], true) + for _, r := range redirs { + if r.Pos() > x.Args[1].Pos() || r.Op == Hdoc || r.Op == DashHdoc { + break + } + if p.wantSpace { + p.WriteByte(' ') + } + if r.N != nil { + p.WriteString(r.N.Value) + } + p.WriteString(r.Op.String()) + p.word(r.Word) + startRedirs++ + } + p.wordJoin(x.Args[1:], true) + case *Block: + p.WriteByte('{') + p.wantSpace = true + p.nestedStmts(x.Stmts, x.Rbrace) + p.semiRsrv("}", x.Rbrace, true) + case *IfClause: + p.spacedString("if") + p.nestedStmts(x.CondStmts, 0) + p.semiOrNewl("then", x.Then) + p.nestedStmts(x.ThenStmts, 0) + for _, el := range x.Elifs { + p.semiRsrv("elif", el.Elif, true) + p.nestedStmts(el.CondStmts, 0) + p.semiOrNewl("then", el.Then) + p.nestedStmts(el.ThenStmts, 0) + } + if len(x.ElseStmts) > 0 { + p.semiRsrv("else", x.Else, true) + p.nestedStmts(x.ElseStmts, 0) + } else if x.Else.IsValid() { + p.incLines(x.Else) + } + p.semiRsrv("fi", x.Fi, true) + case *Subshell: + p.WriteByte('(') + p.wantSpace = len(x.Stmts) > 0 && startsWithLparen(x.Stmts[0]) + p.nestedStmts(x.Stmts, x.Rparen) + p.sepTok(")", x.Rparen) + case *WhileClause: + p.spacedString("while") + p.nestedStmts(x.CondStmts, 0) + p.semiOrNewl("do", x.Do) + p.nestedStmts(x.DoStmts, 0) + p.semiRsrv("done", x.Done, true) + case *ForClause: + p.WriteString("for ") + p.loop(x.Loop) + p.semiOrNewl("do", x.Do) + p.nestedStmts(x.DoStmts, 0) + p.semiRsrv("done", x.Done, true) + case *BinaryCmd: + p.stmt(x.X) + indent := !p.nestedBinary + if indent { + p.incLevel() + } + _, p.nestedBinary = x.Y.Cmd.(*BinaryCmd) + if len(p.pendingHdocs) == 0 && x.Y.Pos() > p.nline { + p.bslashNewl() + p.indent() + } + p.spacedString(x.Op.String()) + if p.anyCommentsBefore(x.Y.Pos()) { + p.wantSpace = false + p.WriteByte('\n') + p.indent() + p.incLines(p.comments[0].Pos()) + p.commentsUpTo(x.Y.Pos()) + p.WriteByte('\n') + p.indent() + } + p.incLines(x.Y.Pos()) + p.stmt(x.Y) + if indent { + p.decLevel() + } + p.nestedBinary = false + case *FuncDecl: + if x.BashStyle { + p.WriteString("function ") + } + p.WriteString(x.Name.Value) + p.WriteString("() ") + p.incLines(x.Body.Pos()) + p.stmt(x.Body) + case *CaseClause: + p.WriteString("case ") + p.word(x.Word) + p.WriteString(" in") + p.incLevel() + for _, pl := range x.List { + p.commentsAndSeparate(pl.Patterns[0].Pos()) + for i, w := range pl.Patterns { + if i > 0 { + p.spacedString("|") + } + if p.wantSpace { + p.WriteByte(' ') + } + p.word(w) + } + p.WriteByte(')') + p.wantSpace = true + sep := len(pl.Stmts) > 1 || (len(pl.Stmts) > 0 && pl.Stmts[0].Pos() > p.nline) + p.nestedStmts(pl.Stmts, 0) + p.level++ + if sep { + p.commentsUpTo(pl.OpPos) + p.newlines(pl.OpPos) + } + p.spacedString(pl.Op.String()) + p.incLines(pl.OpPos) + p.level-- + if sep || pl.OpPos == x.Esac { + p.wantNewline = true + } + } + p.decLevel() + p.semiRsrv("esac", x.Esac, len(x.List) == 0) + case *UntilClause: + p.spacedString("until") + p.nestedStmts(x.CondStmts, 0) + p.semiOrNewl("do", x.Do) + p.nestedStmts(x.DoStmts, 0) + p.semiRsrv("done", x.Done, true) + case *ArithmCmd: + p.WriteString("((") + p.arithmExpr(x.X, false) + p.WriteString("))") + case *TestClause: + p.WriteString("[[ ") + p.testExpr(x.X) + p.spacedString("]]") + case *DeclClause: + name := x.Variant + if name == "" { + name = "declare" + } + p.spacedString(name) + for _, w := range x.Opts { + p.WriteByte(' ') + p.word(w) + } + p.assigns(x.Assigns) + case *EvalClause: + p.spacedString("eval") + if x.Stmt != nil { + p.stmt(x.Stmt) + } + case *CoprocClause: + p.spacedString("coproc") + if x.Name != nil { + p.WriteByte(' ') + p.WriteString(x.Name.Value) + } + p.stmt(x.Stmt) + case *LetClause: + p.spacedString("let") + for _, n := range x.Exprs { + p.WriteByte(' ') + p.arithmExpr(n, true) + } + } + return startRedirs +} + +func startsWithLparen(s *Stmt) bool { + switch x := s.Cmd.(type) { + case *Subshell: + return true + case *BinaryCmd: + return startsWithLparen(x.X) + } + return false +} + +func (p *printer) hasInline(pos, npos, nline Pos) bool { + for _, c := range p.comments { + if c.Hash > nline { + return false + } + if c.Hash > pos && (npos == 0 || c.Hash < npos) { + return true + } + } + return false +} + +func (p *printer) stmts(stmts []*Stmt) { + switch len(stmts) { + case 0: + return + case 1: + s := stmts[0] + pos := s.Pos() + p.commentsUpTo(pos) + if pos <= p.nline { + p.stmt(s) + } else { + if p.nlineIndex > 0 { + p.newlines(pos) + } + p.incLines(pos) + p.stmt(s) + p.wantNewline = true + } + return + } + inlineIndent := 0 + for i, s := range stmts { + pos := s.Pos() + ind := p.nlineIndex + p.commentsUpTo(pos) + if p.nlineIndex > 0 { + p.newlines(pos) + } + p.incLines(pos) + p.stmt(s) + var npos Pos + if i+1 < len(stmts) { + npos = stmts[i+1].Pos() + } + if !p.hasInline(pos, npos, p.nline) { + inlineIndent = 0 + p.commentPadding = 0 + continue + } + if ind < len(p.lines)-1 && s.End() > p.lines[ind+1] { + inlineIndent = 0 + } + if inlineIndent == 0 { + ind2 := p.nlineIndex + nline2 := p.nline + follow := stmts[i:] + for j, s2 := range follow { + pos2 := s2.Pos() + var npos2 Pos + if j+1 < len(follow) { + npos2 = follow[j+1].Pos() + } + if pos2 > nline2 || !p.hasInline(pos2, npos2, nline2) { + break + } + if l := p.stmtLen(s2); l > inlineIndent { + inlineIndent = l + } + if ind2++; ind2 >= len(p.lines) { + nline2 = maxPos + } else { + nline2 = p.lines[ind2] + } + } + if ind2 == p.nlineIndex+1 { + // no inline comments directly after this one + continue + } + } + if inlineIndent > 0 { + p.commentPadding = inlineIndent - p.stmtLen(s) + } + } + p.wantNewline = true +} + +type byteCounter int + +func (c *byteCounter) WriteByte(b byte) error { + *c++ + return nil +} +func (c *byteCounter) WriteString(s string) (int, error) { + *c += byteCounter(len(s)) + return 0, nil +} +func (c *byteCounter) Reset(io.Writer) { *c = 0 } + +func (p *printer) stmtLen(s *Stmt) int { + *p.lenPrinter = printer{bufWriter: &p.lenCounter} + p.lenPrinter.bufWriter.Reset(nil) + p.lenPrinter.incLines(s.Pos()) + p.lenPrinter.stmt(s) + return int(p.lenCounter) +} + +func (p *printer) nestedStmts(stmts []*Stmt, closing Pos) { + p.incLevel() + if len(stmts) == 1 && closing > p.nline && stmts[0].End() <= p.nline { + p.newline(0) + p.indent() + } + p.stmts(stmts) + p.decLevel() +} + +func (p *printer) assigns(assigns []*Assign) { + anyNewline := false + for _, a := range assigns { + if a.Pos() > p.nline { + p.bslashNewl() + if !anyNewline { + p.incLevel() + anyNewline = true + } + p.indent() + } else if p.wantSpace { + p.WriteByte(' ') + } + if a.Name != nil { + p.WriteString(a.Name.Value) + if a.Append { + p.WriteByte('+') + } + p.WriteByte('=') + } + if a.Value != nil { + p.word(a.Value) + } + p.wantSpace = true + } + if anyNewline { + p.decLevel() + } +} diff --git a/vendor/github.com/mvdan/sh/syntax/token_string.go b/vendor/github.com/mvdan/sh/syntax/token_string.go new file mode 100644 index 00000000..4f37c872 --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/token_string.go @@ -0,0 +1,16 @@ +// Code generated by "stringer -type token"; DO NOT EDIT + +package syntax + +import "fmt" + +const _token_name = "illegalTokEOFLitLitWordLitRedir'\"`&&&||||&$$'$\"${$[$($(([(((}])));;;;&;;&!++--***==!=<=>=+=-=*=/=%=&=|=^=<<=>>=>>><<><&>&>|<<<<-<<<&>&>><(>(+:+-:-?:?=:=%%%###^^^,,,@///:-e-f-d-c-b-p-S-L-k-g-u-G-O-N-r-w-x-s-t-z-n-o-v-R=~-nt-ot-ef-eq-ne-le-ge-lt-gt?(*(+(@(!(" + +var _token_index = [...]uint16{0, 10, 13, 16, 23, 31, 32, 33, 34, 35, 37, 39, 40, 42, 43, 45, 47, 49, 51, 53, 56, 57, 58, 60, 61, 62, 63, 65, 66, 68, 70, 73, 74, 76, 78, 79, 81, 83, 85, 87, 89, 91, 93, 95, 97, 99, 101, 103, 105, 108, 111, 112, 114, 115, 117, 119, 121, 123, 125, 128, 131, 133, 136, 138, 140, 141, 143, 144, 146, 147, 149, 150, 152, 153, 155, 156, 158, 159, 161, 162, 164, 165, 166, 168, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 222, 225, 228, 231, 234, 237, 240, 243, 246, 248, 250, 252, 254, 256} + +func (i token) String() string { + if i >= token(len(_token_index)-1) { + return fmt.Sprintf("token(%d)", i) + } + return _token_name[_token_index[i]:_token_index[i+1]] +} diff --git a/vendor/github.com/mvdan/sh/syntax/tokens.go b/vendor/github.com/mvdan/sh/syntax/tokens.go new file mode 100644 index 00000000..26bf6a82 --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/tokens.go @@ -0,0 +1,340 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +type token uint32 + +// Modified version of golang.org/x/tools/cmd/stringer that gets the +// string value from the inline comment of each constant, if there is +// one. Also removes leading '_'. +//go:generate stringer -type token + +// The list of all possible tokens. +const ( + illegalTok token = iota + _EOF + _Lit + _LitWord + _LitRedir + + sglQuote // ' + dblQuote // " + bckQuote // ` + + and // & + andAnd // && + orOr // || + or // | + pipeAll // |& + + dollar // $ + dollSglQuote // $' + dollDblQuote // $" + dollBrace // ${ + dollBrack // $[ + dollParen // $( + dollDblParen // $(( + leftBrack // [ + leftParen // ( + dblLeftParen // (( + + rightBrace // } + rightBrack // ] + rightParen // ) + dblRightParen // )) + semicolon // ; + + dblSemicolon // ;; + semiFall // ;& + dblSemiFall // ;;& + + exclMark // ! + addAdd // ++ + subSub // -- + star // * + power // ** + equal // == + nequal // != + lequal // <= + gequal // >= + + addAssgn // += + subAssgn // -= + mulAssgn // *= + quoAssgn // /= + remAssgn // %= + andAssgn // &= + orAssgn // |= + xorAssgn // ^= + shlAssgn // <<= + shrAssgn // >>= + + rdrOut // > + appOut // >> + rdrIn // < + rdrInOut // <> + dplIn // <& + dplOut // >& + clbOut // >| + hdoc // << + dashHdoc // <<- + wordHdoc // <<< + rdrAll // &> + appAll // &>> + + cmdIn // <( + cmdOut // >( + + plus // + + colPlus // :+ + minus // - + colMinus // :- + quest // ? + colQuest // :? + assgn // = + colAssgn // := + perc // % + dblPerc // %% + hash // # + dblHash // ## + caret // ^ + dblCaret // ^^ + comma // , + dblComma // ,, + at // @ + slash // / + dblSlash // // + colon // : + + tsExists // -e + tsRegFile // -f + tsDirect // -d + tsCharSp // -c + tsBlckSp // -b + tsNmPipe // -p + tsSocket // -S + tsSmbLink // -L + tsSticky // -k + tsGIDSet // -g + tsUIDSet // -u + tsGrpOwn // -G + tsUsrOwn // -O + tsModif // -N + tsRead // -r + tsWrite // -w + tsExec // -x + tsNoEmpty // -s + tsFdTerm // -t + tsEmpStr // -z + tsNempStr // -n + tsOptSet // -o + tsVarSet // -v + tsRefVar // -R + + tsReMatch // =~ + tsNewer // -nt + tsOlder // -ot + tsDevIno // -ef + tsEql // -eq + tsNeq // -ne + tsLeq // -le + tsGeq // -ge + tsLss // -lt + tsGtr // -gt + + globQuest // ?( + globStar // *( + globPlus // +( + globAt // @( + globExcl // !( +) + +type RedirOperator token + +const ( + RdrOut = RedirOperator(rdrOut) + iota + AppOut + RdrIn + RdrInOut + DplIn + DplOut + ClbOut + Hdoc + DashHdoc + WordHdoc + RdrAll + AppAll +) + +type ProcOperator token + +const ( + CmdIn = ProcOperator(cmdIn) + iota + CmdOut +) + +type GlobOperator token + +const ( + GlobQuest = GlobOperator(globQuest) + iota + GlobStar + GlobPlus + GlobAt + GlobExcl +) + +type BinCmdOperator token + +const ( + AndStmt = BinCmdOperator(andAnd) + iota + OrStmt + Pipe + PipeAll +) + +type CaseOperator token + +const ( + DblSemicolon = CaseOperator(dblSemicolon) + iota + SemiFall + DblSemiFall +) + +type ParExpOperator token + +const ( + SubstPlus = ParExpOperator(plus) + iota + SubstColPlus + SubstMinus + SubstColMinus + SubstQuest + SubstColQuest + SubstAssgn + SubstColAssgn + RemSmallSuffix + RemLargeSuffix + RemSmallPrefix + RemLargePrefix + UpperFirst + UpperAll + LowerFirst + LowerAll + OtherParamOps +) + +type UnAritOperator token + +const ( + Not = UnAritOperator(exclMark) + iota + Inc + Dec + Plus = UnAritOperator(plus) + Minus = UnAritOperator(minus) +) + +type BinAritOperator token + +const ( + Add = BinAritOperator(plus) + Sub = BinAritOperator(minus) + Mul = BinAritOperator(star) + Quo = BinAritOperator(slash) + Rem = BinAritOperator(perc) + Pow = BinAritOperator(power) + Eql = BinAritOperator(equal) + Gtr = BinAritOperator(rdrOut) + Lss = BinAritOperator(rdrIn) + Neq = BinAritOperator(nequal) + Leq = BinAritOperator(lequal) + Geq = BinAritOperator(gequal) + And = BinAritOperator(and) + Or = BinAritOperator(or) + Xor = BinAritOperator(caret) + Shr = BinAritOperator(appOut) + Shl = BinAritOperator(hdoc) + + AndArit = BinAritOperator(andAnd) + OrArit = BinAritOperator(orOr) + Comma = BinAritOperator(comma) + Quest = BinAritOperator(quest) + Colon = BinAritOperator(colon) + + Assgn = BinAritOperator(assgn) + AddAssgn = BinAritOperator(addAssgn) + SubAssgn = BinAritOperator(subAssgn) + MulAssgn = BinAritOperator(mulAssgn) + QuoAssgn = BinAritOperator(quoAssgn) + RemAssgn = BinAritOperator(remAssgn) + AndAssgn = BinAritOperator(andAssgn) + OrAssgn = BinAritOperator(orAssgn) + XorAssgn = BinAritOperator(xorAssgn) + ShlAssgn = BinAritOperator(shlAssgn) + ShrAssgn = BinAritOperator(shrAssgn) +) + +type UnTestOperator token + +const ( + TsExists = UnTestOperator(tsExists) + iota + TsRegFile + TsDirect + TsCharSp + TsBlckSp + TsNmPipe + TsSocket + TsSmbLink + TsSticky + TsGIDSet + TsUIDSet + TsGrpOwn + TsUsrOwn + TsModif + TsRead + TsWrite + TsExec + TsNoEmpty + TsFdTerm + TsEmpStr + TsNempStr + TsOptSet + TsVarSet + TsRefVar + TsNot = UnTestOperator(exclMark) +) + +type BinTestOperator token + +const ( + TsReMatch = BinTestOperator(tsReMatch) + iota + TsNewer + TsOlder + TsDevIno + TsEql + TsNeq + TsLeq + TsGeq + TsLss + TsGtr + AndTest = BinTestOperator(andAnd) + OrTest = BinTestOperator(orOr) + // TODO(mvdan): == and != are pattern matches; use more + // appropriate names like TsMatch and TsNoMatch in 2.0 + TsEqual = BinTestOperator(equal) + TsNequal = BinTestOperator(nequal) + TsBefore = BinTestOperator(rdrIn) + TsAfter = BinTestOperator(rdrOut) + // Deprecated: now parses as TsEqual + TsAssgn = BinTestOperator(assgn) // TODO(mvdan): remove in 2.0 +) + +func (o RedirOperator) String() string { return token(o).String() } +func (o ProcOperator) String() string { return token(o).String() } +func (o GlobOperator) String() string { return token(o).String() } +func (o BinCmdOperator) String() string { return token(o).String() } +func (o CaseOperator) String() string { return token(o).String() } +func (o ParExpOperator) String() string { return token(o).String() } +func (o UnAritOperator) String() string { return token(o).String() } +func (o BinAritOperator) String() string { return token(o).String() } +func (o UnTestOperator) String() string { return token(o).String() } +func (o BinTestOperator) String() string { return token(o).String() } diff --git a/vendor/github.com/mvdan/sh/syntax/walk.go b/vendor/github.com/mvdan/sh/syntax/walk.go new file mode 100644 index 00000000..f2b6d3fd --- /dev/null +++ b/vendor/github.com/mvdan/sh/syntax/walk.go @@ -0,0 +1,184 @@ +// Copyright (c) 2016, Daniel Martí +// See LICENSE for licensing information + +package syntax + +import "fmt" + +func walkStmts(stmts []*Stmt, f func(Node) bool) { + for _, s := range stmts { + Walk(s, f) + } +} + +func walkWords(words []*Word, f func(Node) bool) { + for _, w := range words { + Walk(w, f) + } +} + +// Walk traverses an AST in depth-first order: It starts by calling +// f(node); node must not be nil. If f returns true, Walk invokes f +// recursively for each of the non-nil children of node, followed by +// f(nil). +func Walk(node Node, f func(Node) bool) { + if !f(node) { + return + } + + switch x := node.(type) { + case *File: + walkStmts(x.Stmts, f) + case *Stmt: + if x.Cmd != nil { + Walk(x.Cmd, f) + } + for _, a := range x.Assigns { + Walk(a, f) + } + for _, r := range x.Redirs { + Walk(r, f) + } + case *Assign: + if x.Name != nil { + Walk(x.Name, f) + } + if x.Value != nil { + Walk(x.Value, f) + } + case *Redirect: + if x.N != nil { + Walk(x.N, f) + } + Walk(x.Word, f) + if x.Hdoc != nil { + Walk(x.Hdoc, f) + } + case *CallExpr: + walkWords(x.Args, f) + case *Subshell: + walkStmts(x.Stmts, f) + case *Block: + walkStmts(x.Stmts, f) + case *IfClause: + walkStmts(x.CondStmts, f) + walkStmts(x.ThenStmts, f) + for _, elif := range x.Elifs { + walkStmts(elif.CondStmts, f) + walkStmts(elif.ThenStmts, f) + } + walkStmts(x.ElseStmts, f) + case *WhileClause: + walkStmts(x.CondStmts, f) + walkStmts(x.DoStmts, f) + case *UntilClause: + walkStmts(x.CondStmts, f) + walkStmts(x.DoStmts, f) + case *ForClause: + Walk(x.Loop, f) + walkStmts(x.DoStmts, f) + case *WordIter: + Walk(x.Name, f) + walkWords(x.List, f) + case *CStyleLoop: + if x.Init != nil { + Walk(x.Init, f) + } + if x.Cond != nil { + Walk(x.Cond, f) + } + if x.Post != nil { + Walk(x.Post, f) + } + case *BinaryCmd: + Walk(x.X, f) + Walk(x.Y, f) + case *FuncDecl: + Walk(x.Name, f) + Walk(x.Body, f) + case *Word: + for _, wp := range x.Parts { + Walk(wp, f) + } + case *Lit: + case *SglQuoted: + case *DblQuoted: + for _, wp := range x.Parts { + Walk(wp, f) + } + case *CmdSubst: + walkStmts(x.Stmts, f) + case *ParamExp: + if x.Param != nil { + Walk(x.Param, f) + } + if x.Ind != nil { + Walk(x.Ind.Expr, f) + } + if x.Repl != nil { + Walk(x.Repl.Orig, f) + Walk(x.Repl.With, f) + } + if x.Exp != nil { + Walk(x.Exp.Word, f) + } + case *ArithmExp: + if x.X != nil { + Walk(x.X, f) + } + case *ArithmCmd: + if x.X != nil { + Walk(x.X, f) + } + case *BinaryArithm: + Walk(x.X, f) + Walk(x.Y, f) + case *BinaryTest: + Walk(x.X, f) + Walk(x.Y, f) + case *UnaryArithm: + Walk(x.X, f) + case *UnaryTest: + Walk(x.X, f) + case *ParenArithm: + Walk(x.X, f) + case *ParenTest: + Walk(x.X, f) + case *CaseClause: + Walk(x.Word, f) + for _, pl := range x.List { + walkWords(pl.Patterns, f) + walkStmts(pl.Stmts, f) + } + case *TestClause: + Walk(x.X, f) + case *DeclClause: + walkWords(x.Opts, f) + for _, a := range x.Assigns { + Walk(a, f) + } + case *ArrayExpr: + walkWords(x.List, f) + case *ExtGlob: + Walk(x.Pattern, f) + case *ProcSubst: + walkStmts(x.Stmts, f) + case *EvalClause: + if x.Stmt != nil { + Walk(x.Stmt, f) + } + case *CoprocClause: + if x.Name != nil { + Walk(x.Name, f) + } + Walk(x.Stmt, f) + case *LetClause: + for _, expr := range x.Exprs { + Walk(expr, f) + } + default: + panic(fmt.Sprintf("syntax.Walk: unexpected node type %T", x)) + } + + f(nil) +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 60e7ade2..6329c833 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -50,6 +50,18 @@ "revision": "95345c4e1c0ebc9d16a3284177f09360f4d20fab", "revisionTime": "2017-01-24T11:57:57Z" }, + { + "checksumSHA1": "1ZLAvHVYAS3kxaYI8OQiTBllqNU=", + "path": "github.com/mvdan/sh/interp", + "revision": "17e267b541e30baece16b7ddeae50822cc6a795f", + "revisionTime": "2017-04-24T11:31:08Z" + }, + { + "checksumSHA1": "4/7joITdf4wl+uoV8zDXgYqy2aw=", + "path": "github.com/mvdan/sh/syntax", + "revision": "17e267b541e30baece16b7ddeae50822cc6a795f", + "revisionTime": "2017-04-24T11:31:08Z" + }, { "checksumSHA1": "HUXE+Nrcau8FSaVEvPYHMvDjxOE=", "path": "github.com/satori/go.uuid", From 70fa93d0ff1d04bd6a3dc43a1392c266294e643b Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Mon, 24 Apr 2017 09:56:14 -0300 Subject: [PATCH 3/3] Update README documentation after changes --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bdb20c2e..c456411d 100644 --- a/README.md +++ b/README.md @@ -40,8 +40,10 @@ Running the tasks is as simple as running: task assets build ``` -If Bash is available (Linux and Windows while on Git Bash), the commands will -run in Bash, otherwise will fallback to `cmd` (on Windows). +Task uses [github.com/mvdan/sh](https://github.com/mvdan/sh), a native Go sh +interpreter. So you can write sh/bash commands and it will work even on +Windows, where `sh` or `bash` is usually not available. Just remember any +executable called must be available by the OS or in PATH. If you ommit a task name, "default" will be assumed. @@ -287,13 +289,9 @@ Task also adds the following functions: "darwin" (macOS) and "freebsd". - `ARCH`: return the architecture Task was compiled to: "386", "amd64", "arm" or "s390x". -- `IsSH`: on unix systems this should always return `true`. On Windows, will - return `true` if `sh` command was found (Git Bash). In this case commands - will run on `sh`. Otherwise, this function will return `false` meaning - commands will run on `cmd`. -- `ToSlash`: Does nothing on `sh`, but on `cmd` converts a string from `\` +- `ToSlash`: Does nothing on Unix, but on Windows converts a string from `\` path format to `/`. -- `FromSlash`: Oposite of `ToSlash`. Does nothing on `sh`, but on `cmd` +- `FromSlash`: Oposite of `ToSlash`. Does nothing on Unix, but on Windows converts a string from `\` path format to `/`. Example: @@ -303,9 +301,8 @@ printos: cmds: - echo '{{OS}} {{ARCH}}' - "echo '{{if eq OS \"windows\"}}windows-command{{else}}unix-command{{end}}'" - - echo 'Is SH? {{IsSH}}' - # This will be ./bin/executable on sh but .\bin\executable on cmd - - "{{FromSlash \"./bin/executable\"}}" + # This will be path/to/file on Unix but path\to\file on Windows + - "{{FromSlash \"path/to/file\"}}" ``` ### Help @@ -358,3 +355,4 @@ Similar software: [gotemplate]: https://golang.org/pkg/text/template/ [robo]: https://github.com/tj/robo [dog]: https://github.com/dogtools/dog +[sh]: https://github.com/mvdan/sh