1
0
mirror of https://github.com/go-task/task.git synced 2025-03-05 15:05:42 +02:00

Merge pull request #154 from go-task/upgrade-mvdan-sh

Upgrade mvdan/sh
This commit is contained in:
Andrey Nering 2018-12-24 15:30:45 -02:00 committed by GitHub
commit 61b3fca9a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2169 additions and 1341 deletions

View File

@ -3,7 +3,9 @@
## Unreleased
- On Windows, Task can now be installed using [Scoop](https://scoop.sh/)
([#152](https://github.com/go-task/task/pull/152)).
([#152](https://github.com/go-task/task/pull/152));
- Fixes issue with file/directory globing
([#153](https://github.com/go-task/task/issues/153)).
## v2.2.1 - 2018-12-09

2
go.mod
View File

@ -20,5 +20,5 @@ require (
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789 // indirect
gopkg.in/yaml.v2 v2.2.1
mvdan.cc/sh v0.0.0-20180829163519-3a244a89e2e5
mvdan.cc/sh v2.6.3-0.20181216173157-8aeb0734cd0f+incompatible
)

4
go.sum
View File

@ -41,5 +41,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
mvdan.cc/sh v0.0.0-20180829163519-3a244a89e2e5 h1:FKi9XtQO5aNipfQ/qnnLCoM6gdFwPQY702RRbNRxjK8=
mvdan.cc/sh v0.0.0-20180829163519-3a244a89e2e5/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=
mvdan.cc/sh v2.6.3-0.20181216173157-8aeb0734cd0f+incompatible h1:jf0jjqiqwKXdH3JBKY+K3tFXGtUQZr/pFIO+cn0tQCc=
mvdan.cc/sh v2.6.3-0.20181216173157-8aeb0734cd0f+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8=

View File

@ -7,7 +7,9 @@ import (
"os"
"strings"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/interp"
"mvdan.cc/sh/shell"
"mvdan.cc/sh/syntax"
)
@ -41,14 +43,10 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
if len(environ) == 0 {
environ = os.Environ()
}
env, err := interp.EnvFromList(environ)
if err != nil {
return err
}
r, err := interp.New(
interp.Dir(opts.Dir),
interp.Env(env),
interp.Env(expand.ListEnviron(environ...)),
interp.Module(interp.DefaultExec),
interp.Module(interp.OpenDevImpls(interp.DefaultOpen)),
@ -70,3 +68,16 @@ func IsExitError(err error) bool {
return false
}
}
// Expand is a helper to mvdan.cc/shell.Fields that returns the first field
// if available.
func Expand(s string) (string, error) {
fields, err := shell.Fields(s, nil)
if err != nil {
return "", err
}
if len(fields) > 0 {
return fields[0], nil
}
return "", nil
}

View File

@ -4,8 +4,9 @@ import (
"path/filepath"
"sort"
"github.com/go-task/task/v2/internal/execext"
"github.com/mattn/go-zglob"
"mvdan.cc/sh/shell"
)
func glob(dir string, globs []string) (files []string, err error) {
@ -13,7 +14,7 @@ func glob(dir string, globs []string) (files []string, err error) {
if !filepath.IsAbs(g) {
g = filepath.Join(dir, g)
}
g, err = shell.Expand(g, nil)
g, err = execext.Expand(g)
if err != nil {
return nil, err
}

View File

@ -3,10 +3,9 @@ package task
import (
"path/filepath"
"github.com/go-task/task/v2/internal/execext"
"github.com/go-task/task/v2/internal/taskfile"
"github.com/go-task/task/v2/internal/templater"
"mvdan.cc/sh/shell"
)
// CompiledTask returns a copy of a task, but replacing variables in almost all
@ -37,7 +36,7 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) {
Prefix: r.Replace(origTask.Prefix),
IgnoreError: origTask.IgnoreError,
}
new.Dir, err = shell.Expand(new.Dir, nil)
new.Dir, err = execext.Expand(new.Dir)
if err != nil {
return nil, err
}

5
vendor/modules.txt vendored
View File

@ -38,7 +38,8 @@ golang.org/x/sys/unix
golang.org/x/sys/windows
# gopkg.in/yaml.v2 v2.2.1
gopkg.in/yaml.v2
# mvdan.cc/sh v0.0.0-20180829163519-3a244a89e2e5
mvdan.cc/sh/shell
# mvdan.cc/sh v2.6.3-0.20181216173157-8aeb0734cd0f+incompatible
mvdan.cc/sh/expand
mvdan.cc/sh/interp
mvdan.cc/sh/shell
mvdan.cc/sh/syntax

View File

@ -1,57 +1,66 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package interp
package expand
import (
"context"
"fmt"
"strconv"
"mvdan.cc/sh/syntax"
)
func (r *Runner) arithm(ctx context.Context, expr syntax.ArithmExpr) int {
func Arithm(cfg *Config, expr syntax.ArithmExpr) (int, error) {
switch x := expr.(type) {
case *syntax.Word:
str := r.loneWord(ctx, x)
str, err := Literal(cfg, x)
if err != nil {
return 0, err
}
// recursively fetch vars
for str != "" {
val := r.getVar(str)
i := 0
for str != "" && syntax.ValidName(str) {
val := cfg.envGet(str)
if val == "" {
break
}
if i++; i >= maxNameRefDepth {
break
}
str = val
}
// default to 0
return atoi(str)
return atoi(str), nil
case *syntax.ParenArithm:
return r.arithm(ctx, x.X)
return Arithm(cfg, x.X)
case *syntax.UnaryArithm:
switch x.Op {
case syntax.Inc, syntax.Dec:
name := x.X.(*syntax.Word).Parts[0].(*syntax.Lit).Value
old := atoi(r.getVar(name))
name := x.X.(*syntax.Word).Lit()
old := atoi(cfg.envGet(name))
val := old
if x.Op == syntax.Inc {
val++
} else {
val--
}
r.setVarString(ctx, name, strconv.Itoa(val))
cfg.envSet(name, strconv.Itoa(val))
if x.Post {
return old
return old, nil
}
return val
return val, nil
}
val, err := Arithm(cfg, x.X)
if err != nil {
return 0, err
}
val := r.arithm(ctx, x.X)
switch x.Op {
case syntax.Not:
return oneIf(val == 0)
return oneIf(val == 0), nil
case syntax.Plus:
return val
return val, nil
default: // syntax.Minus
return -val
return -val, nil
}
case *syntax.BinaryArithm:
switch x.Op {
@ -59,16 +68,27 @@ func (r *Runner) arithm(ctx context.Context, expr syntax.ArithmExpr) int {
syntax.MulAssgn, syntax.QuoAssgn, syntax.RemAssgn,
syntax.AndAssgn, syntax.OrAssgn, syntax.XorAssgn,
syntax.ShlAssgn, syntax.ShrAssgn:
return r.assgnArit(ctx, x)
return cfg.assgnArit(x)
case syntax.Quest: // Colon can't happen here
cond := r.arithm(ctx, x.X)
cond, err := Arithm(cfg, x.X)
if err != nil {
return 0, err
}
b2 := x.Y.(*syntax.BinaryArithm) // must have Op==Colon
if cond == 1 {
return r.arithm(ctx, b2.X)
return Arithm(cfg, b2.X)
}
return r.arithm(ctx, b2.Y)
return Arithm(cfg, b2.Y)
}
return binArit(x.Op, r.arithm(ctx, x.X), r.arithm(ctx, x.Y))
left, err := Arithm(cfg, x.X)
if err != nil {
return 0, err
}
right, err := Arithm(cfg, x.Y)
if err != nil {
return 0, err
}
return binArit(x.Op, left, right), nil
default:
panic(fmt.Sprintf("unexpected arithm expr: %T", x))
}
@ -88,10 +108,13 @@ func atoi(s string) int {
return n
}
func (r *Runner) assgnArit(ctx context.Context, b *syntax.BinaryArithm) int {
name := b.X.(*syntax.Word).Parts[0].(*syntax.Lit).Value
val := atoi(r.getVar(name))
arg := r.arithm(ctx, b.Y)
func (cfg *Config) assgnArit(b *syntax.BinaryArithm) (int, error) {
name := b.X.(*syntax.Word).Lit()
val := atoi(cfg.envGet(name))
arg, err := Arithm(cfg, b.Y)
if err != nil {
return 0, err
}
switch b.Op {
case syntax.Assgn:
val = arg
@ -116,8 +139,8 @@ func (r *Runner) assgnArit(ctx context.Context, b *syntax.BinaryArithm) int {
case syntax.ShrAssgn:
val >>= uint(arg)
}
r.setVarString(ctx, name, strconv.Itoa(val))
return val
cfg.envSet(name, strconv.Itoa(val))
return val, nil
}
func intPow(a, b int) int {

24
vendor/mvdan.cc/sh/expand/braces.go vendored Normal file
View File

@ -0,0 +1,24 @@
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package expand
import "mvdan.cc/sh/syntax"
// Braces performs Bash brace expansion on words. For example, passing it a
// literal word "foo{bar,baz}" will return two literal words, "foobar" and
// "foobaz".
//
// It does not return an error; malformed brace expansions are simply skipped.
// For example, "a{b{c,d}" results in the words "a{bc" and "a{bd".
//
// Note that the resulting words may have more word parts than necessary, such
// as contiguous *syntax.Lit nodes, and that these parts may be shared between
// words.
func Braces(words ...*syntax.Word) []*syntax.Word {
var res []*syntax.Word
for _, word := range words {
res = append(res, syntax.ExpandBraces(word)...)
}
return res
}

5
vendor/mvdan.cc/sh/expand/doc.go vendored Normal file
View File

@ -0,0 +1,5 @@
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
// Package expand contains code to perform various shell expansions.
package expand

195
vendor/mvdan.cc/sh/expand/environ.go vendored Normal file
View File

@ -0,0 +1,195 @@
// Copyright (c) 2018, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package expand
import (
"runtime"
"sort"
"strings"
)
// Environ is the base interface for a shell's environment, allowing it to fetch
// variables by name and to iterate over all the currently set variables.
type Environ interface {
// Get retrieves a variable by its name. To check if the variable is
// set, use Variable.IsSet.
Get(name string) Variable
// Each iterates over all the currently set variables, calling the
// supplied function on each variable. Iteration is stopped if the
// function returns false.
//
// The names used in the calls aren't required to be unique or sorted.
// If a variable name appears twice, the latest occurrence takes
// priority.
//
// Each is required to forward exported variables when executing
// programs.
Each(func(name string, vr Variable) bool)
}
// WriteEnviron is an extension on Environ that supports modifying and deleting
// variables.
type WriteEnviron interface {
Environ
// Set sets a variable by name. If !vr.IsSet(), the variable is being
// unset; otherwise, the variable is being replaced.
//
// It is the implementation's responsibility to handle variable
// attributes correctly. For example, changing an exported variable's
// value does not unexport it, and overwriting a name reference variable
// should modify its target.
Set(name string, vr Variable)
}
// Variable describes a shell variable, which can have a number of attributes
// and a value.
//
// A Variable is unset if its Value field is untyped nil, which can be checked
// via Variable.IsSet. The zero value of a Variable is thus a valid unset
// variable.
//
// If a variable is set, its Value field will be a []string if it is an indexed
// array, a map[string]string if it's an associative array, or a string
// otherwise.
type Variable struct {
Local bool
Exported bool
ReadOnly bool
NameRef bool // if true, Value must be string
Value interface{} // string, []string, or map[string]string
}
// IsSet returns whether the variable is set. An empty variable is set, but an
// undeclared variable is not.
func (v Variable) IsSet() bool {
return v.Value != nil
}
// String returns the variable's value as a string. In general, this only makes
// sense if the variable has a string value or no value at all.
func (v Variable) String() string {
switch x := v.Value.(type) {
case string:
return x
case []string:
if len(x) > 0 {
return x[0]
}
case map[string]string:
// nothing to do
}
return ""
}
// maxNameRefDepth defines the maximum number of times to follow references when
// resolving a variable. Otherwise, simple name reference loops could crash a
// program quite easily.
const maxNameRefDepth = 100
// Resolve follows a number of nameref variables, returning the last reference
// name that was followed and the variable that it points to.
func (v Variable) Resolve(env Environ) (string, Variable) {
name := ""
for i := 0; i < maxNameRefDepth; i++ {
if !v.NameRef {
return name, v
}
name = v.Value.(string)
v = env.Get(name)
}
return name, Variable{}
}
// FuncEnviron wraps a function mapping variable names to their string values,
// and implements Environ. Empty strings returned by the function will be
// treated as unset variables. All variables will be exported.
//
// Note that the returned Environ's Each method will be a no-op.
func FuncEnviron(fn func(string) string) Environ {
return funcEnviron(fn)
}
type funcEnviron func(string) string
func (f funcEnviron) Get(name string) Variable {
value := f(name)
if value == "" {
return Variable{}
}
return Variable{Exported: true, Value: value}
}
func (f funcEnviron) Each(func(name string, vr Variable) bool) {}
// ListEnviron returns an Environ with the supplied variables, in the form
// "key=value". All variables will be exported.
//
// On Windows, where environment variable names are case-insensitive, the
// resulting variable names will all be uppercase.
func ListEnviron(pairs ...string) Environ {
return listEnvironWithUpper(runtime.GOOS == "windows", pairs...)
}
// listEnvironWithUpper implements ListEnviron, but letting the tests specify
// whether to uppercase all names or not.
func listEnvironWithUpper(upper bool, pairs ...string) Environ {
list := append([]string{}, pairs...)
if upper {
// Uppercase before sorting, so that we can remove duplicates
// without the need for linear search nor a map.
for i, s := range list {
if sep := strings.IndexByte(s, '='); sep > 0 {
list[i] = strings.ToUpper(s[:sep]) + s[sep:]
}
}
}
sort.Strings(list)
last := ""
for i := 0; i < len(list); {
s := list[i]
sep := strings.IndexByte(s, '=')
if sep <= 0 {
// invalid element; remove it
list = append(list[:i], list[i+1:]...)
continue
}
name := s[:sep]
if last == name {
// duplicate; the last one wins
list = append(list[:i-1], list[i:]...)
continue
}
last = name
i++
}
return listEnviron(list)
}
type listEnviron []string
func (l listEnviron) Get(name string) Variable {
// TODO: binary search
prefix := name + "="
for _, pair := range l {
if val := strings.TrimPrefix(pair, prefix); val != pair {
return Variable{Exported: true, Value: val}
}
}
return Variable{}
}
func (l listEnviron) Each(fn func(name string, vr Variable) bool) {
for _, pair := range l {
i := strings.IndexByte(pair, '=')
if i < 0 {
// can't happen; see above
panic("expand.listEnviron: did not expect malformed name-value pair: " + pair)
}
name, value := pair[:i], pair[i+1:]
if !fn(name, Variable{Exported: true, Value: value}) {
return
}
}
}

784
vendor/mvdan.cc/sh/expand/expand.go vendored Normal file
View File

@ -0,0 +1,784 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package expand
import (
"bytes"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"mvdan.cc/sh/syntax"
)
// A Config specifies details about how shell expansion should be performed. The
// zero value is a valid configuration.
type Config struct {
// Env is used to get and set environment variables when performing
// shell expansions. Some special parameters are also expanded via this
// interface, such as:
//
// * "#", "@", "*", "0"-"9" for the shell's parameters
// * "?", "$", "PPID" for the shell's status and process
// * "HOME foo" to retrieve user foo's home directory (if unset,
// os/user.Lookup will be used)
//
// If nil, there are no environment variables set. Use
// ListEnviron(os.Environ()...) to use the system's environment
// variables.
Env Environ
// TODO(mvdan): consider replacing NoGlob==true with ReadDir==nil.
// NoGlob corresponds to the shell option that disables globbing.
NoGlob bool
// GlobStar corresponds to the shell option that allows globbing with
// "**".
GlobStar bool
// CmdSubst expands a command substitution node, writing its standard
// output to the provided io.Writer.
//
// If nil, encountering a command substitution will result in an
// UnexpectedCommandError.
CmdSubst func(io.Writer, *syntax.CmdSubst) error
// ReadDir is used for file path globbing. If nil, globbing is disabled.
// Use ioutil.ReadDir to use the filesystem directly.
ReadDir func(string) ([]os.FileInfo, error)
bufferAlloc bytes.Buffer
fieldAlloc [4]fieldPart
fieldsAlloc [4][]fieldPart
ifs string
// A pointer to a parameter expansion node, if we're inside one.
// Necessary for ${LINENO}.
curParam *syntax.ParamExp
}
// UnexpectedCommandError is returned if a command substitution is encountered
// when Config.CmdSubst is nil.
type UnexpectedCommandError struct {
Node *syntax.CmdSubst
}
func (u UnexpectedCommandError) Error() string {
return fmt.Sprintf("unexpected command substitution at %s", u.Node.Pos())
}
var zeroConfig = &Config{}
func prepareConfig(cfg *Config) *Config {
if cfg == nil {
cfg = zeroConfig
}
if cfg.Env == nil {
cfg.Env = FuncEnviron(func(string) string { return "" })
}
cfg.ifs = " \t\n"
if vr := cfg.Env.Get("IFS"); vr.IsSet() {
cfg.ifs = vr.String()
}
return cfg
}
func (cfg *Config) ifsRune(r rune) bool {
for _, r2 := range cfg.ifs {
if r == r2 {
return true
}
}
return false
}
func (cfg *Config) ifsJoin(strs []string) string {
sep := ""
if cfg.ifs != "" {
sep = cfg.ifs[:1]
}
return strings.Join(strs, sep)
}
func (cfg *Config) strBuilder() *bytes.Buffer {
b := &cfg.bufferAlloc
b.Reset()
return b
}
func (cfg *Config) envGet(name string) string {
return cfg.Env.Get(name).String()
}
func (cfg *Config) envSet(name, value string) {
wenv, ok := cfg.Env.(WriteEnviron)
if !ok {
// TODO: we should probably error here
return
}
wenv.Set(name, Variable{Value: value})
}
// Literal expands a single shell word. It is similar to Fields, but the result
// is a single string. This is the behavior when a word is used as the value in
// a shell variable assignment, for example.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Literal(cfg *Config, word *syntax.Word) (string, error) {
if word == nil {
return "", nil
}
cfg = prepareConfig(cfg)
field, err := cfg.wordField(word.Parts, quoteNone)
if err != nil {
return "", err
}
return cfg.fieldJoin(field), nil
}
// Document expands a single shell word as if it were within double quotes. It
// is simlar to Literal, but without brace expansion, tilde expansion, and
// globbing.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Document(cfg *Config, word *syntax.Word) (string, error) {
if word == nil {
return "", nil
}
cfg = prepareConfig(cfg)
field, err := cfg.wordField(word.Parts, quoteDouble)
if err != nil {
return "", err
}
return cfg.fieldJoin(field), nil
}
// Pattern expands a single shell word as a pattern, using syntax.QuotePattern
// on any non-quoted parts of the input word. The result can be used on
// syntax.TranslatePattern directly.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Pattern(cfg *Config, word *syntax.Word) (string, error) {
cfg = prepareConfig(cfg)
field, err := cfg.wordField(word.Parts, quoteNone)
if err != nil {
return "", err
}
buf := cfg.strBuilder()
for _, part := range field {
if part.quote > quoteNone {
buf.WriteString(syntax.QuotePattern(part.val))
} else {
buf.WriteString(part.val)
}
}
return buf.String(), nil
}
// Format expands a format string with a number of arguments, following the
// shell's format specifications. These include printf(1), among others.
//
// The resulting string is returned, along with the number of arguments used.
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func Format(cfg *Config, format string, args []string) (string, int, error) {
cfg = prepareConfig(cfg)
buf := cfg.strBuilder()
esc := false
var fmts []rune
initialArgs := len(args)
for _, c := range format {
switch {
case esc:
esc = false
switch c {
case 'n':
buf.WriteRune('\n')
case 'r':
buf.WriteRune('\r')
case 't':
buf.WriteRune('\t')
case '\\':
buf.WriteRune('\\')
default:
buf.WriteRune('\\')
buf.WriteRune(c)
}
case len(fmts) > 0:
switch c {
case '%':
buf.WriteByte('%')
fmts = nil
case 'c':
var b byte
if len(args) > 0 {
arg := ""
arg, args = args[0], args[1:]
if len(arg) > 0 {
b = arg[0]
}
}
buf.WriteByte(b)
fmts = nil
case '+', '-', ' ':
if len(fmts) > 1 {
return "", 0, fmt.Errorf("invalid format char: %c", c)
}
fmts = append(fmts, c)
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
fmts = append(fmts, c)
case 's', 'd', 'i', 'u', 'o', 'x':
arg := ""
if len(args) > 0 {
arg, args = args[0], args[1:]
}
var farg interface{} = arg
if c != 's' {
n, _ := strconv.ParseInt(arg, 0, 0)
if c == 'i' || c == 'd' {
farg = int(n)
} else {
farg = uint(n)
}
if c == 'i' || c == 'u' {
c = 'd'
}
}
fmts = append(fmts, c)
fmt.Fprintf(buf, string(fmts), farg)
fmts = nil
default:
return "", 0, fmt.Errorf("invalid format char: %c", c)
}
case c == '\\':
esc = true
case args != nil && c == '%':
// if args == nil, we are not doing format
// arguments
fmts = []rune{c}
default:
buf.WriteRune(c)
}
}
if len(fmts) > 0 {
return "", 0, fmt.Errorf("missing format char")
}
return buf.String(), initialArgs - len(args), nil
}
func (cfg *Config) fieldJoin(parts []fieldPart) string {
switch len(parts) {
case 0:
return ""
case 1: // short-cut without a string copy
return parts[0].val
}
buf := cfg.strBuilder()
for _, part := range parts {
buf.WriteString(part.val)
}
return buf.String()
}
func (cfg *Config) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {
buf := cfg.strBuilder()
for _, part := range parts {
if part.quote > quoteNone {
buf.WriteString(syntax.QuotePattern(part.val))
continue
}
buf.WriteString(part.val)
if syntax.HasPattern(part.val) {
glob = true
}
}
if glob { // only copy the string if it will be used
escaped = buf.String()
}
return escaped, glob
}
// Fields expands a number of words as if they were arguments in a shell
// command. This includes brace expansion, tilde expansion, parameter expansion,
// command substitution, arithmetic expansion, and quote removal.
func Fields(cfg *Config, words ...*syntax.Word) ([]string, error) {
cfg = prepareConfig(cfg)
fields := make([]string, 0, len(words))
dir := cfg.envGet("PWD")
for _, expWord := range Braces(words...) {
wfields, err := cfg.wordFields(expWord.Parts)
if err != nil {
return nil, err
}
for _, field := range wfields {
path, doGlob := cfg.escapedGlobField(field)
var matches []string
abs := filepath.IsAbs(path)
if doGlob && !cfg.NoGlob {
base := ""
if !abs {
base = dir
}
matches, err = cfg.glob(base, path)
if err != nil {
return nil, err
}
}
if len(matches) == 0 {
fields = append(fields, cfg.fieldJoin(field))
continue
}
for _, match := range matches {
if !abs {
match = strings.TrimPrefix(match, dir)
}
fields = append(fields, match)
}
}
}
return fields, nil
}
type fieldPart struct {
val string
quote quoteLevel
}
type quoteLevel uint
const (
quoteNone quoteLevel = iota
quoteDouble
quoteSingle
)
func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {
var field []fieldPart
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 && ql == quoteNone {
s = cfg.expandUser(s)
}
if ql == quoteDouble && strings.Contains(s, "\\") {
buf := cfg.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' && i+1 < len(s) {
switch s[i+1] {
case '\n': // remove \\\n
i++
continue
case '"', '\\', '$', '`': // special chars
continue
}
}
buf.WriteByte(b)
}
s = buf.String()
}
field = append(field, fieldPart{val: s})
case *syntax.SglQuoted:
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
fp.val, _, _ = Format(cfg, fp.val, nil)
}
field = append(field, fp)
case *syntax.DblQuoted:
wfield, err := cfg.wordField(x.Parts, quoteDouble)
if err != nil {
return nil, err
}
for _, part := range wfield {
part.quote = quoteDouble
field = append(field, part)
}
case *syntax.ParamExp:
val, err := cfg.paramExp(x)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: val})
case *syntax.CmdSubst:
val, err := cfg.cmdSubst(x)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: val})
case *syntax.ArithmExp:
n, err := Arithm(cfg, x.X)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: strconv.Itoa(n)})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
return field, nil
}
func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
if cfg.CmdSubst == nil {
return "", UnexpectedCommandError{Node: cs}
}
buf := cfg.strBuilder()
if err := cfg.CmdSubst(buf, cs); err != nil {
return "", err
}
return strings.TrimRight(buf.String(), "\n"), nil
}
func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
fields := cfg.fieldsAlloc[:0]
curField := cfg.fieldAlloc[:0]
allowEmpty := false
flush := func() {
if len(curField) == 0 {
return
}
fields = append(fields, curField)
curField = nil
}
splitAdd := func(val string) {
for i, field := range strings.FieldsFunc(val, cfg.ifsRune) {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{val: field})
}
}
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 {
s = cfg.expandUser(s)
}
if strings.Contains(s, "\\") {
buf := cfg.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' {
i++
b = s[i]
}
buf.WriteByte(b)
}
s = buf.String()
}
curField = append(curField, fieldPart{val: s})
case *syntax.SglQuoted:
allowEmpty = true
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
fp.val, _, _ = Format(cfg, fp.val, nil)
}
curField = append(curField, fp)
case *syntax.DblQuoted:
allowEmpty = true
if len(x.Parts) == 1 {
pe, _ := x.Parts[0].(*syntax.ParamExp)
if elems := cfg.quotedElems(pe); elems != nil {
for i, elem := range elems {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{
quote: quoteDouble,
val: elem,
})
}
continue
}
}
wfield, err := cfg.wordField(x.Parts, quoteDouble)
if err != nil {
return nil, err
}
for _, part := range wfield {
part.quote = quoteDouble
curField = append(curField, part)
}
case *syntax.ParamExp:
val, err := cfg.paramExp(x)
if err != nil {
return nil, err
}
splitAdd(val)
case *syntax.CmdSubst:
val, err := cfg.cmdSubst(x)
if err != nil {
return nil, err
}
splitAdd(val)
case *syntax.ArithmExp:
n, err := Arithm(cfg, x.X)
if err != nil {
return nil, err
}
curField = append(curField, fieldPart{val: strconv.Itoa(n)})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
flush()
if allowEmpty && len(fields) == 0 {
fields = append(fields, curField)
}
return fields, nil
}
// quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]}
func (cfg *Config) quotedElems(pe *syntax.ParamExp) []string {
if pe == nil || pe.Excl || pe.Length || pe.Width {
return nil
}
if pe.Param.Value == "@" {
return cfg.Env.Get("@").Value.([]string)
}
if nodeLit(pe.Index) != "@" {
return nil
}
val := cfg.Env.Get(pe.Param.Value).Value
if x, ok := val.([]string); ok {
return x
}
return nil
}
func (cfg *Config) expandUser(field string) string {
if len(field) == 0 || field[0] != '~' {
return field
}
name := field[1:]
rest := ""
if i := strings.Index(name, "/"); i >= 0 {
rest = name[i:]
name = name[:i]
}
if name == "" {
return cfg.Env.Get("HOME").String() + rest
}
if vr := cfg.Env.Get("HOME " + name); vr.IsSet() {
return vr.String() + rest
}
u, err := user.Lookup(name)
if err != nil {
return field
}
return u.HomeDir + rest
}
func findAllIndex(pattern, name string, n int) [][]int {
expr, err := syntax.TranslatePattern(pattern, true)
if err != nil {
return nil
}
rx := regexp.MustCompile(expr)
return rx.FindAllStringIndex(name, n)
}
// TODO: use this again to optimize globbing; see
// https://github.com/mvdan/sh/issues/213
func hasGlob(path string) bool {
magicChars := `*?[`
if runtime.GOOS != "windows" {
magicChars = `*?[\`
}
return strings.ContainsAny(path, magicChars)
}
var rxGlobStar = regexp.MustCompile(".*")
// pathJoin2 is a simpler version of filepath.Join without cleaning the result,
// since that's needed for globbing.
func pathJoin2(elem1, elem2 string) string {
if elem1 == "" {
return elem2
}
if strings.HasSuffix(elem1, string(filepath.Separator)) {
return elem1 + elem2
}
return elem1 + string(filepath.Separator) + elem2
}
// pathSplit splits a file path into its elements, retaining empty ones. Before
// splitting, slashes are replaced with filepath.Separator, so that splitting
// Unix paths on Windows works as well.
func pathSplit(path string) []string {
path = filepath.FromSlash(path)
return strings.Split(path, string(filepath.Separator))
}
func (cfg *Config) glob(base, pattern string) ([]string, error) {
parts := pathSplit(pattern)
matches := []string{""}
if filepath.IsAbs(pattern) {
if parts[0] == "" {
// unix-like
matches[0] = string(filepath.Separator)
} else {
// windows (for some reason it won't work without the
// trailing separator)
matches[0] = parts[0] + string(filepath.Separator)
}
parts = parts[1:]
}
for _, part := range parts {
switch {
case part == "", part == ".", part == "..":
var newMatches []string
for _, dir := range matches {
// TODO(mvdan): reuse the previous ReadDir call
if cfg.ReadDir == nil {
continue // no globbing
} else if _, err := cfg.ReadDir(filepath.Join(base, dir)); err != nil {
continue // not actually a dir
}
newMatches = append(newMatches, pathJoin2(dir, part))
}
matches = newMatches
continue
case part == "**" && cfg.GlobStar:
for i, match := range matches {
// "a/**" should match "a/ a/b a/b/cfg ..."; note
// how the zero-match case has a trailing
// separator.
matches[i] = pathJoin2(match, "")
}
// expand all the possible levels of **
latest := matches
for {
var newMatches []string
for _, dir := range latest {
var err error
newMatches, err = cfg.globDir(base, dir, rxGlobStar, newMatches)
if err != nil {
return nil, err
}
}
if len(newMatches) == 0 {
// not another level of directories to
// try; stop
break
}
matches = append(matches, newMatches...)
latest = newMatches
}
continue
}
expr, err := syntax.TranslatePattern(part, true)
if err != nil {
// If any glob part is not a valid pattern, don't glob.
return nil, nil
}
rx := regexp.MustCompile("^" + expr + "$")
var newMatches []string
for _, dir := range matches {
newMatches, err = cfg.globDir(base, dir, rx, newMatches)
if err != nil {
return nil, err
}
}
matches = newMatches
}
return matches, nil
}
func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, matches []string) ([]string, error) {
if cfg.ReadDir == nil {
// TODO(mvdan): check this at the beginning of a glob?
return nil, nil
}
infos, err := cfg.ReadDir(filepath.Join(base, dir))
if err != nil {
return nil, err
}
for _, info := range infos {
name := info.Name()
if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' {
continue
}
if rx.MatchString(name) {
matches = append(matches, pathJoin2(dir, name))
}
}
return matches, nil
}
//
// The config specifies shell expansion options; nil behaves the same as an
// empty config.
func ReadFields(cfg *Config, s string, n int, raw bool) []string {
cfg = prepareConfig(cfg)
type pos struct {
start, end int
}
var fpos []pos
runes := make([]rune, 0, len(s))
infield := false
esc := false
for _, r := range s {
if infield {
if cfg.ifsRune(r) && (raw || !esc) {
fpos[len(fpos)-1].end = len(runes)
infield = false
}
} else {
if !cfg.ifsRune(r) && (raw || !esc) {
fpos = append(fpos, pos{start: len(runes), end: -1})
infield = true
}
}
if r == '\\' {
if raw || esc {
runes = append(runes, r)
}
esc = !esc
continue
}
runes = append(runes, r)
esc = false
}
if len(fpos) == 0 {
return nil
}
if infield {
fpos[len(fpos)-1].end = len(runes)
}
switch {
case n == 1:
// include heading/trailing IFSs
fpos[0].start, fpos[0].end = 0, len(runes)
fpos = fpos[:1]
case n != -1 && n < len(fpos):
// combine to max n fields
fpos[n-1].end = fpos[len(fpos)-1].end
fpos = fpos[:n]
}
var fields = make([]string, len(fpos))
for i, p := range fpos {
fields[i] = string(runes[p.start:p.end])
}
return fields
}

View File

@ -1,12 +1,10 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package interp
package expand
import (
"context"
"fmt"
"os"
"regexp"
"sort"
"strconv"
@ -17,146 +15,133 @@ import (
"mvdan.cc/sh/syntax"
)
func anyOfLit(v interface{}, vals ...string) string {
word, _ := v.(*syntax.Word)
if word == nil || len(word.Parts) != 1 {
return ""
}
lit, ok := word.Parts[0].(*syntax.Lit)
if !ok {
return ""
}
for _, val := range vals {
if lit.Value == val {
return val
}
func nodeLit(node syntax.Node) string {
if word, ok := node.(*syntax.Word); ok {
return word.Lit()
}
return ""
}
// quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]}
func (r *Runner) quotedElems(pe *syntax.ParamExp) []string {
if pe == nil || pe.Excl || pe.Length || pe.Width {
return nil
}
if pe.Param.Value == "@" {
return r.Params
}
if anyOfLit(pe.Index, "@") == "" {
return nil
}
val, _ := r.lookupVar(pe.Param.Value)
if x, ok := val.Value.(IndexArray); ok {
return x
}
return nil
type UnsetParameterError struct {
Node *syntax.ParamExp
Message string
}
func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
func (u UnsetParameterError) Error() string {
return u.Message
}
func (cfg *Config) paramExp(pe *syntax.ParamExp) (string, error) {
oldParam := cfg.curParam
cfg.curParam = pe
defer func() { cfg.curParam = oldParam }()
name := pe.Param.Value
var vr Variable
set := false
index := pe.Index
switch name {
case "#":
vr.Value = StringVal(strconv.Itoa(len(r.Params)))
case "@", "*":
vr.Value = IndexArray(r.Params)
index = &syntax.Word{Parts: []syntax.WordPart{
&syntax.Lit{Value: name},
}}
case "?":
vr.Value = StringVal(strconv.Itoa(r.exit))
case "$":
vr.Value = StringVal(strconv.Itoa(os.Getpid()))
case "PPID":
vr.Value = StringVal(strconv.Itoa(os.Getppid()))
}
var vr Variable
switch name {
case "LINENO":
line := uint64(pe.Pos().Line())
vr.Value = StringVal(strconv.FormatUint(line, 10))
case "DIRSTACK":
vr.Value = IndexArray(r.dirStack)
// This is the only parameter expansion that the environment
// interface cannot satisfy.
line := uint64(cfg.curParam.Pos().Line())
vr.Value = strconv.FormatUint(line, 10)
default:
if n, err := strconv.Atoi(name); err == nil {
if i := n - 1; i < len(r.Params) {
vr.Value, set = StringVal(r.Params[i]), true
}
} else {
vr, set = r.lookupVar(name)
}
vr = cfg.Env.Get(name)
}
str := r.varStr(vr, 0)
if index != nil {
str = r.varInd(ctx, vr, index, 0)
orig := vr
_, vr = vr.Resolve(cfg.Env)
str, err := cfg.varInd(vr, index)
if err != nil {
return "", err
}
slicePos := func(expr syntax.ArithmExpr) int {
p := r.arithm(ctx, expr)
if p < 0 {
p = len(str) + p
if p < 0 {
p = len(str)
slicePos := func(n int) int {
if n < 0 {
n = len(str) + n
if n < 0 {
n = len(str)
}
} else if p > len(str) {
p = len(str)
} else if n > len(str) {
n = len(str)
}
return p
return n
}
elems := []string{str}
if anyOfLit(index, "@", "*") != "" {
switch nodeLit(index) {
case "@", "*":
switch x := vr.Value.(type) {
case nil:
elems = nil
case IndexArray:
case []string:
elems = x
}
}
switch {
case pe.Length:
n := len(elems)
if anyOfLit(index, "@", "*") == "" {
switch nodeLit(index) {
case "@", "*":
default:
n = utf8.RuneCountInString(str)
}
str = strconv.Itoa(n)
case pe.Excl:
var strs []string
if pe.Names != 0 {
strs = r.namesByPrefix(pe.Param.Value)
} else if vr.NameRef {
strs = append(strs, string(vr.Value.(StringVal)))
} else if x, ok := vr.Value.(IndexArray); ok {
strs = cfg.namesByPrefix(pe.Param.Value)
} else if orig.NameRef {
strs = append(strs, orig.Value.(string))
} else if x, ok := vr.Value.([]string); ok {
for i, e := range x {
if e != "" {
strs = append(strs, strconv.Itoa(i))
}
}
} else if x, ok := vr.Value.(AssocArray); ok {
} else if x, ok := vr.Value.(map[string]string); ok {
for k := range x {
strs = append(strs, k)
}
} else if str != "" {
vr, _ = r.lookupVar(str)
strs = append(strs, r.varStr(vr, 0))
vr = cfg.Env.Get(str)
strs = append(strs, vr.String())
}
sort.Strings(strs)
str = strings.Join(strs, " ")
case pe.Slice != nil:
if pe.Slice.Offset != nil {
offset := slicePos(pe.Slice.Offset)
str = str[offset:]
n, err := Arithm(cfg, pe.Slice.Offset)
if err != nil {
return "", err
}
str = str[slicePos(n):]
}
if pe.Slice.Length != nil {
length := slicePos(pe.Slice.Length)
str = str[:length]
n, err := Arithm(cfg, pe.Slice.Length)
if err != nil {
return "", err
}
str = str[:slicePos(n)]
}
case pe.Repl != nil:
orig := r.lonePattern(ctx, pe.Repl.Orig)
with := r.loneWord(ctx, pe.Repl.With)
orig, err := Pattern(cfg, pe.Repl.Orig)
if err != nil {
return "", err
}
with, err := Literal(cfg, pe.Repl.With)
if err != nil {
return "", err
}
n := 1
if pe.Repl.All {
n = -1
}
locs := findAllIndex(orig, str, n)
buf := r.strBuilder()
buf := cfg.strBuilder()
last := 0
for _, loc := range locs {
buf.WriteString(str[last:loc[0]])
@ -166,7 +151,10 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
buf.WriteString(str[last:])
str = buf.String()
case pe.Exp != nil:
arg := r.loneWord(ctx, pe.Exp.Word)
arg, err := Literal(cfg, pe.Exp.Word)
if err != nil {
return "", err
}
switch op := pe.Exp.Op; op {
case syntax.SubstColPlus:
if str == "" {
@ -174,11 +162,11 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
}
fallthrough
case syntax.SubstPlus:
if set {
if vr.IsSet() {
str = arg
}
case syntax.SubstMinus:
if set {
if vr.IsSet() {
break
}
fallthrough
@ -187,24 +175,25 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
str = arg
}
case syntax.SubstQuest:
if set {
if vr.IsSet() {
break
}
fallthrough
case syntax.SubstColQuest:
if str == "" {
r.errf("%s\n", arg)
r.exit = 1
r.setErr(ShellExitStatus(r.exit))
return "", UnsetParameterError{
Node: pe,
Message: arg,
}
}
case syntax.SubstAssgn:
if set {
if vr.IsSet() {
break
}
fallthrough
case syntax.SubstColAssgn:
if str == "" {
r.setVarString(ctx, name, arg)
cfg.envSet(name, arg)
str = arg
}
case syntax.RemSmallPrefix, syntax.RemLargePrefix,
@ -229,7 +218,7 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
// empty string means '?'; nothing to do there
expr, err := syntax.TranslatePattern(arg, false)
if err != nil {
return str
return str, nil
}
rx := regexp.MustCompile(expr)
@ -266,7 +255,7 @@ func (r *Runner) paramExp(ctx context.Context, pe *syntax.ParamExp) string {
}
}
}
return str
return str, nil
}
func removePattern(str, pattern string, fromEnd, greedy bool) string {
@ -293,3 +282,67 @@ func removePattern(str, pattern string, fromEnd, greedy bool) string {
}
return str
}
func (cfg *Config) varInd(vr Variable, idx syntax.ArithmExpr) (string, error) {
if idx == nil {
return vr.String(), nil
}
switch x := vr.Value.(type) {
case string:
n, err := Arithm(cfg, idx)
if err != nil {
return "", err
}
if n == 0 {
return x, nil
}
case []string:
switch nodeLit(idx) {
case "@":
return strings.Join(x, " "), nil
case "*":
return cfg.ifsJoin(x), nil
}
i, err := Arithm(cfg, idx)
if err != nil {
return "", err
}
if len(x) > 0 {
return x[i], nil
}
case map[string]string:
switch lit := nodeLit(idx); lit {
case "@", "*":
var strs []string
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
strs = append(strs, x[k])
}
if lit == "*" {
return cfg.ifsJoin(strs), nil
}
return strings.Join(strs, " "), nil
}
val, err := Literal(cfg, idx.(*syntax.Word))
if err != nil {
return "", err
}
return x[val], nil
}
return "", nil
}
func (cfg *Config) namesByPrefix(prefix string) []string {
var names []string
cfg.Env.Each(func(name string, vr Variable) bool {
if strings.HasPrefix(name, prefix) {
names = append(names, name)
}
return true
})
return names
}

View File

@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/syntax"
)
@ -29,6 +30,20 @@ func isBuiltin(name string) bool {
return false
}
func oneIf(b bool) int {
if b {
return 1
}
return 0
}
// atoi is just a shorthand for strconv.Atoi that ignores the error,
// just like shells do.
func atoi(s string) int {
n, _ := strconv.Atoi(s)
return n
}
func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, args []string) int {
switch name {
case "true", ":":
@ -91,7 +106,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
}
for _, arg := range args {
if _, ok := r.lookupVar(arg); ok && vars {
if vr := r.lookupVar(arg); vr.IsSet() && vars {
r.delVar(arg)
continue
}
@ -100,14 +115,14 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
}
}
case "echo":
newline, expand := true, false
newline, doExpand := true, false
echoOpts:
for len(args) > 0 {
switch args[0] {
case "-n":
newline = false
case "-e":
expand = true
doExpand = true
case "-E": // default
default:
break echoOpts
@ -118,8 +133,8 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
if i > 0 {
r.out(" ")
}
if expand {
_, arg, _ = r.expandFormat(arg, nil)
if doExpand {
arg, _, _ = expand.Format(r.ecfg, arg, nil)
}
r.out(arg)
}
@ -133,7 +148,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
}
format, args := args[0], args[1:]
for {
n, s, err := r.expandFormat(format, args)
s, n, err := expand.Format(r.ecfg, format, args)
if err != nil {
r.errf("%v\n", err)
return 1
@ -144,49 +159,35 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
break
}
}
case "break":
case "break", "continue":
if !r.inLoop {
r.errf("break is only useful in a loop")
r.errf("%s is only useful in a loop", name)
break
}
enclosing := &r.breakEnclosing
if name == "continue" {
enclosing = &r.contnEnclosing
}
switch len(args) {
case 0:
r.breakEnclosing = 1
*enclosing = 1
case 1:
if n, err := strconv.Atoi(args[0]); err == nil {
r.breakEnclosing = n
*enclosing = n
break
}
fallthrough
default:
r.errf("usage: break [n]\n")
return 2
}
case "continue":
if !r.inLoop {
r.errf("continue is only useful in a loop")
break
}
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")
r.errf("usage: %s [n]\n", name)
return 2
}
case "pwd":
r.outf("%s\n", r.getVar("PWD"))
r.outf("%s\n", r.envGet("PWD"))
case "cd":
var path string
switch len(args) {
case 0:
path = r.getVar("HOME")
path = r.envGet("HOME")
case 1:
path = args[0]
default:
@ -462,13 +463,13 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
args = append(args, "REPLY")
}
values := r.ifsFields(string(line), len(args), raw)
values := expand.ReadFields(r.ecfg, string(line), len(args), raw)
for i, name := range args {
val := ""
if i < len(values) {
val = values[i]
}
r.setVar(ctx, name, nil, Variable{Value: StringVal(val)})
r.setVar(name, nil, expand.Variable{Value: val})
}
return 0
@ -478,7 +479,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
r.errf("getopts: usage: getopts optstring name [arg]\n")
return 2
}
optind, _ := strconv.Atoi(r.getVar("OPTIND"))
optind, _ := strconv.Atoi(r.envGet("OPTIND"))
if optind-1 != r.optState.argidx {
if optind < 1 {
optind = 1
@ -499,7 +500,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
opt, optarg, done := r.optState.Next(optstr, args)
r.setVarString(ctx, name, string(opt))
r.setVarString(name, string(opt))
r.delVar("OPTARG")
switch {
case opt == '?' && diagnostics && !done:
@ -508,11 +509,11 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
r.errf("getopts: option requires an argument -- %q\n", optarg)
default:
if optarg != "" {
r.setVarString(ctx, "OPTARG", optarg)
r.setVarString("OPTARG", optarg)
}
}
if optind-1 != r.optState.argidx {
r.setVarString(ctx, "OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
}
return oneIf(done)
@ -559,6 +560,7 @@ func (r *Runner) builtinCode(ctx context.Context, pos syntax.Pos, name string, a
r.printOptLine(arg, *opt)
}
}
r.updateExpandOpts()
default:
// "trap", "umask", "alias", "unalias", "fg", "bg",
@ -575,62 +577,6 @@ func (r *Runner) printOptLine(name string, enabled bool) {
r.outf("%s\t%s\n", name, status)
}
func (r *Runner) ifsFields(s string, n int, raw bool) []string {
type pos struct {
start, end int
}
var fpos []pos
runes := make([]rune, 0, len(s))
infield := false
esc := false
for _, c := range s {
if infield {
if r.ifsRune(c) && (raw || !esc) {
fpos[len(fpos)-1].end = len(runes)
infield = false
}
} else {
if !r.ifsRune(c) && (raw || !esc) {
fpos = append(fpos, pos{start: len(runes), end: -1})
infield = true
}
}
if c == '\\' {
if raw || esc {
runes = append(runes, c)
}
esc = !esc
continue
}
runes = append(runes, c)
esc = false
}
if len(fpos) == 0 {
return nil
}
if infield {
fpos[len(fpos)-1].end = len(runes)
}
switch {
case n == 1:
// include heading/trailing IFSs
fpos[0].start, fpos[0].end = 0, len(runes)
fpos = fpos[:1]
case n != -1 && n < len(fpos):
// combine to max n fields
fpos[n-1].end = fpos[len(fpos)-1].end
fpos = fpos[:n]
}
var fields = make([]string, len(fpos))
for i, p := range fpos {
fields[i] = string(runes[p.start:p.end])
}
return fields
}
func (r *Runner) readLine(raw bool) ([]byte, error) {
var line []byte
esc := false
@ -675,7 +621,7 @@ func (r *Runner) changeDir(path string) int {
}
r.Dir = path
r.Vars["OLDPWD"] = r.Vars["PWD"]
r.Vars["PWD"] = Variable{Value: StringVal(path)}
r.Vars["PWD"] = expand.Variable{Value: path}
return 0
}

View File

@ -4,7 +4,4 @@
// Package interp implements an interpreter that executes shell
// programs. It aims to support POSIX, but its support is not complete
// yet. It also supports some Bash features.
//
// This package is a work in progress and EXPERIMENTAL; its API is not
// subject to the 1.x backwards compatibility guarantee.
package interp

View File

@ -1,508 +0,0 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package interp
import (
"context"
"fmt"
"os"
"os/user"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
"mvdan.cc/sh/syntax"
)
func (r *Runner) expandFormat(format string, args []string) (int, string, error) {
buf := r.strBuilder()
esc := false
var fmts []rune
initialArgs := len(args)
for _, c := range format {
switch {
case esc:
esc = false
switch c {
case 'n':
buf.WriteRune('\n')
case 'r':
buf.WriteRune('\r')
case 't':
buf.WriteRune('\t')
case '\\':
buf.WriteRune('\\')
default:
buf.WriteRune('\\')
buf.WriteRune(c)
}
case len(fmts) > 0:
switch c {
case '%':
buf.WriteByte('%')
fmts = nil
case 'c':
var b byte
if len(args) > 0 {
arg := ""
arg, args = args[0], args[1:]
if len(arg) > 0 {
b = arg[0]
}
}
buf.WriteByte(b)
fmts = nil
case '+', '-', ' ':
if len(fmts) > 1 {
return 0, "", fmt.Errorf("invalid format char: %c", c)
}
fmts = append(fmts, c)
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
fmts = append(fmts, c)
case 's', 'd', 'i', 'u', 'o', 'x':
arg := ""
if len(args) > 0 {
arg, args = args[0], args[1:]
}
var farg interface{} = arg
if c != 's' {
n, _ := strconv.ParseInt(arg, 0, 0)
if c == 'i' || c == 'd' {
farg = int(n)
} else {
farg = uint(n)
}
if c == 'i' || c == 'u' {
c = 'd'
}
}
fmts = append(fmts, c)
fmt.Fprintf(buf, string(fmts), farg)
fmts = nil
default:
return 0, "", fmt.Errorf("invalid format char: %c", c)
}
case c == '\\':
esc = true
case args != nil && c == '%':
// if args == nil, we are not doing format
// arguments
fmts = []rune{c}
default:
buf.WriteRune(c)
}
}
if len(fmts) > 0 {
return 0, "", fmt.Errorf("missing format char")
}
return initialArgs - len(args), buf.String(), nil
}
func (r *Runner) fieldJoin(parts []fieldPart) string {
switch len(parts) {
case 0:
return ""
case 1: // short-cut without a string copy
return parts[0].val
}
buf := r.strBuilder()
for _, part := range parts {
buf.WriteString(part.val)
}
return buf.String()
}
func (r *Runner) escapedGlobField(parts []fieldPart) (escaped string, glob bool) {
buf := r.strBuilder()
for _, part := range parts {
quoted := syntax.QuotePattern(part.val)
if quoted != part.val {
if part.quote > quoteNone {
buf.WriteString(quoted)
} else {
buf.WriteString(part.val)
glob = true
}
}
}
if glob { // only copy the string if it will be used
escaped = buf.String()
}
return escaped, glob
}
func (r *Runner) Fields(ctx context.Context, words ...*syntax.Word) ([]string, error) {
if !r.didReset {
r.Reset()
}
return r.fields(ctx, words...), r.err
}
func (r *Runner) fields(ctx context.Context, words ...*syntax.Word) []string {
fields := make([]string, 0, len(words))
baseDir := syntax.QuotePattern(r.Dir)
for _, word := range words {
for _, expWord := range syntax.ExpandBraces(word) {
for _, field := range r.wordFields(ctx, expWord.Parts) {
path, doGlob := r.escapedGlobField(field)
var matches []string
abs := filepath.IsAbs(path)
if doGlob && !r.opts[optNoGlob] {
if !abs {
path = filepath.Join(baseDir, path)
}
matches = glob(path, r.opts[optGlobStar])
}
if len(matches) == 0 {
fields = append(fields, r.fieldJoin(field))
continue
}
for _, match := range matches {
if !abs {
endSeparator := strings.HasSuffix(match, string(filepath.Separator))
match, _ = filepath.Rel(r.Dir, match)
if endSeparator {
match += string(filepath.Separator)
}
}
fields = append(fields, match)
}
}
}
}
return fields
}
func (r *Runner) loneWord(ctx context.Context, word *syntax.Word) string {
if word == nil {
return ""
}
field := r.wordField(ctx, word.Parts, quoteDouble)
return r.fieldJoin(field)
}
func (r *Runner) lonePattern(ctx context.Context, word *syntax.Word) string {
field := r.wordField(ctx, word.Parts, quoteSingle)
buf := r.strBuilder()
for _, part := range field {
if part.quote > quoteNone {
buf.WriteString(syntax.QuotePattern(part.val))
} else {
buf.WriteString(part.val)
}
}
return buf.String()
}
func (r *Runner) expandAssigns(ctx context.Context, as *syntax.Assign) []*syntax.Assign {
// Convert "declare $x" into "declare value".
// Don't use syntax.Parser here, as we only want the basic
// splitting by '='.
if as.Name != nil {
return []*syntax.Assign{as} // nothing to do
}
var asgns []*syntax.Assign
for _, field := range r.fields(ctx, as.Value) {
as := &syntax.Assign{}
parts := strings.SplitN(field, "=", 2)
as.Name = &syntax.Lit{Value: parts[0]}
if len(parts) == 1 {
as.Naked = true
} else {
as.Value = &syntax.Word{Parts: []syntax.WordPart{
&syntax.Lit{Value: parts[1]},
}}
}
asgns = append(asgns, as)
}
return asgns
}
type fieldPart struct {
val string
quote quoteLevel
}
type quoteLevel uint
const (
quoteNone quoteLevel = iota
quoteDouble
quoteSingle
)
func (r *Runner) wordField(ctx context.Context, wps []syntax.WordPart, ql quoteLevel) []fieldPart {
var field []fieldPart
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 {
s = r.expandUser(s)
}
if ql == quoteDouble && strings.Contains(s, "\\") {
buf := r.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' && i+1 < len(s) {
switch s[i+1] {
case '\n': // remove \\\n
i++
continue
case '"', '\\', '$', '`': // special chars
continue
}
}
buf.WriteByte(b)
}
s = buf.String()
}
field = append(field, fieldPart{val: s})
case *syntax.SglQuoted:
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
_, fp.val, _ = r.expandFormat(fp.val, nil)
}
field = append(field, fp)
case *syntax.DblQuoted:
for _, part := range r.wordField(ctx, x.Parts, quoteDouble) {
part.quote = quoteDouble
field = append(field, part)
}
case *syntax.ParamExp:
field = append(field, fieldPart{val: r.paramExp(ctx, x)})
case *syntax.CmdSubst:
field = append(field, fieldPart{val: r.cmdSubst(ctx, x)})
case *syntax.ArithmExp:
field = append(field, fieldPart{
val: strconv.Itoa(r.arithm(ctx, x.X)),
})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
return field
}
func (r *Runner) cmdSubst(ctx context.Context, cs *syntax.CmdSubst) string {
r2 := r.sub()
buf := r.strBuilder()
r2.Stdout = buf
r2.stmts(ctx, cs.StmtList)
r.setErr(r2.err)
return strings.TrimRight(buf.String(), "\n")
}
func (r *Runner) wordFields(ctx context.Context, wps []syntax.WordPart) [][]fieldPart {
fields := r.fieldsAlloc[:0]
curField := r.fieldAlloc[:0]
allowEmpty := false
flush := func() {
if len(curField) == 0 {
return
}
fields = append(fields, curField)
curField = nil
}
splitAdd := func(val string) {
for i, field := range strings.FieldsFunc(val, r.ifsRune) {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{val: field})
}
}
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 {
s = r.expandUser(s)
}
if strings.Contains(s, "\\") {
buf := r.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' {
i++
b = s[i]
}
buf.WriteByte(b)
}
s = buf.String()
}
curField = append(curField, fieldPart{val: s})
case *syntax.SglQuoted:
allowEmpty = true
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
_, fp.val, _ = r.expandFormat(fp.val, nil)
}
curField = append(curField, fp)
case *syntax.DblQuoted:
allowEmpty = true
if len(x.Parts) == 1 {
pe, _ := x.Parts[0].(*syntax.ParamExp)
if elems := r.quotedElems(pe); elems != nil {
for i, elem := range elems {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{
quote: quoteDouble,
val: elem,
})
}
continue
}
}
for _, part := range r.wordField(ctx, x.Parts, quoteDouble) {
part.quote = quoteDouble
curField = append(curField, part)
}
case *syntax.ParamExp:
splitAdd(r.paramExp(ctx, x))
case *syntax.CmdSubst:
splitAdd(r.cmdSubst(ctx, x))
case *syntax.ArithmExp:
curField = append(curField, fieldPart{
val: strconv.Itoa(r.arithm(ctx, x.X)),
})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
flush()
if allowEmpty && len(fields) == 0 {
fields = append(fields, curField)
}
return fields
}
func (r *Runner) expandUser(field string) string {
if len(field) == 0 || field[0] != '~' {
return field
}
name := field[1:]
rest := ""
if i := strings.Index(name, "/"); i >= 0 {
rest = name[i:]
name = name[:i]
}
if name == "" {
return r.getVar("HOME") + rest
}
u, err := user.Lookup(name)
if err != nil {
return field
}
return u.HomeDir + rest
}
func match(pattern, name string) bool {
expr, err := syntax.TranslatePattern(pattern, true)
if err != nil {
return false
}
rx := regexp.MustCompile("^" + expr + "$")
return rx.MatchString(name)
}
func findAllIndex(pattern, name string, n int) [][]int {
expr, err := syntax.TranslatePattern(pattern, true)
if err != nil {
return nil
}
rx := regexp.MustCompile(expr)
return rx.FindAllStringIndex(name, n)
}
func hasGlob(path string) bool {
magicChars := `*?[`
if runtime.GOOS != "windows" {
magicChars = `*?[\`
}
return strings.ContainsAny(path, magicChars)
}
var rxGlobStar = regexp.MustCompile(".*")
func glob(pattern string, globStar bool) []string {
parts := strings.Split(pattern, string(filepath.Separator))
matches := []string{"."}
if filepath.IsAbs(pattern) {
if parts[0] == "" {
// unix-like
matches[0] = string(filepath.Separator)
} else {
// windows (for some reason it won't work without the
// trailing separator)
matches[0] = parts[0] + string(filepath.Separator)
}
parts = parts[1:]
}
for _, part := range parts {
if part == "**" && globStar {
for i := range matches {
// "a/**" should match "a/ a/b a/b/c ..."; note
// how the zero-match case has a trailing
// separator.
matches[i] += string(filepath.Separator)
}
// expand all the possible levels of **
latest := matches
for {
var newMatches []string
for _, dir := range latest {
newMatches = globDir(dir, rxGlobStar, newMatches)
}
if len(newMatches) == 0 {
// not another level of directories to
// try; stop
break
}
matches = append(matches, newMatches...)
latest = newMatches
}
continue
}
expr, err := syntax.TranslatePattern(part, true)
if err != nil {
return nil
}
rx := regexp.MustCompile("^" + expr + "$")
var newMatches []string
for _, dir := range matches {
newMatches = globDir(dir, rx, newMatches)
}
matches = newMatches
}
return matches
}
func globDir(dir string, rx *regexp.Regexp, matches []string) []string {
d, err := os.Open(dir)
if err != nil {
return nil
}
defer d.Close()
names, _ := d.Readdirnames(-1)
sort.Strings(names)
for _, name := range names {
if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' {
continue
}
if rx.MatchString(name) {
matches = append(matches, filepath.Join(dir, name))
}
}
return matches
}

View File

@ -13,6 +13,7 @@ import (
"os"
"os/user"
"path/filepath"
"regexp"
"runtime"
"strings"
"sync"
@ -20,6 +21,7 @@ import (
"golang.org/x/sync/errgroup"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/syntax"
)
@ -46,10 +48,10 @@ func New(opts ...func(*Runner) error) (*Runner, error) {
}
}
if r.Exec == nil {
Module(nil)(r)
Module(ModuleExec(nil))(r)
}
if r.Open == nil {
Module(nil)(r)
Module(ModuleOpen(nil))(r)
}
if r.Stdout == nil || r.Stderr == nil {
StdIO(r.Stdin, r.Stdout, r.Stderr)(r)
@ -57,12 +59,127 @@ func New(opts ...func(*Runner) error) (*Runner, error) {
return r, nil
}
// Env sets the interpreter's environment. If nil, the current process's
// environment is used.
func Env(env Environ) func(*Runner) error {
func (r *Runner) fillExpandConfig(ctx context.Context) {
r.ectx = ctx
r.ecfg = &expand.Config{
Env: expandEnv{r},
CmdSubst: func(w io.Writer, cs *syntax.CmdSubst) error {
switch len(cs.Stmts) {
case 0: // nothing to do
return nil
case 1: // $(<file)
word := catShortcutArg(cs.Stmts[0])
if word == nil {
break
}
path := r.literal(word)
f, err := r.open(ctx, r.relPath(path), os.O_RDONLY, 0, true)
if err != nil {
return err
}
_, err = io.Copy(w, f)
return err
}
r2 := r.sub()
r2.Stdout = w
r2.stmts(ctx, cs.StmtList)
return r2.err
},
ReadDir: ioutil.ReadDir,
}
r.updateExpandOpts()
}
// catShortcutArg checks if a statement is of the form "$(<file)". The redirect
// word is returned if there's a match, and nil otherwise.
func catShortcutArg(stmt *syntax.Stmt) *syntax.Word {
if stmt.Cmd != nil || stmt.Negated || stmt.Background || stmt.Coprocess {
return nil
}
if len(stmt.Redirs) != 1 {
return nil
}
redir := stmt.Redirs[0]
if redir.Op != syntax.RdrIn {
return nil
}
return redir.Word
}
func (r *Runner) updateExpandOpts() {
r.ecfg.NoGlob = r.opts[optNoGlob]
r.ecfg.GlobStar = r.opts[optGlobStar]
}
func (r *Runner) expandErr(err error) {
switch err := err.(type) {
case nil:
case expand.UnsetParameterError:
r.errf("%s\n", err.Message)
r.exit = 1
r.setErr(ShellExitStatus(r.exit))
default:
r.setErr(err)
r.exit = 1
}
}
func (r *Runner) arithm(expr syntax.ArithmExpr) int {
n, err := expand.Arithm(r.ecfg, expr)
r.expandErr(err)
return n
}
func (r *Runner) fields(words ...*syntax.Word) []string {
strs, err := expand.Fields(r.ecfg, words...)
r.expandErr(err)
return strs
}
func (r *Runner) literal(word *syntax.Word) string {
str, err := expand.Literal(r.ecfg, word)
r.expandErr(err)
return str
}
func (r *Runner) document(word *syntax.Word) string {
str, err := expand.Document(r.ecfg, word)
r.expandErr(err)
return str
}
func (r *Runner) pattern(word *syntax.Word) string {
str, err := expand.Pattern(r.ecfg, word)
r.expandErr(err)
return str
}
// expandEnv exposes Runner's variables to the expand package.
type expandEnv struct {
r *Runner
}
func (e expandEnv) Get(name string) expand.Variable {
return e.r.lookupVar(name)
}
func (e expandEnv) Set(name string, vr expand.Variable) {
e.r.setVarInternal(name, vr)
}
func (e expandEnv) Each(fn func(name string, vr expand.Variable) bool) {
e.r.Env.Each(fn)
for name, vr := range e.r.Vars {
if !fn(name, vr) {
return
}
}
}
// Env sets the interpreter's environment. If nil, a copy of the current
// process's environment is used.
func Env(env expand.Environ) func(*Runner) error {
return func(r *Runner) error {
if env == nil {
env, _ = EnvFromList(os.Environ())
env = expand.ListEnviron(os.Environ()...)
}
r.Env = env
return nil
@ -143,6 +260,7 @@ func Params(args ...string) func(*Runner) error {
args = args[1:]
}
r.Params = args
r.updateExpandOpts()
return nil
}
}
@ -203,7 +321,7 @@ func StdIO(in io.Reader, out, err io.Writer) func(*Runner) error {
type Runner struct {
// Env specifies the environment of the interpreter, which must be
// non-nil.
Env Environ
Env expand.Environ
// Dir specifies the working directory of the command, which must be an
// absolute path.
@ -223,12 +341,15 @@ type Runner struct {
Stdout io.Writer
Stderr io.Writer
// Separate maps, note that bash allows a name to be both a var
// and a func simultaneously
// TODO: merge into Env?
Vars map[string]Variable
// Separate maps - note that bash allows a name to be both a var and a
// func simultaneously
Vars map[string]expand.Variable
Funcs map[string]*syntax.Stmt
ecfg *expand.Config
ectx context.Context // just so that Runner.Sub can use it again
// didReset remembers whether the runner has ever been reset. This is
// used so that Reset is automatically called when running any program
// or node for the first time on a Runner.
@ -239,7 +360,7 @@ type Runner struct {
filename string // only if Node was a File
// like Vars, but local to a func i.e. "local foo=bar"
funcVars map[string]Variable
funcVars map[string]expand.Variable
// like Vars, but local to a cmd i.e. "foo=bar prog args..."
cmdVars map[string]string
@ -262,9 +383,6 @@ type Runner struct {
optState getopts
ifsJoin string
ifsRune func(rune) bool
// keepRedirs is used so that "exec" can make any redirections
// apply to the current shell, and not just the command.
keepRedirs bool
@ -281,17 +399,6 @@ type Runner struct {
// On Windows, the kill signal is always sent immediately,
// because Go doesn't currently support sending Interrupt on Windows.
KillTimeout time.Duration
fieldAlloc [4]fieldPart
fieldsAlloc [4][]fieldPart
bufferAlloc bytes.Buffer
oneWord [1]*syntax.Word
}
func (r *Runner) strBuilder() *bytes.Buffer {
b := &r.bufferAlloc
b.Reset()
return b
}
func (r *Runner) optByFlag(flag string) *bool {
@ -380,7 +487,7 @@ func (r *Runner) Reset() {
usedNew: r.usedNew,
}
if r.Vars == nil {
r.Vars = make(map[string]Variable)
r.Vars = make(map[string]expand.Variable)
} else {
for k := range r.Vars {
delete(r.Vars, k)
@ -393,29 +500,22 @@ func (r *Runner) Reset() {
delete(r.cmdVars, k)
}
}
if _, ok := r.Env.Get("HOME"); !ok {
if vr := r.Env.Get("HOME"); !vr.IsSet() {
u, _ := user.Current()
r.Vars["HOME"] = Variable{Value: StringVal(u.HomeDir)}
r.Vars["HOME"] = expand.Variable{Value: u.HomeDir}
}
r.Vars["PWD"] = Variable{Value: StringVal(r.Dir)}
r.Vars["IFS"] = Variable{Value: StringVal(" \t\n")}
r.ifsUpdated()
r.Vars["OPTIND"] = Variable{Value: StringVal("1")}
r.Vars["PWD"] = expand.Variable{Value: r.Dir}
r.Vars["IFS"] = expand.Variable{Value: " \t\n"}
r.Vars["OPTIND"] = expand.Variable{Value: "1"}
if runtime.GOOS == "windows" {
// convert $PATH to a unix path list
path, _ := r.Env.Get("PATH")
path := r.Env.Get("PATH").String()
path = strings.Join(filepath.SplitList(path), ":")
r.Vars["PATH"] = Variable{Value: StringVal(path)}
r.Vars["PATH"] = expand.Variable{Value: path}
}
r.dirStack = append(r.dirStack, r.Dir)
if r.Exec == nil {
r.Exec = DefaultExec
}
if r.Open == nil {
r.Open = DefaultOpen
}
if r.KillTimeout == 0 {
r.KillTimeout = 2 * time.Second
}
@ -424,23 +524,26 @@ func (r *Runner) Reset() {
func (r *Runner) modCtx(ctx context.Context) context.Context {
mc := ModuleCtx{
Env: r.Env,
Dir: r.Dir,
Stdin: r.Stdin,
Stdout: r.Stdout,
Stderr: r.Stderr,
KillTimeout: r.KillTimeout,
}
mc.Env = r.Env.Copy()
oenv := overlayEnviron{
parent: r.Env,
values: make(map[string]expand.Variable),
}
for name, vr := range r.Vars {
if !vr.Exported {
continue
}
mc.Env.Set(name, r.varStr(vr, 0))
oenv.Set(name, vr)
}
for name, val := range r.cmdVars {
mc.Env.Set(name, val)
for name, vr := range r.funcVars {
oenv.Set(name, vr)
}
for name, value := range r.cmdVars {
oenv.Set(name, expand.Variable{Exported: true, Value: value})
}
mc.Env = oenv
return context.WithValue(ctx, moduleCtxKey{}, mc)
}
@ -471,6 +574,7 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
if !r.didReset {
r.Reset()
}
r.fillExpandConfig(ctx)
r.err = nil
r.filename = ""
switch x := node.(type) {
@ -482,7 +586,7 @@ func (r *Runner) Run(ctx context.Context, node syntax.Node) error {
case syntax.Command:
r.cmd(ctx, x)
default:
return fmt.Errorf("Node can only be File, Stmt, or Command: %T", x)
return fmt.Errorf("node can only be File, Stmt, or Command: %T", x)
}
if r.exit > 0 {
r.setErr(ExitStatus(r.exit))
@ -564,6 +668,7 @@ func (r *Runner) sub() *Runner {
// Keep in sync with the Runner type. Manually copy fields, to not copy
// sensitive ones like errgroup.Group, and to do deep copies of slices.
r2 := &Runner{
Env: r.Env,
Dir: r.Dir,
Params: r.Params,
Exec: r.Exec,
@ -576,19 +681,20 @@ func (r *Runner) sub() *Runner {
filename: r.filename,
opts: r.opts,
}
// TODO: perhaps we could do a lazy copy here, or some sort of
// overlay to avoid copying all the time
r2.Env = r.Env.Copy()
r2.Vars = make(map[string]Variable, len(r.Vars))
r2.Vars = make(map[string]expand.Variable, len(r.Vars))
for k, v := range r.Vars {
r2.Vars[k] = v
}
r2.funcVars = make(map[string]expand.Variable, len(r.funcVars))
for k, v := range r.funcVars {
r2.funcVars[k] = v
}
r2.cmdVars = make(map[string]string, len(r.cmdVars))
for k, v := range r.cmdVars {
r2.cmdVars[k] = v
}
r2.dirStack = append([]string(nil), r.dirStack...)
r2.ifsUpdated()
r2.fillExpandConfig(r.ectx)
r2.didReset = true
return r2
}
@ -606,23 +712,19 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
r.exit = r2.exit
r.setErr(r2.err)
case *syntax.CallExpr:
fields := r.fields(ctx, x.Args...)
fields := r.fields(x.Args...)
if len(fields) == 0 {
for _, as := range x.Assigns {
vr, _ := r.lookupVar(as.Name.Value)
vr.Value = r.assignVal(ctx, as, "")
r.setVar(ctx, as.Name.Value, as.Index, vr)
vr := r.lookupVar(as.Name.Value)
vr.Value = r.assignVal(as, "")
r.setVar(as.Name.Value, as.Index, vr)
}
break
}
for _, as := range x.Assigns {
val := r.assignVal(ctx, as, "")
val := r.assignVal(as, "")
// we know that inline vars must be strings
r.cmdVars[as.Name.Value] = string(val.(StringVal))
if as.Name.Value == "IFS" {
r.ifsUpdated()
defer r.ifsUpdated()
}
r.cmdVars[as.Name.Value] = val.(string)
}
r.call(ctx, x.Args[0].Pos(), fields)
// cmdVars can be nuked here, as they are never useful
@ -689,37 +791,37 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
switch y := x.Loop.(type) {
case *syntax.WordIter:
name := y.Name.Value
for _, field := range r.fields(ctx, y.Items...) {
r.setVarString(ctx, name, field)
for _, field := range r.fields(y.Items...) {
r.setVarString(name, field)
if r.loopStmtsBroken(ctx, x.Do) {
break
}
}
case *syntax.CStyleLoop:
r.arithm(ctx, y.Init)
for r.arithm(ctx, y.Cond) != 0 {
r.arithm(y.Init)
for r.arithm(y.Cond) != 0 {
if r.loopStmtsBroken(ctx, x.Do) {
break
}
r.arithm(ctx, y.Post)
r.arithm(y.Post)
}
}
case *syntax.FuncDecl:
r.setFunc(x.Name.Value, x.Body)
case *syntax.ArithmCmd:
r.exit = oneIf(r.arithm(ctx, x.X) == 0)
r.exit = oneIf(r.arithm(x.X) == 0)
case *syntax.LetClause:
var val int
for _, expr := range x.Exprs {
val = r.arithm(ctx, expr)
val = r.arithm(expr)
}
r.exit = oneIf(val == 0)
case *syntax.CaseClause:
str := r.loneWord(ctx, x.Word)
str := r.literal(x.Word)
for _, ci := range x.Items {
for _, word := range ci.Patterns {
pat := r.lonePattern(ctx, word)
if match(pat, str) {
pattern := r.pattern(word)
if match(pattern, str) {
r.stmts(ctx, ci.StmtList)
return
}
@ -732,13 +834,13 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
r.exit = 1
}
case *syntax.DeclClause:
local := false
local, global := false, false
var modes []string
valType := ""
switch x.Variant.Value {
case "declare":
// When used in a function, "declare" acts as
// "local" unless the "-g" option is used.
// When used in a function, "declare" acts as "local"
// unless the "-g" option is used.
local = r.inFunc
case "local":
if !r.inFunc {
@ -755,13 +857,13 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
modes = append(modes, "-n")
}
for _, opt := range x.Opts {
switch s := r.loneWord(ctx, opt); s {
switch s := r.literal(opt); s {
case "-x", "-r", "-n":
modes = append(modes, s)
case "-a", "-A":
valType = s
case "-g":
local = false
global = true
default:
r.errf("declare: invalid option %q\n", s)
r.exit = 2
@ -769,11 +871,20 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
}
}
for _, as := range x.Assigns {
for _, as := range r.expandAssigns(ctx, as) {
for _, as := range r.flattenAssign(as) {
name := as.Name.Value
vr, _ := r.lookupVar(as.Name.Value)
vr.Value = r.assignVal(ctx, as, valType)
vr.Local = local
if !syntax.ValidName(name) {
r.errf("declare: invalid name %q\n", name)
r.exit = 1
return
}
vr := r.lookupVar(as.Name.Value)
vr.Value = r.assignVal(as, valType)
if global {
vr.Local = false
} else if local {
vr.Local = true
}
for _, mode := range modes {
switch mode {
case "-x":
@ -784,7 +895,7 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
vr.NameRef = true
}
}
r.setVar(ctx, name, as.Index, vr)
r.setVar(name, as.Index, vr)
}
}
case *syntax.TimeClause:
@ -808,6 +919,39 @@ func (r *Runner) cmd(ctx context.Context, cm syntax.Command) {
}
}
func (r *Runner) flattenAssign(as *syntax.Assign) []*syntax.Assign {
// Convert "declare $x" into "declare value".
// Don't use syntax.Parser here, as we only want the basic
// splitting by '='.
if as.Name != nil {
return []*syntax.Assign{as} // nothing to do
}
var asgns []*syntax.Assign
for _, field := range r.fields(as.Value) {
as := &syntax.Assign{}
parts := strings.SplitN(field, "=", 2)
as.Name = &syntax.Lit{Value: parts[0]}
if len(parts) == 1 {
as.Naked = true
} else {
as.Value = &syntax.Word{Parts: []syntax.WordPart{
&syntax.Lit{Value: parts[1]},
}}
}
asgns = append(asgns, as)
}
return asgns
}
func match(pattern, name string) bool {
expr, err := syntax.TranslatePattern(pattern, true)
if err != nil {
return false
}
rx := regexp.MustCompile("^" + expr + "$")
return rx.MatchString(name)
}
func elapsedString(d time.Duration, posix bool) string {
if posix {
return fmt.Sprintf("%.2f", d.Seconds())
@ -823,10 +967,42 @@ func (r *Runner) stmts(ctx context.Context, sl syntax.StmtList) {
}
}
func (r *Runner) hdocReader(rd *syntax.Redirect) io.Reader {
if rd.Op != syntax.DashHdoc {
hdoc := r.document(rd.Hdoc)
return strings.NewReader(hdoc)
}
var buf bytes.Buffer
var cur []syntax.WordPart
flushLine := func() {
if buf.Len() > 0 {
buf.WriteByte('\n')
}
buf.WriteString(r.document(&syntax.Word{Parts: cur}))
cur = cur[:0]
}
for _, wp := range rd.Hdoc.Parts {
lit, ok := wp.(*syntax.Lit)
if !ok {
cur = append(cur, wp)
continue
}
for i, part := range strings.Split(lit.Value, "\n") {
if i > 0 {
flushLine()
cur = cur[:0]
}
part = strings.TrimLeft(part, "\t")
cur = append(cur, &syntax.Lit{Value: part})
}
}
flushLine()
return &buf
}
func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, error) {
if rd.Hdoc != nil {
hdoc := r.loneWord(ctx, rd.Hdoc)
r.Stdin = strings.NewReader(hdoc)
r.Stdin = r.hdocReader(rd)
return nil, nil
}
orig := &r.Stdout
@ -837,7 +1013,7 @@ func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, err
orig = &r.Stderr
}
}
arg := r.loneWord(ctx, rd.Word)
arg := r.literal(rd.Word)
switch rd.Op {
case syntax.WordHdoc:
r.Stdin = strings.NewReader(arg + "\n")
@ -860,9 +1036,9 @@ func (r *Runner) redir(ctx context.Context, rd *syntax.Redirect) (io.Closer, err
mode := os.O_RDONLY
switch rd.Op {
case syntax.AppOut, syntax.AppAll:
mode = os.O_RDWR | os.O_CREATE | os.O_APPEND
mode = os.O_WRONLY | os.O_CREATE | os.O_APPEND
case syntax.RdrOut, syntax.RdrAll:
mode = os.O_RDWR | os.O_CREATE | os.O_TRUNC
mode = os.O_WRONLY | os.O_CREATE | os.O_TRUNC
}
f, err := r.open(ctx, r.relPath(arg), mode, 0644, true)
if err != nil {
@ -1039,7 +1215,7 @@ func splitList(path string) []string {
}
func (r *Runner) lookPath(file string) string {
pathList := splitList(r.getVar("PATH"))
pathList := splitList(r.envGet("PATH"))
chars := `/`
if runtime.GOOS == "windows" {
chars = `:\/`
@ -1070,7 +1246,7 @@ func (r *Runner) pathExts() []string {
if runtime.GOOS != "windows" {
return nil
}
pathext := r.getVar("PATHEXT")
pathext := r.envGet("PATHEXT")
if pathext == "" {
return []string{".com", ".exe", ".bat", ".cmd"}
}

View File

@ -13,6 +13,8 @@ import (
"strings"
"syscall"
"time"
"mvdan.cc/sh/expand"
)
// FromModuleContext returns the ModuleCtx value stored in ctx, if any.
@ -27,7 +29,7 @@ type moduleCtxKey struct{}
// It contains some of the current state of the Runner, as well as some fields
// necessary to implement some of the modules.
type ModuleCtx struct {
Env Environ
Env expand.Environ
Dir string
Stdin io.Reader
Stdout io.Writer

View File

@ -19,22 +19,22 @@ import (
func (r *Runner) bashTest(ctx context.Context, expr syntax.TestExpr, classic bool) string {
switch x := expr.(type) {
case *syntax.Word:
return r.loneWord(ctx, x)
return r.literal(x)
case *syntax.ParenTest:
return r.bashTest(ctx, x.X, classic)
case *syntax.BinaryTest:
switch x.Op {
case syntax.TsMatch, syntax.TsNoMatch:
str := r.loneWord(ctx, x.X.(*syntax.Word))
str := r.literal(x.X.(*syntax.Word))
yw := x.Y.(*syntax.Word)
if classic { // test, [
lit := r.loneWord(ctx, yw)
lit := r.literal(yw)
if (str == lit) == (x.Op == syntax.TsMatch) {
return "1"
}
} else { // [[
pat := r.lonePattern(ctx, yw)
if match(pat, str) == (x.Op == syntax.TsMatch) {
pattern := r.pattern(yw)
if match(pattern, str) == (x.Op == syntax.TsMatch) {
return "1"
}
}
@ -173,11 +173,9 @@ func (r *Runner) unTest(ctx context.Context, op syntax.UnTestOperator, x string)
}
return false
case syntax.TsVarSet:
_, e := r.lookupVar(x)
return e
return r.lookupVar(x).IsSet()
case syntax.TsRefVar:
v, _ := r.lookupVar(x)
return v.NameRef
return r.lookupVar(x).NameRef
case syntax.TsNot:
return x == ""
default:

View File

@ -4,259 +4,137 @@
package interp
import (
"context"
"fmt"
"os"
"runtime"
"sort"
"strconv"
"strings"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/syntax"
)
type Environ interface {
Get(name string) (value string, exists bool)
Set(name, value string)
Delete(name string)
Names() []string
Copy() Environ
type overlayEnviron struct {
parent expand.Environ
values map[string]expand.Variable
}
type mapEnviron struct {
names []string
values map[string]string
}
func (m *mapEnviron) Get(name string) (string, bool) {
val, ok := m.values[name]
return val, ok
}
func (m *mapEnviron) Set(name, value string) {
_, ok := m.values[name]
if !ok {
m.names = append(m.names, name)
sort.Strings(m.names)
func (o overlayEnviron) Get(name string) expand.Variable {
if vr, ok := o.values[name]; ok {
return vr
}
m.values[name] = value
return o.parent.Get(name)
}
func (m *mapEnviron) Delete(name string) {
if _, ok := m.values[name]; !ok {
return
}
delete(m.values, name)
for i, iname := range m.names {
if iname == name {
m.names = append(m.names[:i], m.names[i+1:]...)
func (o overlayEnviron) Set(name string, vr expand.Variable) {
o.values[name] = vr
}
func (o overlayEnviron) Each(f func(name string, vr expand.Variable) bool) {
o.parent.Each(f)
for name, vr := range o.values {
if !f(name, vr) {
return
}
}
}
func (m *mapEnviron) Names() []string {
return m.names
}
func (m *mapEnviron) Copy() Environ {
m2 := &mapEnviron{
names: make([]string, len(m.names)),
values: make(map[string]string, len(m.values)),
}
copy(m2.names, m.names)
for name, val := range m.values {
m2.values[name] = val
}
return m2
}
func execEnv(env Environ) []string {
names := env.Names()
list := make([]string, len(names))
for i, name := range names {
val, _ := env.Get(name)
list[i] = name + "=" + val
}
func execEnv(env expand.Environ) []string {
list := make([]string, 0, 32)
env.Each(func(name string, vr expand.Variable) bool {
if vr.Exported {
list = append(list, name+"="+vr.String())
}
return true
})
return list
}
func EnvFromList(list []string) (Environ, error) {
m := mapEnviron{
names: make([]string, 0, len(list)),
values: make(map[string]string, len(list)),
}
for _, kv := range list {
i := strings.IndexByte(kv, '=')
if i < 0 {
return nil, fmt.Errorf("env not in the form key=value: %q", kv)
}
name, val := kv[:i], kv[i+1:]
if runtime.GOOS == "windows" {
name = strings.ToUpper(name)
}
m.names = append(m.names, name)
m.values[name] = val
}
sort.Strings(m.names)
return &m, nil
}
type FuncEnviron func(string) string
func (f FuncEnviron) Get(name string) (string, bool) {
val := f(name)
return val, val != ""
}
func (f FuncEnviron) Set(name, value string) {}
func (f FuncEnviron) Delete(name string) {}
func (f FuncEnviron) Names() []string { return nil }
func (f FuncEnviron) Copy() Environ { return f }
type Variable struct {
Local bool
Exported bool
ReadOnly bool
NameRef bool
Value VarValue
}
// VarValue is one of:
//
// StringVal
// IndexArray
// AssocArray
type VarValue interface{}
type StringVal string
type IndexArray []string
type AssocArray map[string]string
func (r *Runner) lookupVar(name string) (Variable, bool) {
func (r *Runner) lookupVar(name string) expand.Variable {
if name == "" {
panic("variable name must not be empty")
}
if val, e := r.cmdVars[name]; e {
return Variable{Value: StringVal(val)}, true
var value interface{}
switch name {
case "#":
value = strconv.Itoa(len(r.Params))
case "@", "*":
value = r.Params
case "?":
value = strconv.Itoa(r.exit)
case "$":
value = strconv.Itoa(os.Getpid())
case "PPID":
value = strconv.Itoa(os.Getppid())
case "DIRSTACK":
value = r.dirStack
case "0":
if r.filename != "" {
value = r.filename
} else {
value = "gosh"
}
case "1", "2", "3", "4", "5", "6", "7", "8", "9":
i := int(name[0] - '1')
if i < len(r.Params) {
value = r.Params[i]
} else {
value = ""
}
}
if value != nil {
return expand.Variable{Value: value}
}
if value, e := r.cmdVars[name]; e {
return expand.Variable{Value: value}
}
if vr, e := r.funcVars[name]; e {
return vr, true
vr.Local = true
return vr
}
if vr, e := r.Vars[name]; e {
return vr, true
return vr
}
if str, e := r.Env.Get(name); e {
return Variable{Value: StringVal(str)}, true
if vr := r.Env.Get(name); vr.IsSet() {
return vr
}
if runtime.GOOS == "windows" {
upper := strings.ToUpper(name)
if str, e := r.Env.Get(upper); e {
return Variable{Value: StringVal(str)}, true
if vr := r.Env.Get(upper); vr.IsSet() {
return vr
}
}
if r.opts[optNoUnset] {
r.errf("%s: unbound variable\n", name)
r.setErr(ShellExitStatus(1))
}
return Variable{}, false
return expand.Variable{}
}
func (r *Runner) getVar(name string) string {
val, _ := r.lookupVar(name)
return r.varStr(val, 0)
func (r *Runner) envGet(name string) string {
return r.lookupVar(name).String()
}
func (r *Runner) delVar(name string) {
val, _ := r.lookupVar(name)
if val.ReadOnly {
vr := r.lookupVar(name)
if vr.ReadOnly {
r.errf("%s: readonly variable\n", name)
r.exit = 1
return
}
delete(r.Vars, name)
delete(r.funcVars, name)
delete(r.cmdVars, name)
r.Env.Delete(name)
if vr.Local {
// don't overwrite a non-local var with the same name
r.funcVars[name] = expand.Variable{}
} else {
r.Vars[name] = expand.Variable{} // to not query r.Env
}
}
// maxNameRefDepth defines the maximum number of times to follow
// references when expanding a variable. Otherwise, simple name
// reference loops could crash the interpreter quite easily.
const maxNameRefDepth = 100
func (r *Runner) varStr(vr Variable, depth int) string {
if depth > maxNameRefDepth {
return ""
}
switch x := vr.Value.(type) {
case StringVal:
if vr.NameRef {
vr, _ = r.lookupVar(string(x))
return r.varStr(vr, depth+1)
}
return string(x)
case IndexArray:
if len(x) > 0 {
return x[0]
}
case AssocArray:
// nothing to do
}
return ""
func (r *Runner) setVarString(name, value string) {
r.setVar(name, nil, expand.Variable{Value: value})
}
func (r *Runner) varInd(ctx context.Context, vr Variable, e syntax.ArithmExpr, depth int) string {
if depth > maxNameRefDepth {
return ""
}
switch x := vr.Value.(type) {
case StringVal:
if vr.NameRef {
vr, _ = r.lookupVar(string(x))
return r.varInd(ctx, vr, e, depth+1)
}
if r.arithm(ctx, e) == 0 {
return string(x)
}
case IndexArray:
switch anyOfLit(e, "@", "*") {
case "@":
return strings.Join(x, " ")
case "*":
return strings.Join(x, r.ifsJoin)
}
i := r.arithm(ctx, e)
if len(x) > 0 {
return x[i]
}
case AssocArray:
if lit := anyOfLit(e, "@", "*"); lit != "" {
var strs IndexArray
keys := make([]string, 0, len(x))
for k := range x {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
strs = append(strs, x[k])
}
if lit == "*" {
return strings.Join(strs, r.ifsJoin)
}
return strings.Join(strs, " ")
}
return x[r.loneWord(ctx, e.(*syntax.Word))]
}
return ""
}
func (r *Runner) setVarString(ctx context.Context, name, val string) {
r.setVar(ctx, name, nil, Variable{Value: StringVal(val)})
}
func (r *Runner) setVarInternal(name string, vr Variable) {
if _, ok := vr.Value.(StringVal); ok {
func (r *Runner) setVarInternal(name string, vr expand.Variable) {
if _, ok := vr.Value.(string); ok {
if r.opts[optAllExport] {
vr.Exported = true
}
@ -265,28 +143,31 @@ func (r *Runner) setVarInternal(name string, vr Variable) {
}
if vr.Local {
if r.funcVars == nil {
r.funcVars = make(map[string]Variable)
r.funcVars = make(map[string]expand.Variable)
}
r.funcVars[name] = vr
} else {
r.Vars[name] = vr
}
if name == "IFS" {
r.ifsUpdated()
}
}
func (r *Runner) setVar(ctx context.Context, name string, index syntax.ArithmExpr, vr Variable) {
cur, _ := r.lookupVar(name)
func (r *Runner) setVar(name string, index syntax.ArithmExpr, vr expand.Variable) {
cur := r.lookupVar(name)
if cur.ReadOnly {
r.errf("%s: readonly variable\n", name)
r.exit = 1
return
}
_, isIndexArray := cur.Value.(IndexArray)
_, isAssocArray := cur.Value.(AssocArray)
if name2, var2 := cur.Resolve(r.Env); name2 != "" {
name = name2
cur = var2
vr.NameRef = false
cur.NameRef = false
}
_, isIndexArray := cur.Value.([]string)
_, isAssocArray := cur.Value.(map[string]string)
if _, ok := vr.Value.(StringVal); ok && index == nil {
if _, ok := vr.Value.(string); ok && index == nil {
// When assigning a string to an array, fall back to the
// zero value for the index.
if isIndexArray {
@ -304,33 +185,33 @@ func (r *Runner) setVar(ctx context.Context, name string, index syntax.ArithmExp
return
}
// from the syntax package, we know that val must be a string if
// index is non-nil; nested arrays are forbidden.
valStr := string(vr.Value.(StringVal))
// from the syntax package, we know that value must be a string if index
// is non-nil; nested arrays are forbidden.
valStr := vr.Value.(string)
// if the existing variable is already an AssocArray, try our best
// to convert the key to a string
if isAssocArray {
amap := cur.Value.(AssocArray)
amap := cur.Value.(map[string]string)
w, ok := index.(*syntax.Word)
if !ok {
return
}
k := r.loneWord(ctx, w)
k := r.literal(w)
amap[k] = valStr
cur.Value = amap
r.setVarInternal(name, cur)
return
}
var list IndexArray
var list []string
switch x := cur.Value.(type) {
case StringVal:
list = append(list, string(x))
case IndexArray:
case string:
list = append(list, x)
case []string:
list = x
case AssocArray: // done above
case map[string]string: // done above
}
k := r.arithm(ctx, index)
k := r.arithm(index)
for len(list) < k+1 {
list = append(list, "")
}
@ -358,32 +239,33 @@ func stringIndex(index syntax.ArithmExpr) bool {
return false
}
func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType string) VarValue {
prev, prevOk := r.lookupVar(as.Name.Value)
func (r *Runner) assignVal(as *syntax.Assign, valType string) interface{} {
prev := r.lookupVar(as.Name.Value)
if as.Naked {
return prev.Value
}
if as.Value != nil {
s := r.loneWord(ctx, as.Value)
if !as.Append || !prevOk {
return StringVal(s)
s := r.literal(as.Value)
if !as.Append || !prev.IsSet() {
return s
}
switch x := prev.Value.(type) {
case StringVal:
return x + StringVal(s)
case IndexArray:
case string:
return x + s
case []string:
if len(x) == 0 {
x = append(x, "")
}
x[0] += s
return x
case AssocArray:
case map[string]string:
// TODO
}
return StringVal(s)
return s
}
if as.Array == nil {
return nil
// don't return nil, as that's an unset variable
return ""
}
elems := as.Array.Elems
if valType == "" {
@ -395,12 +277,12 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin
}
if valType == "-A" {
// associative array
amap := AssocArray(make(map[string]string, len(elems)))
amap := make(map[string]string, len(elems))
for _, elem := range elems {
k := r.loneWord(ctx, elem.Index.(*syntax.Word))
amap[k] = r.loneWord(ctx, elem.Value)
k := r.literal(elem.Index.(*syntax.Word))
amap[k] = r.literal(elem.Value)
}
if !as.Append || !prevOk {
if !as.Append || !prev.IsSet() {
return amap
}
// TODO
@ -414,7 +296,7 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin
indexes[i] = i
continue
}
k := r.arithm(ctx, elem.Index)
k := r.arithm(elem.Index)
indexes[i] = k
if k > maxIndex {
maxIndex = k
@ -422,50 +304,18 @@ func (r *Runner) assignVal(ctx context.Context, as *syntax.Assign, valType strin
}
strs := make([]string, maxIndex+1)
for i, elem := range elems {
strs[indexes[i]] = r.loneWord(ctx, elem.Value)
strs[indexes[i]] = r.literal(elem.Value)
}
if !as.Append || !prevOk {
return IndexArray(strs)
if !as.Append || !prev.IsSet() {
return strs
}
switch x := prev.Value.(type) {
case StringVal:
prevList := IndexArray([]string{string(x)})
return append(prevList, strs...)
case IndexArray:
case string:
return append([]string{x}, strs...)
case []string:
return append(x, strs...)
case AssocArray:
case map[string]string:
// TODO
}
return IndexArray(strs)
}
func (r *Runner) ifsUpdated() {
runes := r.getVar("IFS")
r.ifsJoin = ""
if len(runes) > 0 {
r.ifsJoin = runes[:1]
}
r.ifsRune = func(r rune) bool {
for _, r2 := range runes {
if r == r2 {
return true
}
}
return false
}
}
func (r *Runner) namesByPrefix(prefix string) []string {
var names []string
for _, name := range r.Env.Names() {
if strings.HasPrefix(name, prefix) {
names = append(names, name)
}
}
for name := range r.Vars {
if strings.HasPrefix(name, prefix) {
names = append(names, name)
}
}
return names
return strs
}

View File

@ -1,9 +1,6 @@
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
// Package shell contains high-level features that use the syntax and
// Package shell contains high-level features that use the syntax, expand, and
// interp packages under the hood.
//
// This package is a work in progress and EXPERIMENTAL; its API is not
// subject to the 1.x backwards compatibility guarantee.
package shell

View File

@ -4,41 +4,60 @@
package shell
import (
"context"
"os"
"strings"
"mvdan.cc/sh/interp"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/syntax"
)
// Expand performs shell expansion on s, using env to resolve variables.
// The expansion will apply to parameter expansions like $var and
// ${#var}, but also to arithmetic expansions like $((var + 3)), and
// command substitutions like $(echo foo).
// Expand performs shell expansion on s as if it were within double quotes,
// using env to resolve variables. This includes parameter expansion, arithmetic
// expansion, and quote removal.
//
// If env is nil, the current environment variables are used.
// If env is nil, the current environment variables are used. Empty variables
// are treated as unset; to support variables which are set but empty, use the
// expand package directly.
//
// Any side effects or modifications to the system are forbidden when
// interpreting the program. This is enforced via whitelists when
// executing programs and opening paths. The interpreter also has a timeout of
// two seconds.
// Command subsitutions like $(echo foo) aren't supported to avoid running
// arbitrary code. To support those, use an interpreter with the expand package.
//
// An error will be reported if the input string had invalid syntax.
func Expand(s string, env func(string) string) (string, error) {
p := syntax.NewParser()
src := "<<EXPAND_EOF\n" + s + "\nEXPAND_EOF"
f, err := p.Parse(strings.NewReader(src), "")
word, err := p.Document(strings.NewReader(s))
if err != nil {
return "", err
}
word := f.Stmts[0].Redirs[0].Hdoc
last := word.Parts[len(word.Parts)-1].(*syntax.Lit)
// since the heredoc implies a trailing newline
last.Value = strings.TrimSuffix(last.Value, "\n")
r := pureRunner()
if env != nil {
r.Env = interp.FuncEnviron(env)
if env == nil {
env = os.Getenv
}
ctx, cancel := context.WithTimeout(context.Background(), pureRunnerTimeout)
defer cancel()
fields, err := r.Fields(ctx, word)
return strings.Join(fields, ""), err
cfg := &expand.Config{Env: expand.FuncEnviron(env)}
return expand.Document(cfg, word)
}
// Fields performs shell expansion on s as if it were a command's arguments,
// using env to resolve variables. It is similar to Expand, but includes brace
// expansion, tilde expansion, and globbing.
//
// If env is nil, the current environment variables are used. Empty variables
// are treated as unset; to support variables which are set but empty, use the
// expand package directly.
//
// An error will be reported if the input string had invalid syntax.
func Fields(s string, env func(string) string) ([]string, error) {
p := syntax.NewParser()
var words []*syntax.Word
err := p.Words(strings.NewReader(s), func(w *syntax.Word) bool {
words = append(words, w)
return true
})
if err != nil {
return nil, err
}
if env == nil {
env = os.Getenv
}
cfg := &expand.Config{Env: expand.FuncEnviron(env)}
return expand.Fields(cfg, words...)
}

View File

@ -6,81 +6,42 @@ package shell
import (
"context"
"fmt"
"io"
"os"
"time"
"mvdan.cc/sh/expand"
"mvdan.cc/sh/interp"
"mvdan.cc/sh/syntax"
)
// SourceFile sources a shell file from disk and returns the variables
// declared in it.
// declared in it. It is a convenience function that uses a default shell
// parser, parses a file from disk, and calls SourceNode.
//
// A default parser is used; to set custom options, use SourceNode
// instead.
func SourceFile(path string) (map[string]interp.Variable, error) {
// This function should be used with caution, as it can interpret arbitrary
// code. Untrusted shell programs shoudn't be sourced outside of a sandbox
// environment.
func SourceFile(ctx context.Context, path string) (map[string]expand.Variable, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("could not open: %v", err)
}
defer f.Close()
p := syntax.NewParser()
file, err := p.Parse(f, path)
file, err := syntax.NewParser().Parse(f, path)
if err != nil {
return nil, fmt.Errorf("could not parse: %v", err)
}
return SourceNode(file)
}
// purePrograms holds a list of common programs that do not have side
// effects, or otherwise cannot modify or harm the system that runs
// them.
var purePrograms = []string{
// string handling
"sed", "grep", "tr", "cut", "cat", "head", "tail", "seq", "yes",
"wc",
// paths
"ls", "pwd", "basename", "realpath",
// others
"env", "sleep", "uniq", "sort",
}
var pureRunnerTimeout = 2 * time.Second
func pureRunner() *interp.Runner {
// forbid executing programs that might cause trouble
exec := interp.ModuleExec(func(ctx context.Context, path string, args []string) error {
for _, name := range purePrograms {
if args[0] == name {
return interp.DefaultExec(ctx, path, args)
}
}
return fmt.Errorf("program not in whitelist: %s", args[0])
})
// forbid opening any real files
open := interp.OpenDevImpls(func(ctx context.Context, path string, flags int, mode os.FileMode) (io.ReadWriteCloser, error) {
mc, _ := interp.FromModuleContext(ctx)
return nil, fmt.Errorf("cannot open path: %s", mc.UnixPath(path))
})
r, err := interp.New(interp.Module(exec), interp.Module(open))
if err != nil {
panic(err)
}
return r
return SourceNode(ctx, file)
}
// SourceNode sources a shell program from a node and returns the
// variables declared in it.
// variables declared in it. It accepts the same set of node types that
// interp/Runner.Run does.
//
// Any side effects or modifications to the system are forbidden when
// interpreting the program. This is enforced via whitelists when
// executing programs and opening paths. The interpreter also has a timeout of
// two seconds.
func SourceNode(node syntax.Node) (map[string]interp.Variable, error) {
r := pureRunner()
ctx, cancel := context.WithTimeout(context.Background(), pureRunnerTimeout)
defer cancel()
// This function should be used with caution, as it can interpret arbitrary
// code. Untrusted shell programs shoudn't be sourced outside of a sandbox
// environment.
func SourceNode(ctx context.Context, node syntax.Node) (map[string]expand.Variable, error) {
r, _ := interp.New()
if err := r.Run(ctx, node); err != nil {
return nil, fmt.Errorf("could not run: %v", err)
}

View File

@ -5,7 +5,8 @@ package syntax
import "strconv"
// TODO: consider making these special syntax nodes
// TODO(v3): Consider making these special syntax nodes.
// Among other things, we can make use of Word.Lit.
type brace struct {
seq bool // {x..y[..incr]} instead of {x,y[,...]}
@ -265,17 +266,13 @@ func expandRec(bw *braceWord) []*Word {
return []*Word{{Parts: left}}
}
// TODO(v3): remove
// ExpandBraces performs Bash brace expansion on a word. For example,
// passing it a single-literal word "foo{bar,baz}" will return two
// single-literal words, "foobar" and "foobaz".
//
// It does not return an error; malformed brace expansions are simply
// skipped. For example, "a{b{c,d}" results in the words "a{bc" and
// "a{bd".
//
// Note that the resulting words may have more word parts than
// necessary, such as contiguous *Lit nodes, and that these parts may be
// shared between words.
// Deprecated: use mvdan.cc/sh/expand.Braces instead.
func ExpandBraces(word *Word) []*Word {
topBrace, any := splitBraces(word)
if !any {

View File

@ -60,10 +60,9 @@ func (p *Parser) rune() rune {
// p.r instead of b so that newline
// character positions don't have col 0.
p.npos.line++
p.npos.col = 1
} else {
p.npos.col += p.w
p.npos.col = 0
}
p.npos.col += p.w
bquotes := 0
retry:
if p.bsp < len(p.bs) {
@ -87,9 +86,8 @@ retry:
p.w, p.r = 1, rune(b)
return p.r
}
if p.bsp+utf8.UTFMax >= len(p.bs) {
// we might need up to 4 bytes to read a full
// non-ascii rune
if !utf8.FullRune(p.bs[p.bsp:]) {
// we need more bytes to read a full non-ascii rune
p.fill()
}
var w int
@ -122,14 +120,18 @@ func (p *Parser) fill() {
p.offs += p.bsp
left := len(p.bs) - p.bsp
copy(p.readBuf[:left], p.readBuf[p.bsp:])
readAgain:
n, err := 0, p.readErr
if err == nil {
n, err = p.src.Read(p.readBuf[left:])
p.readErr = err
}
if n == 0 {
if err == nil {
goto readAgain
}
// don't use p.errPass as we don't want to overwrite p.tok
if err != nil && err != io.EOF {
if err != io.EOF {
p.err = err
}
if left > 0 {
@ -238,6 +240,7 @@ skipSpace:
return
}
}
changedState:
p.pos = p.getPos()
switch {
case p.quote&allRegTokens != 0:
@ -292,15 +295,21 @@ skipSpace:
case p.quote&allParamExp != 0 && paramOps(r):
p.tok = p.paramToken(r)
case p.quote == testRegexp:
if !p.rxFirstPart && p.spaced {
p.quote = noState
goto changedState
}
p.rxFirstPart = false
switch r {
case ';', '"', '\'', '$', '&', '>', '<', '`':
p.tok = p.regToken(r)
case ')':
if p.reOpenParens > 0 {
if p.rxOpenParens > 0 {
// continuation of open paren
p.advanceLitRe(r)
} else {
p.tok = rightParen
p.quote = noState
}
default: // including '(', '|'
p.advanceLitRe(r)
@ -900,7 +909,6 @@ func (p *Parser) advanceLitHdoc(r rune) {
p.newLit(r)
if p.quote == hdocBodyTabs {
for r == '\t' {
p.discardLit(1)
r = p.rune()
}
}
@ -916,7 +924,12 @@ func (p *Parser) advanceLitHdoc(r rune) {
case '\\': // escaped byte follows
p.rune()
case '\n', utf8.RuneSelf:
if bytes.HasPrefix(p.litBs[lStart:], p.hdocStop) {
if p.parsingDoc {
if r == utf8.RuneSelf {
p.val = p.endLit()
return
}
} else if bytes.HasPrefix(p.litBs[lStart:], p.hdocStop) {
p.val = p.endLit()[:lStart]
if p.val == "" {
p.tok = _Newl
@ -930,7 +943,6 @@ func (p *Parser) advanceLitHdoc(r rune) {
if p.quote == hdocBodyTabs {
for p.peekByte('\t') {
p.rune()
p.discardLit(1)
}
}
lStart = len(p.litBs)
@ -938,7 +950,7 @@ func (p *Parser) advanceLitHdoc(r rune) {
}
}
func (p *Parser) hdocLitWord() *Word {
func (p *Parser) quotedHdocWord() *Word {
r := p.r
p.newLit(r)
pos := p.getPos()
@ -948,7 +960,6 @@ func (p *Parser) hdocLitWord() *Word {
}
if p.quote == hdocBodyTabs {
for r == '\t' {
p.discardLit(1)
r = p.rune()
}
}
@ -976,19 +987,25 @@ func (p *Parser) advanceLitRe(r rune) {
case '\\':
p.rune()
case '(':
p.reOpenParens++
p.rxOpenParens++
case ')':
if p.reOpenParens--; p.reOpenParens < 0 {
if p.rxOpenParens--; p.rxOpenParens < 0 {
p.tok, p.val = _LitWord, p.endLit()
p.quote = noState
return
}
case ' ', '\t', '\r', '\n':
if p.reOpenParens <= 0 {
if p.rxOpenParens <= 0 {
p.tok, p.val = _LitWord, p.endLit()
p.quote = noState
return
}
case utf8.RuneSelf, ';', '"', '\'', '$', '&', '>', '<', '`':
case '"', '\'', '$', '`':
p.tok, p.val = _Lit, p.endLit()
return
case utf8.RuneSelf, ';', '&', '>', '<':
p.tok, p.val = _LitWord, p.endLit()
p.quote = noState
return
}
}

View File

@ -3,7 +3,10 @@
package syntax
import "fmt"
import (
"fmt"
"strings"
)
// Node represents a syntax tree node.
type Node interface {
@ -243,7 +246,12 @@ func (r *Redirect) Pos() Pos {
}
return r.OpPos
}
func (r *Redirect) End() Pos { return r.Word.End() }
func (r *Redirect) End() Pos {
if r.Hdoc != nil {
return r.Hdoc.End()
}
return r.Word.End()
}
// CallExpr represents a command execution or function call, otherwise known as
// a "simple command".
@ -289,6 +297,10 @@ type Block struct {
func (b *Block) Pos() Pos { return b.Lbrace }
func (b *Block) End() Pos { return posAddCol(b.Rbrace, 1) }
// TODO(v3): Refactor and simplify elif/else. For example, we could likely make
// Else an *IfClause, remove ElsePos, make IfPos also do opening "else"
// positions, and join the comment slices as Last []Comment.
// IfClause represents an if statement.
type IfClause struct {
Elif bool // whether this IfClause begins with "elif"
@ -302,6 +314,7 @@ type IfClause struct {
Else StmtList
ElseComments []Comment // comments on the "else"
FiComments []Comment // comments on the "fi"
}
func (c *IfClause) Pos() Pos { return c.IfPos }
@ -415,6 +428,28 @@ type Word struct {
func (w *Word) Pos() Pos { return w.Parts[0].Pos() }
func (w *Word) End() Pos { return w.Parts[len(w.Parts)-1].End() }
// Lit returns the word as a literal value, if the word consists of *syntax.Lit
// nodes only. An empty string is returned otherwise. Words with multiple
// literals, which can appear in some edge cases, are handled properly.
//
// For example, the word "foo" will return "foo", but the word "foo${bar}" will
// return "".
func (w *Word) Lit() string {
// In the usual case, we'll have either a single part that's a literal,
// or one of the parts being a non-literal. Using strings.Join instead
// of a strings.Builder avoids extra work in these cases, since a single
// part is a shortcut, and many parts don't incur string copies.
lits := make([]string, 0, 1)
for _, part := range w.Parts {
lit, ok := part.(*Lit)
if !ok {
return ""
}
lits = append(lits, lit.Value)
}
return strings.Join(lits, "")
}
// WordPart represents all nodes that can form part of a word.
//
// These are *Lit, *SglQuoted, *DblQuoted, *ParamExp, *CmdSubst, *ArithmExp,

View File

@ -113,6 +113,135 @@ func (p *Parser) Stmts(r io.Reader, fn func(*Stmt) bool) error {
return p.err
}
type wrappedReader struct {
*Parser
io.Reader
lastLine uint16
accumulated []*Stmt
fn func([]*Stmt) bool
}
func (w *wrappedReader) Read(p []byte) (n int, err error) {
// If we lexed a newline for the first time, we just finished a line, so
// we may need to give a callback for the edge cases below not covered
// by Parser.Stmts.
if w.r == '\n' && w.npos.line > w.lastLine {
if w.Incomplete() {
// Incomplete statement; call back to print "> ".
if !w.fn(w.accumulated) {
return 0, io.EOF
}
} else if len(w.accumulated) == 0 {
// Nothing was parsed; call back to print another "$ ".
if !w.fn(nil) {
return 0, io.EOF
}
}
w.lastLine = w.npos.line
}
return w.Reader.Read(p)
}
// Interactive implements what is necessary to parse statements in an
// interactive shell. The parser will call the given function under two
// circumstances outlined below.
//
// If a line containing any number of statements is parsed, the function will be
// called with said statements.
//
// If a line ending in an incomplete statement is parsed, the function will be
// called with any fully parsed statents, and Parser.Incomplete will return
// true.
//
// One can imagine a simple interactive shell implementation as follows:
//
// fmt.Fprintf(os.Stdout, "$ ")
// parser.Interactive(os.Stdin, func(stmts []*syntax.Stmt) bool {
// if parser.Incomplete() {
// fmt.Fprintf(os.Stdout, "> ")
// return true
// }
// run(stmts)
// fmt.Fprintf(os.Stdout, "$ ")
// return true
// }
//
// If the callback function returns false, parsing is stopped and the function
// is not called again.
func (p *Parser) Interactive(r io.Reader, fn func([]*Stmt) bool) error {
w := wrappedReader{Parser: p, Reader: r, fn: fn}
return p.Stmts(&w, func(stmt *Stmt) bool {
w.accumulated = append(w.accumulated, stmt)
// We finished parsing a statement and we're at a newline token,
// so we finished fully parsing a number of statements. Call
// back to run the statements and print "$ ".
if p.tok == _Newl {
if !fn(w.accumulated) {
return false
}
w.accumulated = w.accumulated[:0]
// The callback above would already print "$ ", so we
// don't want the subsequent wrappedReader.Read to cause
// another "$ " print thinking that nothing was parsed.
w.lastLine = w.npos.line + 1
}
return true
})
}
// Words reads and parses words one at a time, calling a function each time one
// is parsed. If the function returns false, parsing is stopped and the function
// is not called again.
//
// Newlines are skipped, meaning that multi-line input will work fine. If the
// parser encounters a token that isn't a word, such as a semicolon, an error
// will be returned.
//
// Note that the lexer doesn't currently tokenize spaces, so it may need to read
// a non-space byte such as a newline or a letter before finishing the parsing
// of a word. This will be fixed in the future.
func (p *Parser) Words(r io.Reader, fn func(*Word) bool) error {
p.reset()
p.f = &File{}
p.src = r
p.rune()
p.next()
for {
p.got(_Newl)
w := p.getWord()
if w == nil {
if p.tok != _EOF {
p.curErr("%s is not a valid word", p.tok)
}
return p.err
}
if !fn(w) {
return nil
}
}
}
// Document parses a single here-document word. That is, it parses the input as
// if they were lines following a <<EOF redirection.
//
// In practice, this is the same as parsing the input as if it were within
// double quotes, but without having to escape all double quote characters.
// Similarly, the here-document word parsed here cannot be ended by any
// delimiter other than reaching the end of the input.
func (p *Parser) Document(r io.Reader) (*Word, error) {
p.reset()
p.f = &File{}
p.src = r
p.rune()
p.quote = hdocBody
p.hdocStop = []byte("MVDAN_CC_SH_SYNTAX_EOF")
p.parsingDoc = true
p.next()
w := p.getWord()
return w, p.err
}
// Parser holds the internal state of the parsing mechanism of a
// program.
type Parser struct {
@ -150,18 +279,23 @@ type Parser struct {
buriedHdocs int
heredocs []*Redirect
hdocStop []byte
parsingDoc bool
// openBquotes is how many levels of backquotes are open at the
// moment
// openStmts is how many entire statements we're currently parsing. A
// non-zero number means that we require certain tokens or words before
// reaching EOF.
openStmts int
// openBquotes is how many levels of backquotes are open at the moment.
openBquotes int
// lastBquoteEsc is how many times the last backquote token was
// escaped
// lastBquoteEsc is how many times the last backquote token was escaped
lastBquoteEsc int
// buriedBquotes is like openBquotes, but saved for when the
// parser comes out of single quotes
// buriedBquotes is like openBquotes, but saved for when the parser
// comes out of single quotes
buriedBquotes int
reOpenParens int
rxOpenParens int
rxFirstPart bool
accComs []Comment
curComs *[]Comment
@ -180,6 +314,14 @@ type Parser struct {
litBs []byte
}
func (p *Parser) Incomplete() bool {
// If we're in a quote state other than noState, we're parsing a node
// such as a double-quoted string.
// If there are any open statements, we need to finish them.
// If we're constructing a literal, we need to finish it.
return p.quote != noState || p.openStmts > 0 || p.litBs != nil
}
const bufSize = 1 << 10
func (p *Parser) reset() {
@ -191,9 +333,10 @@ func (p *Parser) reset() {
p.r, p.w = 0, 0
p.err, p.readErr = nil, nil
p.quote, p.forbidNested = noState, false
p.openStmts = 0
p.heredocs, p.buriedHdocs = p.heredocs[:0], 0
p.parsingDoc = false
p.openBquotes, p.buriedBquotes = 0, 0
p.reOpenParens = 0
p.accComs, p.curComs = nil, &p.accComs
}
@ -270,6 +413,8 @@ func (p *Parser) call(w *Word) *CallExpr {
return ce
}
//go:generate stringer -type=quoteState
type quoteState uint32
const (
@ -372,7 +517,7 @@ func (p *Parser) doHeredocs() {
p.rune()
}
if quoted {
r.Hdoc = p.hdocLitWord()
r.Hdoc = p.quotedHdocWord()
} else {
p.next()
r.Hdoc = p.getWord()
@ -597,7 +742,9 @@ loop:
if p.tok == _EOF {
break
}
p.openStmts++
s := p.getStmt(true, false, false)
p.openStmts--
if s == nil {
p.invalidStmtStart()
break
@ -619,7 +766,7 @@ func (p *Parser) stmtList(stops ...string) (sl StmtList) {
}
p.stmts(fn, stops...)
split := len(p.accComs)
if p.tok == _LitWord && (p.val == "elif" || p.val == "else") {
if p.tok == _LitWord && (p.val == "elif" || p.val == "else" || p.val == "fi") {
// Split the comments, so that any aligned with an opening token
// get attached to it. For example:
//
@ -630,7 +777,7 @@ func (p *Parser) stmtList(stops ...string) (sl StmtList) {
// fi
// TODO(mvdan): look into deduplicating this with similar logic
// in caseItems.
for i := len(p.accComs)-1; i >= 0; i-- {
for i := len(p.accComs) - 1; i >= 0; i-- {
c := p.accComs[i]
if c.Pos().Col() != p.pos.Col() {
break
@ -1250,15 +1397,8 @@ func (p *Parser) paramExp() *ParamExp {
default:
pe.Exp = p.paramExpExp()
}
case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn:
// if unset/null actions
switch pe.Param.Value {
case "#", "$", "?", "!":
p.curErr("$%s can never be unset or null", pe.Param.Value)
}
pe.Exp = p.paramExpExp()
case perc, dblPerc, hash, dblHash:
// pattern string manipulation
case plus, colPlus, minus, colMinus, quest, colQuest, assgn, colAssgn,
perc, dblPerc, hash, dblHash:
pe.Exp = p.paramExpExp()
case _EOF:
default:
@ -1333,6 +1473,9 @@ func (p *Parser) backquoteEnd() bool {
// ValidName returns whether val is a valid name as per the POSIX spec.
func ValidName(val string) bool {
if val == "" {
return false
}
for i, r := range val {
switch {
case 'a' <= r && r <= 'z':
@ -1797,6 +1940,8 @@ func (p *Parser) ifClause(s *Stmt) {
curIf.ElsePos = elsePos
curIf.Else = p.followStmts("else", curIf.ElsePos, "fi")
}
curIf.FiComments = p.accComs
p.accComs = nil
rif.FiPos = p.stmtEnd(rif, "if", "fi")
curIf.FiPos = rif.FiPos
s.Cmd = rif
@ -1952,7 +2097,7 @@ func (p *Parser) caseItems(stop string) (items []*CaseItem) {
p.got(_Newl)
split := len(p.accComs)
if p.tok == _LitWord && p.val != stop {
for i := len(p.accComs)-1; i >= 0; i-- {
for i := len(p.accComs) - 1; i >= 0; i-- {
c := p.accComs[i]
if c.Pos().Col() != p.pos.Col() {
break
@ -1982,6 +2127,7 @@ func (p *Parser) testClause(s *Stmt) {
}
func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
p.got(_Newl)
var left TestExpr
if pastAndOr {
left = p.testExprBase(ftok, fpos)
@ -1991,6 +2137,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
if left == nil {
return left
}
p.got(_Newl)
switch p.tok {
case andAnd, orOr:
case _LitWord:
@ -2015,10 +2162,12 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
Op: BinTestOperator(p.tok),
X: left,
}
// Save the previous quoteState, since we change it in TsReMatch.
oldQuote := p.quote
switch b.Op {
case AndTest, OrTest:
p.next()
p.got(_Newl)
if b.Y = p.testExpr(token(b.Op), b.OpPos, false); b.Y == nil {
p.followErrExp(b.OpPos, b.Op.String())
}
@ -2026,12 +2175,12 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
if p.lang != LangBash {
p.langErr(p.pos, "regex tests", LangBash)
}
oldReOpenParens := p.reOpenParens
old := p.preNested(testRegexp)
defer func() {
p.postNested(old)
p.reOpenParens = oldReOpenParens
}()
p.rxOpenParens = 0
p.rxFirstPart = true
// TODO(mvdan): Using nested states within a regex will break in
// all sorts of ways. The better fix is likely to use a stop
// token, like we do with heredocs.
p.quote = testRegexp
fallthrough
default:
if _, ok := b.X.(*Word); !ok {
@ -2041,6 +2190,7 @@ func (p *Parser) testExpr(ftok token, fpos Pos, pastAndOr bool) TestExpr {
p.next()
b.Y = p.followWordTok(token(b.Op), b.OpPos)
}
p.quote = oldQuote
return b
}
@ -2079,14 +2229,12 @@ func (p *Parser) testExprBase(ftok token, fpos Pos) TestExpr {
case leftParen:
pe := &ParenTest{Lparen: p.pos}
p.next()
p.got(_Newl)
if pe.X = p.testExpr(leftParen, pe.Lparen, false); pe.X == nil {
p.followErrExp(pe.Lparen, "(")
}
pe.Rparen = p.matched(pe.Lparen, leftParen, rightParen)
return pe
default:
p.got(_Newl)
return p.followWordTok(ftok, fpos)
}
}

View File

@ -32,17 +32,16 @@ func charClass(s string) (string, error) {
return s[:len(name)+6], nil
}
// TranslatePattern turns a shell pattern expression into a regular
// expression that can be used with regexp.Compile. It will return an
// error if the input pattern was incorrect. Otherwise, the returned
// expression can be passed to regexp.MustCompile.
// TranslatePattern turns a shell wildcard pattern into a regular expression
// that can be used with regexp.Compile. It will return an error if the input
// pattern was incorrect. Otherwise, the returned expression can be passed to
// regexp.MustCompile.
//
// For example, TranslatePattern(`foo*bar?`, true) returns `foo.*bar.`.
//
// Note that this function (and QuotePattern) should not be directly
// used with file paths if Windows is supported, as the path separator
// on that platform is the same character as the escaping character for
// shell patterns.
// Note that this function (and QuotePattern) should not be directly used with
// file paths if Windows is supported, as the path separator on that platform is
// the same character as the escaping character for shell patterns.
func TranslatePattern(pattern string, greedy bool) (string, error) {
any := false
loop:
@ -122,9 +121,31 @@ loop:
return buf.String(), nil
}
// QuotePattern returns a string that quotes all special characters in
// the given pattern. The returned string is a pattern that matches the
// literal string.
// HasPattern returns whether a string contains any unescaped wildcard
// characters: '*', '?', or '['. When the function returns false, the given
// pattern can only match at most one string.
//
// For example, HasPattern(`foo\*bar`) returns false, but HasPattern(`foo*bar`)
// returns true.
//
// This can be useful to avoid extra work, like TranslatePattern. Note that this
// function cannot be used to avoid QuotePattern, as backslashes are quoted by
// that function but ignored here.
func HasPattern(pattern string) bool {
for i := 0; i < len(pattern); i++ {
switch pattern[i] {
case '\\':
i++
case '*', '?', '[':
return true
}
}
return false
}
// QuotePattern returns a string that quotes all special characters in the given
// wildcard pattern. The returned string is a pattern that matches the literal
// string.
//
// For example, QuotePattern(`foo*bar?`) returns `foo\*bar\?`.
func QuotePattern(pattern string) string {

View File

@ -5,6 +5,8 @@ package syntax
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"unicode"
@ -63,8 +65,9 @@ func NewPrinter(options ...func(*Printer)) *Printer {
// Print "pretty-prints" the given syntax tree node to the given writer. Writes
// to w are buffered.
//
// The node types supported at the moment are *File, *Stmt, *Word, and any
// Command node. A trailing newline will only be printed when a *File is used.
// The node types supported at the moment are *File, *Stmt, *Word, any Command
// node, and any WordPart node. A trailing newline will only be printed when a
// *File is used.
func (p *Printer) Print(w io.Writer, node Node) error {
p.reset()
p.bufWriter.Reset(w)
@ -74,10 +77,15 @@ func (p *Printer) Print(w io.Writer, node Node) error {
p.newline(x.End())
case *Stmt:
p.stmtList(StmtList{Stmts: []*Stmt{x}})
case Command:
p.line = x.Pos().Line()
p.command(x, nil)
case *Word:
p.word(x)
case Command:
p.command(x, nil)
case WordPart:
p.wordPart(x, nil)
default:
return fmt.Errorf("unsupported node type: %T", x)
}
p.flushHeredocs()
p.flushComments()
@ -85,39 +93,46 @@ func (p *Printer) Print(w io.Writer, node Node) error {
}
type bufWriter interface {
WriteByte(byte) error
Write([]byte) (int, error)
WriteString(string) (int, error)
WriteByte(byte) error
Reset(io.Writer)
Flush() error
}
type colCounter struct {
*bufio.Writer
column int
column int
lineStart bool
}
func (c *colCounter) WriteByte(b byte) error {
if b == '\n' {
c.column = 1
} else {
c.column++
switch b {
case '\n':
c.column = 0
c.lineStart = true
case '\t', ' ':
default:
c.lineStart = false
}
c.column++
return c.Writer.WriteByte(b)
}
func (c *colCounter) WriteString(s string) (int, error) {
c.lineStart = false
for _, r := range s {
if r == '\n' {
c.column = 1
} else {
c.column++
c.column = 0
}
c.column++
}
return c.Writer.WriteString(s)
}
func (c *colCounter) Reset(w io.Writer) {
c.column = 1
c.lineStart = true
c.Writer.Reset(w)
}
@ -204,7 +219,12 @@ func (p *Printer) spacePad(pos Pos) {
p.WriteByte(' ')
p.wantSpace = false
}
for p.cols.column > 0 && p.cols.column < int(pos.col) {
if p.cols.lineStart {
// Never add padding at the start of a line, since this may
// result in broken indentation or mixing of spaces and tabs.
return
}
for !p.cols.lineStart && p.cols.column > 0 && p.cols.column < int(pos.col) {
p.WriteByte(' ')
}
}
@ -329,9 +349,9 @@ func (p *Printer) flushHeredocs() {
!p.minify && p.tabsPrinter != nil {
if r.Hdoc != nil {
extra := extraIndenter{
bufWriter: p.bufWriter,
afterNewl: true,
level: p.level + 1,
bufWriter: p.bufWriter,
baseIndent: int(p.level + 1),
firstIndent: -1,
}
*p.tabsPrinter = Printer{
bufWriter: &extra,
@ -396,13 +416,6 @@ func (p *Printer) semiRsrv(s string, pos Pos) {
p.wantSpace = true
}
func (p *Printer) comment(c Comment) {
if p.minify {
return
}
p.pendingComments = append(p.pendingComments, c)
}
func (p *Printer) flushComments() {
for i, c := range p.pendingComments {
p.firstLine = false
@ -434,11 +447,11 @@ func (p *Printer) flushComments() {
p.pendingComments = nil
}
func (p *Printer) comments(cs []Comment) {
func (p *Printer) comments(comments ...Comment) {
if p.minify {
return
}
p.pendingComments = append(p.pendingComments, cs...)
p.pendingComments = append(p.pendingComments, comments...)
}
func (p *Printer) wordParts(wps []WordPart) {
@ -487,7 +500,7 @@ func (p *Printer) wordPart(wp, next WordPart) {
}
case *ParamExp:
litCont := ";"
if nextLit, ok := next.(*Lit); ok {
if nextLit, ok := next.(*Lit); ok && nextLit.Value != "" {
litCont = nextLit.Value[:1]
}
name := x.Param.Value
@ -758,13 +771,13 @@ func (p *Printer) casePatternJoin(pats []*Word) {
func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
p.incLevel()
for _, el := range elems {
var left *Comment
var left []Comment
for _, c := range el.Comments {
if c.Pos().After(el.Pos()) {
left = &c
left = append(left, c)
break
}
p.comment(c)
p.comments(c)
}
if el.Pos().Line() > p.line {
p.newline(el.Pos())
@ -776,12 +789,10 @@ func (p *Printer) elemJoin(elems []*ArrayElem, last []Comment) {
p.WriteByte('=')
}
p.word(el.Value)
if left != nil {
p.comment(*left)
}
p.comments(left...)
}
if len(last) > 0 {
p.comments(last)
p.comments(last...)
p.flushComments()
}
p.decLevel()
@ -924,14 +935,14 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.wantSpace = false
p.newline(Pos{})
p.indent()
p.comments(x.Y.Comments)
p.comments(x.Y.Comments...)
p.newline(Pos{})
p.indent()
}
} else {
p.spacedToken(x.Op.String(), x.OpPos)
p.line = x.OpPos.Line()
p.comments(x.Y.Comments)
p.comments(x.Y.Comments...)
p.newline(Pos{})
p.indent()
}
@ -952,7 +963,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
p.space()
}
p.line = x.Body.Pos().Line()
p.comments(x.Body.Comments)
p.comments(x.Body.Comments...)
p.stmt(x.Body)
case *CaseClause:
p.WriteString("case ")
@ -968,7 +979,7 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
last = ci.Comments[i:]
break
}
p.comment(c)
p.comments(c)
}
p.newlines(ci.Pos())
p.casePatternJoin(ci.Patterns)
@ -987,11 +998,11 @@ func (p *Printer) command(cmd Command, redirs []*Redirect) (startRedirs int) {
// avoid ; directly after tokens like ;;
p.wroteSemi = true
}
p.comments(last)
p.comments(last...)
p.flushComments()
p.level--
}
p.comments(x.Last)
p.comments(x.Last...)
if p.swtCaseIndent {
p.flushComments()
p.decLevel()
@ -1048,20 +1059,30 @@ func (p *Printer) ifClause(ic *IfClause, elif bool) {
p.nestedStmts(ic.Cond, Pos{})
p.semiOrNewl("then", ic.ThenPos)
p.nestedStmts(ic.Then, ic.bodyEndPos())
p.comments(ic.ElseComments)
var left []Comment
for _, c := range ic.ElseComments {
if c.Pos().After(ic.ElsePos) {
left = append(left, c)
break
}
p.comments(c)
}
if ic.FollowedByElif() {
s := ic.Else.Stmts[0]
p.comments(s.Comments)
p.comments(s.Comments...)
p.semiRsrv("elif", ic.ElsePos)
p.ifClause(s.Cmd.(*IfClause), true)
return
}
if !ic.Else.empty() {
p.semiRsrv("else", ic.ElsePos)
p.comments(left...)
p.nestedStmts(ic.Else, ic.FiPos)
} else if ic.ElsePos.IsValid() {
p.line = ic.ElsePos.Line()
}
p.comments(ic.FiComments...)
p.semiRsrv("fi", ic.FiPos)
}
@ -1091,18 +1112,17 @@ func (p *Printer) stmtList(sl StmtList) {
lastIndentedLine := uint(0)
for i, s := range sl.Stmts {
pos := s.Pos()
var endCom *Comment
var midComs []Comment
var midComs, endComs []Comment
for _, c := range s.Comments {
if c.End().After(s.End()) {
endCom = &c
endComs = append(endComs, c)
break
}
if c.Pos().After(s.Pos()) {
midComs = append(midComs, c)
continue
}
p.comment(c)
p.comments(c)
}
if !p.minify || p.wantSpace {
p.newlines(pos)
@ -1111,12 +1131,12 @@ func (p *Printer) stmtList(sl StmtList) {
if !p.hasInline(s) {
inlineIndent = 0
p.commentPadding = 0
p.comments(midComs)
p.comments(midComs...)
p.stmt(s)
p.wantNewline = true
continue
}
p.comments(midComs)
p.comments(midComs...)
p.stmt(s)
if s.Pos().Line() > lastIndentedLine+1 {
inlineIndent = 0
@ -1137,15 +1157,13 @@ func (p *Printer) stmtList(sl StmtList) {
}
lastIndentedLine = p.line
}
if endCom != nil {
p.comment(*endCom)
}
p.comments(endComs...)
p.wantNewline = true
}
if len(sl.Stmts) == 1 && !sep {
p.wantNewline = false
}
p.comments(sl.Last)
p.comments(sl.Last...)
}
type byteCounter int
@ -1160,6 +1178,9 @@ func (c *byteCounter) WriteByte(b byte) error {
}
return nil
}
func (c *byteCounter) Write(p []byte) (int, error) {
return c.WriteString(string(p))
}
func (c *byteCounter) WriteString(s string) (int, error) {
switch {
case *c < 0:
@ -1173,20 +1194,41 @@ func (c *byteCounter) WriteString(s string) (int, error) {
func (c *byteCounter) Reset(io.Writer) { *c = 0 }
func (c *byteCounter) Flush() error { return nil }
// extraIndenter ensures that all lines in a '<<-' heredoc body have at least
// baseIndent leading tabs. Those that had more tab indentation than the first
// heredoc line will keep that relative indentation.
type extraIndenter struct {
bufWriter
afterNewl bool
level uint
baseIndent int
firstIndent int
firstChange int
curLine []byte
}
func (e *extraIndenter) WriteByte(b byte) error {
if e.afterNewl {
for i := uint(0); i < e.level; i++ {
e.bufWriter.WriteByte('\t')
e.curLine = append(e.curLine, b)
if b != '\n' {
return nil
}
trimmed := bytes.TrimLeft(e.curLine, "\t")
lineIndent := len(e.curLine) - len(trimmed)
if e.firstIndent < 0 {
e.firstIndent = lineIndent
e.firstChange = e.baseIndent - lineIndent
lineIndent = e.baseIndent
} else {
if lineIndent < e.firstIndent {
lineIndent = e.firstIndent
} else {
lineIndent += e.firstChange
}
}
e.bufWriter.WriteByte(b)
e.afterNewl = b == '\n'
for i := 0; i < lineIndent; i++ {
e.bufWriter.WriteByte('\t')
}
e.bufWriter.Write(trimmed)
e.curLine = e.curLine[:0]
return nil
}
@ -1220,7 +1262,7 @@ func (p *Printer) nestedStmts(sl StmtList, closing Pos) {
// { stmt; stmt; }
p.wantNewline = true
case closing.Line() > p.line && len(sl.Stmts) > 0 &&
sl.end().Line() <= p.line:
sl.end().Line() < closing.Line():
// Force a newline if we find:
// { stmt
// }

View File

@ -0,0 +1,35 @@
// Code generated by "stringer -type=quoteState"; DO NOT EDIT.
package syntax
import "strconv"
const _quoteState_name = "noStatesubCmdsubCmdBckquodblQuoteshdocWordhdocBodyhdocBodyTabsarithmExprarithmExprLetarithmExprCmdarithmExprBracktestRegexpswitchCaseparamExpNameparamExpSliceparamExpReplparamExpExparrayElems"
var _quoteState_map = map[quoteState]string{
1: _quoteState_name[0:7],
2: _quoteState_name[7:13],
4: _quoteState_name[13:25],
8: _quoteState_name[25:34],
16: _quoteState_name[34:42],
32: _quoteState_name[42:50],
64: _quoteState_name[50:62],
128: _quoteState_name[62:72],
256: _quoteState_name[72:85],
512: _quoteState_name[85:98],
1024: _quoteState_name[98:113],
2048: _quoteState_name[113:123],
4096: _quoteState_name[123:133],
8192: _quoteState_name[133:145],
16384: _quoteState_name[145:158],
32768: _quoteState_name[158:170],
65536: _quoteState_name[170:181],
131072: _quoteState_name[181:191],
}
func (i quoteState) String() string {
if str, ok := _quoteState_map[i]; ok {
return str
}
return "quoteState(" + strconv.FormatInt(int64(i), 10) + ")"
}

View File

@ -16,9 +16,6 @@ import "bytes"
// Remove redundant quotes [[ "$var" == str ]]
// Merge negations with unary operators [[ ! -n $var ]]
// Use single quotes to shorten literals "\$foo"
//
// This function is EXPERIMENTAL; it may change or disappear at any
// point until this notice is removed.
func Simplify(n Node) bool {
s := simplifier{}
Walk(n, s.visit)

View File

@ -39,7 +39,7 @@ func Walk(node Node, f func(Node) bool) {
case *Comment:
case *Stmt:
for _, c := range x.Comments {
if c.Pos().After(x.Pos()) {
if !x.End().After(c.Pos()) {
defer Walk(&c, f)
break
}