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/interp/builtin.go
2019-10-27 18:14:22 -03:00

676 lines
13 KiB
Go

// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
// See LICENSE for licensing information
package interp
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/syntax"
)
func isBuiltin(name string) bool {
switch name {
case "true", ":", "false", "exit", "set", "shift", "unset",
"echo", "printf", "break", "continue", "pwd", "cd",
"wait", "builtin", "trap", "type", "source", ".", "command",
"dirs", "pushd", "popd", "umask", "alias", "unalias",
"fg", "bg", "getopts", "eval", "test", "[", "exec",
"return", "read", "shopt":
return true
}
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", ":":
case "false":
return 1
case "exit":
r.exitShell = true
switch len(args) {
case 0:
return r.exit
case 1:
n, err := strconv.Atoi(args[0])
if err != nil {
r.errf("invalid exit status code: %q\n", args[0])
return 2
}
return n
default:
r.errf("exit cannot take multiple arguments\n")
return 1
}
case "set":
if err := Params(args...)(r); err != nil {
r.errf("set: %v\n", err)
return 2
}
r.updateExpandOpts()
case "shift":
n := 1
switch len(args) {
case 0:
case 1:
if n2, err := strconv.Atoi(args[0]); err == nil {
n = n2
break
}
fallthrough
default:
r.errf("usage: shift [n]\n")
return 2
}
if n >= len(r.Params) {
r.Params = nil
} else {
r.Params = r.Params[n:]
}
case "unset":
vars := true
funcs := true
unsetOpts:
for i, arg := range args {
switch arg {
case "-v":
funcs = false
case "-f":
vars = false
default:
args = args[i:]
break unsetOpts
}
}
for _, arg := range args {
if vr := r.lookupVar(arg); vr.IsSet() && vars {
r.delVar(arg)
continue
}
if _, ok := r.Funcs[arg]; ok && funcs {
delete(r.Funcs, arg)
}
}
case "echo":
newline, doExpand := true, false
echoOpts:
for len(args) > 0 {
switch args[0] {
case "-n":
newline = false
case "-e":
doExpand = true
case "-E": // default
default:
break echoOpts
}
args = args[1:]
}
for i, arg := range args {
if i > 0 {
r.out(" ")
}
if doExpand {
arg, _, _ = expand.Format(r.ecfg, arg, nil)
}
r.out(arg)
}
if newline {
r.out("\n")
}
case "printf":
if len(args) == 0 {
r.errf("usage: printf format [arguments]\n")
return 2
}
format, args := args[0], args[1:]
for {
s, n, err := expand.Format(r.ecfg, format, args)
if err != nil {
r.errf("%v\n", err)
return 1
}
r.out(s)
args = args[n:]
if n == 0 || len(args) == 0 {
break
}
}
case "break", "continue":
if !r.inLoop {
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:
*enclosing = 1
case 1:
if n, err := strconv.Atoi(args[0]); err == nil {
*enclosing = n
break
}
fallthrough
default:
r.errf("usage: %s [n]\n", name)
return 2
}
case "pwd":
r.outf("%s\n", r.envGet("PWD"))
case "cd":
var path string
switch len(args) {
case 0:
path = r.envGet("HOME")
case 1:
path = args[0]
default:
r.errf("usage: cd [dir]\n")
return 2
}
return r.changeDir(path)
case "wait":
if len(args) > 0 {
panic("wait with args not handled yet")
}
switch err := r.bgShells.Wait().(type) {
case nil:
case ExitStatus:
default:
r.setErr(err)
}
case "builtin":
if len(args) < 1 {
break
}
if !isBuiltin(args[0]) {
return 1
}
return r.builtinCode(ctx, pos, args[0], args[1:])
case "type":
anyNotFound := false
for _, arg := range args {
if _, ok := r.Funcs[arg]; ok {
r.outf("%s is a function\n", arg)
continue
}
if isBuiltin(arg) {
r.outf("%s is a shell builtin\n", arg)
continue
}
if path, err := exec.LookPath(arg); err == nil {
r.outf("%s is %s\n", arg, path)
continue
}
r.errf("type: %s: not found\n", arg)
anyNotFound = true
}
if anyNotFound {
return 1
}
case "eval":
src := strings.Join(args, " ")
p := syntax.NewParser()
file, err := p.Parse(strings.NewReader(src), "")
if err != nil {
r.errf("eval: %v\n", err)
return 1
}
r.stmts(ctx, file.Stmts)
return r.exit
case "source", ".":
if len(args) < 1 {
r.errf("%v: source: need filename\n", pos)
return 2
}
f, err := r.open(ctx, args[0], os.O_RDONLY, 0, false)
if err != nil {
r.errf("source: %v\n", err)
return 1
}
defer f.Close()
p := syntax.NewParser()
file, err := p.Parse(f, args[0])
if err != nil {
r.errf("source: %v\n", err)
return 1
}
oldParams := r.Params
r.Params = args[1:]
oldInSource := r.inSource
r.inSource = true
r.stmts(ctx, file.Stmts)
r.Params = oldParams
r.inSource = oldInSource
if code, ok := r.err.(returnStatus); ok {
r.err = nil
return int(code)
}
return r.exit
case "[":
if len(args) == 0 || args[len(args)-1] != "]" {
r.errf("%v: [: missing matching ]\n", pos)
return 2
}
args = args[:len(args)-1]
fallthrough
case "test":
parseErr := false
p := testParser{
rem: args,
err: func(err error) {
r.errf("%v: %v\n", pos, err)
parseErr = true
},
}
p.next()
expr := p.classicTest("[", false)
if parseErr {
return 2
}
return oneIf(r.bashTest(ctx, expr, true) == "")
case "exec":
// TODO: Consider syscall.Exec, i.e. actually replacing
// the process. It's in theory what a shell should do,
// but in practice it would kill the entire Go process
// and it's not available on Windows.
if len(args) == 0 {
r.keepRedirs = true
break
}
r.exec(ctx, args)
r.exitShell = true
return r.exit
case "command":
show := false
for len(args) > 0 && strings.HasPrefix(args[0], "-") {
switch args[0] {
case "-v":
show = true
default:
r.errf("command: invalid option %s\n", args[0])
return 2
}
args = args[1:]
}
if len(args) == 0 {
break
}
if !show {
if isBuiltin(args[0]) {
return r.builtinCode(ctx, pos, args[0], args[1:])
}
r.exec(ctx, args)
return r.exit
}
last := 0
for _, arg := range args {
last = 0
if r.Funcs[arg] != nil || isBuiltin(arg) {
r.outf("%s\n", arg)
} else if path, err := exec.LookPath(arg); err == nil {
r.outf("%s\n", path)
} else {
last = 1
}
}
return last
case "dirs":
for i := len(r.dirStack) - 1; i >= 0; i-- {
r.outf("%s", r.dirStack[i])
if i > 0 {
r.out(" ")
}
}
r.out("\n")
case "pushd":
change := true
if len(args) > 0 && args[0] == "-n" {
change = false
args = args[1:]
}
swap := func() string {
oldtop := r.dirStack[len(r.dirStack)-1]
top := r.dirStack[len(r.dirStack)-2]
r.dirStack[len(r.dirStack)-1] = top
r.dirStack[len(r.dirStack)-2] = oldtop
return top
}
switch len(args) {
case 0:
if !change {
break
}
if len(r.dirStack) < 2 {
r.errf("pushd: no other directory\n")
return 1
}
newtop := swap()
if code := r.changeDir(newtop); code != 0 {
return code
}
r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
case 1:
if change {
if code := r.changeDir(args[0]); code != 0 {
return code
}
r.dirStack = append(r.dirStack, r.Dir)
} else {
r.dirStack = append(r.dirStack, args[0])
swap()
}
r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
default:
r.errf("pushd: too many arguments\n")
return 2
}
case "popd":
change := true
if len(args) > 0 && args[0] == "-n" {
change = false
args = args[1:]
}
switch len(args) {
case 0:
if len(r.dirStack) < 2 {
r.errf("popd: directory stack empty\n")
return 1
}
oldtop := r.dirStack[len(r.dirStack)-1]
r.dirStack = r.dirStack[:len(r.dirStack)-1]
if change {
newtop := r.dirStack[len(r.dirStack)-1]
if code := r.changeDir(newtop); code != 0 {
return code
}
} else {
r.dirStack[len(r.dirStack)-1] = oldtop
}
r.builtinCode(ctx, syntax.Pos{}, "dirs", nil)
default:
r.errf("popd: invalid argument\n")
return 2
}
case "return":
if !r.inFunc && !r.inSource {
r.errf("return: can only be done from a func or sourced script\n")
return 1
}
code := 0
switch len(args) {
case 0:
case 1:
code = atoi(args[0])
default:
r.errf("return: too many arguments\n")
return 2
}
r.setErr(returnStatus(code))
case "read":
raw := false
for len(args) > 0 && strings.HasPrefix(args[0], "-") {
switch args[0] {
case "-r":
raw = true
default:
r.errf("read: invalid option %q\n", args[0])
return 2
}
args = args[1:]
}
for _, name := range args {
if !syntax.ValidName(name) {
r.errf("read: invalid identifier %q\n", name)
return 2
}
}
line, err := r.readLine(raw)
if err != nil {
return 1
}
if len(args) == 0 {
args = append(args, "REPLY")
}
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(name, nil, expand.Variable{Kind: expand.String, Str: val})
}
return 0
case "getopts":
if len(args) < 2 {
r.errf("getopts: usage: getopts optstring name [arg]\n")
return 2
}
optind, _ := strconv.Atoi(r.envGet("OPTIND"))
if optind-1 != r.optState.argidx {
if optind < 1 {
optind = 1
}
r.optState = getopts{argidx: optind - 1}
}
optstr := args[0]
name := args[1]
if !syntax.ValidName(name) {
r.errf("getopts: invalid identifier: %q\n", name)
return 2
}
args = args[2:]
if len(args) == 0 {
args = r.Params
}
diagnostics := !strings.HasPrefix(optstr, ":")
opt, optarg, done := r.optState.Next(optstr, args)
r.setVarString(name, string(opt))
r.delVar("OPTARG")
switch {
case opt == '?' && diagnostics && !done:
r.errf("getopts: illegal option -- %q\n", optarg)
case opt == ':' && diagnostics:
r.errf("getopts: option requires an argument -- %q\n", optarg)
default:
if optarg != "" {
r.setVarString("OPTARG", optarg)
}
}
if optind-1 != r.optState.argidx {
r.setVarString("OPTIND", strconv.FormatInt(int64(r.optState.argidx+1), 10))
}
return oneIf(done)
case "shopt":
mode := ""
posixOpts := false
for len(args) > 0 && strings.HasPrefix(args[0], "-") {
switch args[0] {
case "-s", "-u":
mode = args[0]
case "-o":
posixOpts = true
case "-p", "-q":
panic(fmt.Sprintf("unhandled shopt flag: %s", args[0]))
default:
r.errf("shopt: invalid option %q\n", args[0])
return 2
}
args = args[1:]
}
if len(args) == 0 {
if !posixOpts {
for i, name := range bashOptsTable {
r.printOptLine(name, r.opts[len(shellOptsTable)+i])
}
break
}
for i, opt := range &shellOptsTable {
r.printOptLine(opt.name, r.opts[i])
}
break
}
for _, arg := range args {
opt := r.optByName(arg, !posixOpts)
if opt == nil {
r.errf("shopt: invalid option name %q\n", arg)
return 1
}
switch mode {
case "-s", "-u":
*opt = mode == "-s"
default: // ""
r.printOptLine(arg, *opt)
}
}
r.updateExpandOpts()
default:
// "trap", "umask", "alias", "unalias", "fg", "bg",
panic(fmt.Sprintf("unhandled builtin: %s", name))
}
return 0
}
func (r *Runner) printOptLine(name string, enabled bool) {
status := "off"
if enabled {
status = "on"
}
r.outf("%s\t%s\n", name, status)
}
func (r *Runner) readLine(raw bool) ([]byte, error) {
var line []byte
esc := false
for {
var buf [1]byte
n, err := r.Stdin.Read(buf[:])
if n > 0 {
b := buf[0]
switch {
case !raw && b == '\\':
line = append(line, b)
esc = !esc
case !raw && b == '\n' && esc:
// line continuation
line = line[len(line)-1:]
esc = false
case b == '\n':
return line, nil
default:
line = append(line, b)
esc = false
}
}
if err == io.EOF && len(line) > 0 {
return line, nil
}
if err != nil {
return nil, err
}
}
}
func (r *Runner) changeDir(path string) int {
path = r.absPath(path)
info, err := r.stat(path)
if err != nil || !info.IsDir() {
return 1
}
if !hasPermissionToDir(info) {
return 1
}
r.Dir = path
r.Vars["OLDPWD"] = r.Vars["PWD"]
r.Vars["PWD"] = expand.Variable{Kind: expand.String, Str: path}
return 0
}
func (r *Runner) absPath(path string) string {
if !filepath.IsAbs(path) {
path = filepath.Join(r.Dir, path)
}
return filepath.Clean(path)
}
type getopts struct {
argidx int
runeidx int
}
func (g *getopts) Next(optstr string, args []string) (opt rune, optarg string, done bool) {
if len(args) == 0 || g.argidx >= len(args) {
return '?', "", true
}
arg := []rune(args[g.argidx])
if len(arg) < 2 || arg[0] != '-' || arg[1] == '-' {
return '?', "", true
}
opts := arg[1:]
opt = opts[g.runeidx]
if g.runeidx+1 < len(opts) {
g.runeidx++
} else {
g.argidx++
g.runeidx = 0
}
i := strings.IndexRune(optstr, opt)
if i < 0 {
// invalid option
return '?', string(opt), false
}
if i+1 < len(optstr) && optstr[i+1] == ':' {
if g.argidx >= len(args) {
// missing argument
return ':', string(opt), false
}
optarg = args[g.argidx]
g.argidx++
g.runeidx = 0
}
return opt, optarg, false
}