1
0
mirror of https://github.com/go-task/task.git synced 2024-12-14 10:52:43 +02:00
task/vendor/mvdan.cc/sh/interp/expand.go
2018-06-24 10:40:44 -03:00

501 lines
11 KiB
Go

// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package interp
import (
"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(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(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(word *syntax.Word) string {
if word == nil {
return ""
}
field := r.wordField(word.Parts, quoteDouble)
return r.fieldJoin(field)
}
func (r *Runner) lonePattern(word *syntax.Word) string {
field := r.wordField(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(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
}
type fieldPart struct {
val string
quote quoteLevel
}
type quoteLevel uint
const (
quoteNone quoteLevel = iota
quoteDouble
quoteSingle
)
func (r *Runner) wordField(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(x.Parts, quoteDouble) {
part.quote = quoteDouble
field = append(field, part)
}
case *syntax.ParamExp:
field = append(field, fieldPart{val: r.paramExp(x)})
case *syntax.CmdSubst:
field = append(field, fieldPart{val: r.cmdSubst(x)})
case *syntax.ArithmExp:
field = append(field, fieldPart{
val: strconv.Itoa(r.arithm(x.X)),
})
default:
panic(fmt.Sprintf("unhandled word part: %T", x))
}
}
return field
}
func (r *Runner) cmdSubst(cs *syntax.CmdSubst) string {
r2 := r.sub()
buf := r.strBuilder()
r2.Stdout = buf
r2.stmts(cs.StmtList)
r.setErr(r2.err)
return strings.TrimRight(buf.String(), "\n")
}
func (r *Runner) wordFields(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(x.Parts, quoteDouble) {
part.quote = quoteDouble
curField = append(curField, part)
}
case *syntax.ParamExp:
splitAdd(r.paramExp(x))
case *syntax.CmdSubst:
splitAdd(r.cmdSubst(x))
case *syntax.ArithmExp:
curField = append(curField, fieldPart{
val: strconv.Itoa(r.arithm(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
}