1
0
mirror of https://github.com/go-task/task.git synced 2024-12-16 10:59:23 +02:00
task/vendor/mvdan.cc/sh/v3/expand/expand.go
2019-11-24 21:00:02 -03:00

848 lines
20 KiB
Go

// 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/v3/pattern"
"mvdan.cc/sh/v3/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
// 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)
// GlobStar corresponds to the shell option that allows globbing with
// "**".
GlobStar bool
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) error {
wenv, ok := cfg.Env.(WriteEnviron)
if !ok {
return fmt.Errorf("environment is read-only")
}
return wenv.Set(name, Variable{Kind: String, Str: 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(pattern.QuoteMeta(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()
var fmts []byte
initialArgs := len(args)
for i := 0; i < len(format); i++ {
// readDigits reads from 0 to max digits, either octal or
// hexadecimal.
readDigits := func(max int, hex bool) string {
j := 0
for ; j < max; j++ {
c := format[i+j]
if (c >= '0' && c <= '9') ||
(hex && c >= 'a' && c <= 'f') ||
(hex && c >= 'A' && c <= 'F') {
// valid octal or hex char
} else {
break
}
}
digits := format[i : i+j]
i += j - 1 // -1 since the outer loop does i++
return digits
}
c := format[i]
switch {
case c == '\\': // escaped
i++
switch c = format[i]; c {
case 'a': // bell
buf.WriteByte('\a')
case 'b': // backspace
buf.WriteByte('\b')
case 'e', 'E': // escape
buf.WriteByte('\x1b')
case 'f': // form feed
buf.WriteByte('\f')
case 'n': // new line
buf.WriteByte('\n')
case 'r': // carriage return
buf.WriteByte('\r')
case 't': // horizontal tab
buf.WriteByte('\t')
case 'v': // vertical tab
buf.WriteByte('\v')
case '\\', '\'', '"', '?': // just the character
buf.WriteByte(c)
case '0', '1', '2', '3', '4', '5', '6', '7':
digits := readDigits(3, false)
// if digits don't fit in 8 bits, 0xff via strconv
n, _ := strconv.ParseUint(digits, 8, 8)
buf.WriteByte(byte(n))
case 'x', 'u', 'U':
i++
max := 2
if c == 'u' {
max = 4
} else if c == 'U' {
max = 8
}
digits := readDigits(max, true)
if len(digits) > 0 {
// can't error
n, _ := strconv.ParseUint(digits, 16, 32)
if c == 'x' {
// always as a single byte
buf.WriteByte(byte(n))
} else {
buf.WriteRune(rune(n))
}
break
}
fallthrough
default: // no escape sequence
buf.WriteByte('\\')
buf.WriteByte(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 args != nil && c == '%':
// if args == nil, we are not doing format
// arguments
fmts = []byte{c}
default:
buf.WriteByte(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(pattern.QuoteMeta(part.val))
continue
}
buf.WriteString(part.val)
if pattern.HasMeta(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 _, word := range words {
afterBraces := []*syntax.Word{word}
if syntax.SplitBraces(word) {
afterBraces = Braces(word)
}
for _, word2 := range afterBraces {
wfields, err := cfg.wordFields(word2.Parts)
if err != nil {
return nil, err
}
for _, field := range wfields {
path, doGlob := cfg.escapedGlobField(field)
var matches []string
if doGlob && cfg.ReadDir != nil {
matches, err = cfg.glob(dir, path)
if err != nil {
return nil, err
}
if len(matches) > 0 {
fields = append(fields, matches...)
continue
}
}
fields = append(fields, cfg.fieldJoin(field))
}
}
}
return fields, nil
}
type fieldPart struct {
val string
quote quoteLevel
}
type quoteLevel uint
const (
quoteNone quoteLevel = iota
quoteDouble
quoteSingle
)
func (cfg *Config) wordField(wps []syntax.WordPart, ql quoteLevel) ([]fieldPart, error) {
var field []fieldPart
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 && ql == quoteNone {
if prefix, rest := cfg.expandUser(s); prefix != "" {
// TODO: return two separate fieldParts,
// like in wordFields?
s = prefix + rest
}
}
if ql == quoteDouble && strings.Contains(s, "\\") {
buf := cfg.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' && i+1 < len(s) {
switch s[i+1] {
case '"', '\\', '$', '`': // special chars
continue
}
}
buf.WriteByte(b)
}
s = buf.String()
}
field = append(field, fieldPart{val: s})
case *syntax.SglQuoted:
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
fp.val, _, _ = Format(cfg, fp.val, nil)
}
field = append(field, fp)
case *syntax.DblQuoted:
wfield, err := cfg.wordField(x.Parts, quoteDouble)
if err != nil {
return nil, err
}
for _, part := range wfield {
part.quote = quoteDouble
field = append(field, part)
}
case *syntax.ParamExp:
val, err := cfg.paramExp(x)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: val})
case *syntax.CmdSubst:
val, err := cfg.cmdSubst(x)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: val})
case *syntax.ArithmExp:
n, err := Arithm(cfg, x.X)
if err != nil {
return nil, err
}
field = append(field, fieldPart{val: strconv.Itoa(n)})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
return field, nil
}
func (cfg *Config) cmdSubst(cs *syntax.CmdSubst) (string, error) {
if cfg.CmdSubst == nil {
return "", UnexpectedCommandError{Node: cs}
}
buf := cfg.strBuilder()
if err := cfg.CmdSubst(buf, cs); err != nil {
return "", err
}
return strings.TrimRight(buf.String(), "\n"), nil
}
func (cfg *Config) wordFields(wps []syntax.WordPart) ([][]fieldPart, error) {
fields := cfg.fieldsAlloc[:0]
curField := cfg.fieldAlloc[:0]
allowEmpty := false
flush := func() {
if len(curField) == 0 {
return
}
fields = append(fields, curField)
curField = nil
}
splitAdd := func(val string) {
for i, field := range strings.FieldsFunc(val, cfg.ifsRune) {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{val: field})
}
}
for i, wp := range wps {
switch x := wp.(type) {
case *syntax.Lit:
s := x.Value
if i == 0 {
prefix, rest := cfg.expandUser(s)
curField = append(curField, fieldPart{
quote: quoteSingle,
val: prefix,
})
s = rest
}
if strings.Contains(s, "\\") {
buf := cfg.strBuilder()
for i := 0; i < len(s); i++ {
b := s[i]
if b == '\\' {
i++
b = s[i]
}
buf.WriteByte(b)
}
s = buf.String()
}
curField = append(curField, fieldPart{val: s})
case *syntax.SglQuoted:
allowEmpty = true
fp := fieldPart{quote: quoteSingle, val: x.Value}
if x.Dollar {
fp.val, _, _ = Format(cfg, fp.val, nil)
}
curField = append(curField, fp)
case *syntax.DblQuoted:
allowEmpty = true
if len(x.Parts) == 1 {
pe, _ := x.Parts[0].(*syntax.ParamExp)
if elems := cfg.quotedElems(pe); elems != nil {
for i, elem := range elems {
if i > 0 {
flush()
}
curField = append(curField, fieldPart{
quote: quoteDouble,
val: elem,
})
}
continue
}
}
wfield, err := cfg.wordField(x.Parts, quoteDouble)
if err != nil {
return nil, err
}
for _, part := range wfield {
part.quote = quoteDouble
curField = append(curField, part)
}
case *syntax.ParamExp:
val, err := cfg.paramExp(x)
if err != nil {
return nil, err
}
splitAdd(val)
case *syntax.CmdSubst:
val, err := cfg.cmdSubst(x)
if err != nil {
return nil, err
}
splitAdd(val)
case *syntax.ArithmExp:
n, err := Arithm(cfg, x.X)
if err != nil {
return nil, err
}
curField = append(curField, fieldPart{val: strconv.Itoa(n)})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
flush()
if allowEmpty && len(fields) == 0 {
fields = append(fields, curField)
}
return fields, nil
}
// quotedElems checks if a parameter expansion is exactly ${@} or ${foo[@]}
func (cfg *Config) quotedElems(pe *syntax.ParamExp) []string {
if pe == nil || pe.Excl || pe.Length || pe.Width {
return nil
}
if pe.Param.Value == "@" {
return cfg.Env.Get("@").List
}
if nodeLit(pe.Index) != "@" {
return nil
}
vr := cfg.Env.Get(pe.Param.Value)
if vr.Kind == Indexed {
return vr.List
}
return nil
}
func (cfg *Config) expandUser(field string) (prefix, rest string) {
if len(field) == 0 || field[0] != '~' {
return "", field
}
name := field[1:]
if i := strings.Index(name, "/"); i >= 0 {
rest = name[i:]
name = name[:i]
}
if name == "" {
// Current user; try via "HOME", otherwise fall back to the
// system's appropriate home dir env var. Don't use os/user, as
// that's overkill. We can't use os.UserHomeDir, because we want
// to use cfg.Env, and we always want to check "HOME" first.
if vr := cfg.Env.Get("HOME"); vr.IsSet() {
return vr.String(), rest
}
if runtime.GOOS == "windows" {
if vr := cfg.Env.Get("USERPROFILE"); vr.IsSet() {
return vr.String(), rest
}
}
return "", field
}
// Not the current user; try via "HOME <name>", otherwise fall back to
// os/user. There isn't a way to lookup user home dirs without cgo.
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(pat, name string, n int) [][]int {
expr, err := pattern.Regexp(pat, 0)
if err != nil {
return nil
}
rx := regexp.MustCompile(expr)
return rx.FindAllStringIndex(name, n)
}
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, pat string) ([]string, error) {
parts := pathSplit(pat)
matches := []string{""}
if filepath.IsAbs(pat) {
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 i, part := range parts {
wantDir := i < len(parts)-1
switch {
case part == "", part == ".", part == "..":
for i, dir := range matches {
matches[i] = pathJoin2(dir, part)
}
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, wantDir, 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 := pattern.Regexp(part, pattern.Filenames)
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, wantDir, newMatches)
if err != nil {
return nil, err
}
}
matches = newMatches
}
return matches, nil
}
func (cfg *Config) globDir(base, dir string, rx *regexp.Regexp, wantDir bool, matches []string) ([]string, error) {
fullDir := dir
if !filepath.IsAbs(dir) {
fullDir = filepath.Join(base, dir)
}
infos, err := cfg.ReadDir(fullDir)
if err != nil {
return nil, err
}
for _, info := range infos {
name := info.Name()
if !wantDir {
// no filtering
} else if mode := info.Mode(); mode&os.ModeSymlink != 0 {
// TODO: is there a way to do this without the
// extra syscall?
if _, err := cfg.ReadDir(filepath.Join(fullDir, name)); err != nil {
// symlink pointing to non-directory
continue
}
} else if !mode.IsDir() {
// definitely not a directory
continue
}
if !strings.HasPrefix(rx.String(), `^\.`) && name[0] == '.' {
continue
}
if rx.MatchString(name) {
matches = append(matches, pathJoin2(dir, name))
}
}
return matches, nil
}
// ReadFields TODO write doc.
//
// 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]
}
fields := make([]string, len(fpos))
for i, p := range fpos {
fields[i] = string(runes[p.start:p.end])
}
return fields
}