mirror of
https://github.com/go-task/task.git
synced 2024-12-16 10:59:23 +02:00
967 lines
20 KiB
Go
967 lines
20 KiB
Go
// Copyright (c) 2017, Daniel Martí <mvdan@mvdan.cc>
|
|
// See LICENSE for licensing information
|
|
|
|
package interp
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"mvdan.cc/sh/syntax"
|
|
)
|
|
|
|
// A Runner interprets shell programs. It cannot be reused once a
|
|
// program has been interpreted.
|
|
//
|
|
// Note that writes to Stdout and Stderr may not be sequential. If
|
|
// you plan on using an io.Writer implementation that isn't safe for
|
|
// concurrent use, consider a workaround like hiding writes behind a
|
|
// mutex.
|
|
type Runner struct {
|
|
// Env specifies the environment of the interpreter.
|
|
// If Env is nil, Run uses the current process's environment.
|
|
Env []string
|
|
|
|
// envMap is just Env as a map, to simplify and speed up its use
|
|
envMap map[string]string
|
|
|
|
// Dir specifies the working directory of the command. If Dir is
|
|
// the empty string, Run runs the command in the calling
|
|
// process's current directory.
|
|
Dir string
|
|
|
|
// Params are the current parameters, e.g. from running a shell
|
|
// file or calling a function. Accessible via the $@/$* family
|
|
// of vars.
|
|
Params []string
|
|
|
|
Exec ModuleExec
|
|
Open ModuleOpen
|
|
|
|
filename string // only if Node was a File
|
|
|
|
// Separate maps, note that bash allows a name to be both a var
|
|
// and a func simultaneously
|
|
Vars map[string]Variable
|
|
Funcs map[string]*syntax.Stmt
|
|
|
|
// like Vars, but local to a func i.e. "local foo=bar"
|
|
funcVars map[string]Variable
|
|
|
|
// like Vars, but local to a cmd i.e. "foo=bar prog args..."
|
|
cmdVars map[string]string
|
|
|
|
// >0 to break or continue out of N enclosing loops
|
|
breakEnclosing, contnEnclosing int
|
|
|
|
inLoop bool
|
|
inFunc bool
|
|
inSource bool
|
|
|
|
err error // current fatal error
|
|
exit int // current (last) exit code
|
|
|
|
Stdin io.Reader
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
|
|
bgShells sync.WaitGroup
|
|
|
|
// Context can be used to cancel the interpreter before it finishes
|
|
Context context.Context
|
|
|
|
shellOpts [len(shellOptsTable)]bool
|
|
|
|
dirStack []string
|
|
|
|
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
|
|
|
|
// KillTimeout holds how much time the interpreter will wait for a
|
|
// program to stop after being sent an interrupt signal, after
|
|
// which a kill signal will be sent. This process will happen when the
|
|
// interpreter's context is cancelled.
|
|
//
|
|
// The zero value will default to 2 seconds.
|
|
//
|
|
// A negative value means that a kill signal will be sent immediately.
|
|
//
|
|
// 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
|
|
braceAlloc braceWord
|
|
bracePartsAlloc [4]braceWordPart
|
|
}
|
|
|
|
func (r *Runner) strBuilder() *bytes.Buffer {
|
|
b := &r.bufferAlloc
|
|
b.Reset()
|
|
return b
|
|
}
|
|
|
|
func (r *Runner) optByFlag(flag string) *bool {
|
|
for i, opt := range shellOptsTable {
|
|
if opt.flag == flag {
|
|
return &r.shellOpts[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) optByName(name string) *bool {
|
|
for i, opt := range shellOptsTable {
|
|
if opt.name == name {
|
|
return &r.shellOpts[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var shellOptsTable = [...]struct {
|
|
flag, name string
|
|
}{
|
|
// sorted alphabetically by name; use a space for the options
|
|
// that have no flag form
|
|
{"a", "allexport"},
|
|
{"e", "errexit"},
|
|
{"n", "noexec"},
|
|
{"f", "noglob"},
|
|
{"u", "nounset"},
|
|
{" ", "pipefail"},
|
|
}
|
|
|
|
// To access the shell options arrays without a linear search when we
|
|
// know which option we're after at compile time.
|
|
const (
|
|
optAllExport = iota
|
|
optErrExit
|
|
optNoExec
|
|
optNoGlob
|
|
optNoUnset
|
|
optPipeFail
|
|
)
|
|
|
|
// Reset will set the unexported fields back to zero, fill any exported
|
|
// fields with their default values if not set, and prepare the runner
|
|
// to interpret a program.
|
|
//
|
|
// This function should be called once before running any node. It can
|
|
// be skipped before any following runs to keep internal state, such as
|
|
// declared variables.
|
|
func (r *Runner) Reset() error {
|
|
// reset the internal state
|
|
*r = Runner{
|
|
Env: r.Env,
|
|
Dir: r.Dir,
|
|
Params: r.Params,
|
|
Context: r.Context,
|
|
Stdin: r.Stdin,
|
|
Stdout: r.Stdout,
|
|
Stderr: r.Stderr,
|
|
Exec: r.Exec,
|
|
Open: r.Open,
|
|
KillTimeout: r.KillTimeout,
|
|
|
|
// emptied below, to reuse the space
|
|
envMap: r.envMap,
|
|
Vars: r.Vars,
|
|
cmdVars: r.cmdVars,
|
|
dirStack: r.dirStack[:0],
|
|
}
|
|
if r.envMap == nil {
|
|
r.envMap = make(map[string]string)
|
|
} else {
|
|
for k := range r.envMap {
|
|
delete(r.envMap, k)
|
|
}
|
|
}
|
|
if r.Vars == nil {
|
|
r.Vars = make(map[string]Variable)
|
|
} else {
|
|
for k := range r.Vars {
|
|
delete(r.Vars, k)
|
|
}
|
|
}
|
|
if r.cmdVars == nil {
|
|
r.cmdVars = make(map[string]string)
|
|
} else {
|
|
for k := range r.cmdVars {
|
|
delete(r.cmdVars, k)
|
|
}
|
|
}
|
|
if r.Context == nil {
|
|
r.Context = context.Background()
|
|
}
|
|
if r.Env == nil {
|
|
r.Env = os.Environ()
|
|
}
|
|
for _, kv := range r.Env {
|
|
i := strings.IndexByte(kv, '=')
|
|
if i < 0 {
|
|
return fmt.Errorf("env not in the form key=value: %q", kv)
|
|
}
|
|
name, val := kv[:i], kv[i+1:]
|
|
if runtime.GOOS == "windows" {
|
|
name = strings.ToUpper(name)
|
|
}
|
|
r.envMap[name] = val
|
|
}
|
|
if _, ok := r.envMap["HOME"]; !ok {
|
|
u, _ := user.Current()
|
|
r.Vars["HOME"] = Variable{Value: StringVal(u.HomeDir)}
|
|
}
|
|
if r.Dir == "" {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return fmt.Errorf("could not get current dir: %v", err)
|
|
}
|
|
r.Dir = dir
|
|
}
|
|
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")}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// convert $PATH to a unix path list
|
|
path := r.envMap["PATH"]
|
|
path = strings.Join(filepath.SplitList(path), ":")
|
|
r.Vars["PATH"] = Variable{Value: StringVal(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
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Runner) ctx() Ctxt {
|
|
c := Ctxt{
|
|
Context: r.Context,
|
|
Env: r.Env,
|
|
Dir: r.Dir,
|
|
Stdin: r.Stdin,
|
|
Stdout: r.Stdout,
|
|
Stderr: r.Stderr,
|
|
KillTimeout: r.KillTimeout,
|
|
}
|
|
for name, vr := range r.Vars {
|
|
if !vr.Exported {
|
|
continue
|
|
}
|
|
c.Env = append(c.Env, name+"="+r.varStr(vr, 0))
|
|
}
|
|
for name, val := range r.cmdVars {
|
|
c.Env = append(c.Env, name+"="+val)
|
|
}
|
|
return c
|
|
}
|
|
|
|
type ExitCode uint8
|
|
|
|
func (e ExitCode) Error() string { return fmt.Sprintf("exit status %d", e) }
|
|
|
|
func (r *Runner) setErr(err error) {
|
|
if r.err == nil {
|
|
r.err = err
|
|
}
|
|
}
|
|
|
|
func (r *Runner) lastExit() {
|
|
if r.err == nil {
|
|
r.err = ExitCode(r.exit)
|
|
}
|
|
}
|
|
|
|
// FromArgs populates the shell options and returns the remaining
|
|
// arguments. For example, running FromArgs("-e", "--", "foo") will set
|
|
// the "-e" option and return []string{"foo"}.
|
|
//
|
|
// This is similar to what the interpreter's "set" builtin does.
|
|
func (r *Runner) FromArgs(args ...string) ([]string, error) {
|
|
for len(args) > 0 {
|
|
arg := args[0]
|
|
if arg == "" || (arg[0] != '-' && arg[0] != '+') {
|
|
break
|
|
}
|
|
if arg == "--" {
|
|
args = args[1:]
|
|
break
|
|
}
|
|
enable := arg[0] == '-'
|
|
var opt *bool
|
|
if flag := arg[1:]; flag == "o" {
|
|
args = args[1:]
|
|
if len(args) == 0 && enable {
|
|
for i, opt := range shellOptsTable {
|
|
status := "off"
|
|
if r.shellOpts[i] {
|
|
status = "on"
|
|
}
|
|
r.outf("%s:\t%s\n", opt.name, status)
|
|
}
|
|
break
|
|
}
|
|
if len(args) == 0 && !enable {
|
|
for i, opt := range shellOptsTable {
|
|
setFlag := "+o"
|
|
if r.shellOpts[i] {
|
|
setFlag = "-o"
|
|
}
|
|
r.outf("set %s %s\n", setFlag, opt.name)
|
|
}
|
|
break
|
|
}
|
|
opt = r.optByName(args[0])
|
|
} else {
|
|
opt = r.optByFlag(flag)
|
|
}
|
|
if opt == nil {
|
|
return nil, fmt.Errorf("invalid option: %q", arg)
|
|
}
|
|
*opt = enable
|
|
args = args[1:]
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
// Run starts the interpreter and returns any error.
|
|
func (r *Runner) Run(node syntax.Node) error {
|
|
r.filename = ""
|
|
switch x := node.(type) {
|
|
case *syntax.File:
|
|
r.filename = x.Name
|
|
r.stmts(x.StmtList)
|
|
case *syntax.Stmt:
|
|
r.stmt(x)
|
|
case syntax.Command:
|
|
r.cmd(x)
|
|
default:
|
|
return fmt.Errorf("Node can only be File, Stmt, or Command: %T", x)
|
|
}
|
|
r.lastExit()
|
|
if r.err == ExitCode(0) {
|
|
r.err = nil
|
|
}
|
|
return r.err
|
|
}
|
|
|
|
func (r *Runner) Stmt(stmt *syntax.Stmt) error {
|
|
r.stmt(stmt)
|
|
return r.err
|
|
}
|
|
|
|
func (r *Runner) out(s string) {
|
|
io.WriteString(r.Stdout, s)
|
|
}
|
|
|
|
func (r *Runner) outf(format string, a ...interface{}) {
|
|
fmt.Fprintf(r.Stdout, format, a...)
|
|
}
|
|
|
|
func (r *Runner) errf(format string, a ...interface{}) {
|
|
fmt.Fprintf(r.Stderr, format, a...)
|
|
}
|
|
|
|
func (r *Runner) stop() bool {
|
|
if r.err != nil {
|
|
return true
|
|
}
|
|
if err := r.Context.Err(); err != nil {
|
|
r.err = err
|
|
return true
|
|
}
|
|
if r.shellOpts[optNoExec] {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (r *Runner) stmt(st *syntax.Stmt) {
|
|
if r.stop() {
|
|
return
|
|
}
|
|
if st.Background {
|
|
r.bgShells.Add(1)
|
|
r2 := r.sub()
|
|
go func() {
|
|
r2.stmtSync(st)
|
|
r.bgShells.Done()
|
|
}()
|
|
} else {
|
|
r.stmtSync(st)
|
|
}
|
|
}
|
|
|
|
func (r *Runner) stmtSync(st *syntax.Stmt) {
|
|
oldIn, oldOut, oldErr := r.Stdin, r.Stdout, r.Stderr
|
|
for _, rd := range st.Redirs {
|
|
cls, err := r.redir(rd)
|
|
if err != nil {
|
|
r.exit = 1
|
|
return
|
|
}
|
|
if cls != nil {
|
|
defer cls.Close()
|
|
}
|
|
}
|
|
if st.Cmd == nil {
|
|
r.exit = 0
|
|
} else {
|
|
r.cmd(st.Cmd)
|
|
}
|
|
if st.Negated {
|
|
r.exit = oneIf(r.exit == 0)
|
|
}
|
|
if r.exit != 0 && r.shellOpts[optErrExit] {
|
|
r.lastExit()
|
|
}
|
|
if !r.keepRedirs {
|
|
r.Stdin, r.Stdout, r.Stderr = oldIn, oldOut, oldErr
|
|
}
|
|
}
|
|
|
|
func (r *Runner) sub() *Runner {
|
|
r2 := *r
|
|
r2.bgShells = sync.WaitGroup{}
|
|
r2.bufferAlloc = bytes.Buffer{}
|
|
// TODO: perhaps we could do a lazy copy here, or some sort of
|
|
// overlay to avoid copying all the time
|
|
r2.envMap = make(map[string]string, len(r.envMap))
|
|
for k, v := range r.envMap {
|
|
r2.envMap[k] = v
|
|
}
|
|
r2.Vars = make(map[string]Variable, len(r.Vars))
|
|
for k, v := range r.Vars {
|
|
r2.Vars[k] = v
|
|
}
|
|
r2.cmdVars = make(map[string]string, len(r.cmdVars))
|
|
for k, v := range r.cmdVars {
|
|
r2.cmdVars[k] = v
|
|
}
|
|
return &r2
|
|
}
|
|
|
|
func (r *Runner) cmd(cm syntax.Command) {
|
|
if r.stop() {
|
|
return
|
|
}
|
|
switch x := cm.(type) {
|
|
case *syntax.Block:
|
|
r.stmts(x.StmtList)
|
|
case *syntax.Subshell:
|
|
r2 := r.sub()
|
|
r2.stmts(x.StmtList)
|
|
r.exit = r2.exit
|
|
r.setErr(r2.err)
|
|
case *syntax.CallExpr:
|
|
fields := r.Fields(x.Args...)
|
|
if len(fields) == 0 {
|
|
for _, as := range x.Assigns {
|
|
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(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.call(x.Args[0].Pos(), fields)
|
|
// cmdVars can be nuked here, as they are never useful
|
|
// again once we nest into further levels of inline
|
|
// vars.
|
|
for k := range r.cmdVars {
|
|
delete(r.cmdVars, k)
|
|
}
|
|
case *syntax.BinaryCmd:
|
|
switch x.Op {
|
|
case syntax.AndStmt:
|
|
r.stmt(x.X)
|
|
if r.exit == 0 {
|
|
r.stmt(x.Y)
|
|
}
|
|
case syntax.OrStmt:
|
|
r.stmt(x.X)
|
|
if r.exit != 0 {
|
|
r.stmt(x.Y)
|
|
}
|
|
case syntax.Pipe, syntax.PipeAll:
|
|
pr, pw := io.Pipe()
|
|
r2 := r.sub()
|
|
r2.Stdout = pw
|
|
if x.Op == syntax.PipeAll {
|
|
r2.Stderr = pw
|
|
} else {
|
|
r2.Stderr = r.Stderr
|
|
}
|
|
r.Stdin = pr
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
r2.stmt(x.X)
|
|
pw.Close()
|
|
wg.Done()
|
|
}()
|
|
r.stmt(x.Y)
|
|
pr.Close()
|
|
wg.Wait()
|
|
if r.shellOpts[optPipeFail] && r2.exit > 0 && r.exit == 0 {
|
|
r.exit = r2.exit
|
|
}
|
|
r.setErr(r2.err)
|
|
}
|
|
case *syntax.IfClause:
|
|
r.stmts(x.Cond)
|
|
if r.exit == 0 {
|
|
r.stmts(x.Then)
|
|
break
|
|
}
|
|
r.exit = 0
|
|
r.stmts(x.Else)
|
|
case *syntax.WhileClause:
|
|
for !r.stop() {
|
|
r.stmts(x.Cond)
|
|
stop := (r.exit == 0) == x.Until
|
|
r.exit = 0
|
|
if stop || r.loopStmtsBroken(x.Do) {
|
|
break
|
|
}
|
|
}
|
|
case *syntax.ForClause:
|
|
switch y := x.Loop.(type) {
|
|
case *syntax.WordIter:
|
|
name := y.Name.Value
|
|
for _, field := range r.Fields(y.Items...) {
|
|
r.setVarString(name, field)
|
|
if r.loopStmtsBroken(x.Do) {
|
|
break
|
|
}
|
|
}
|
|
case *syntax.CStyleLoop:
|
|
r.arithm(y.Init)
|
|
for r.arithm(y.Cond) != 0 {
|
|
if r.loopStmtsBroken(x.Do) {
|
|
break
|
|
}
|
|
r.arithm(y.Post)
|
|
}
|
|
}
|
|
case *syntax.FuncDecl:
|
|
r.setFunc(x.Name.Value, x.Body)
|
|
case *syntax.ArithmCmd:
|
|
r.exit = oneIf(r.arithm(x.X) == 0)
|
|
case *syntax.LetClause:
|
|
var val int
|
|
for _, expr := range x.Exprs {
|
|
val = r.arithm(expr)
|
|
}
|
|
r.exit = oneIf(val == 0)
|
|
case *syntax.CaseClause:
|
|
str := r.loneWord(x.Word)
|
|
for _, ci := range x.Items {
|
|
for _, word := range ci.Patterns {
|
|
pat := r.lonePattern(word)
|
|
if match(pat, str) {
|
|
r.stmts(ci.StmtList)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
case *syntax.TestClause:
|
|
r.exit = 0
|
|
if r.bashTest(x.X) == "" && r.exit == 0 {
|
|
// to preserve exit code 2 for regex
|
|
// errors, etc
|
|
r.exit = 1
|
|
}
|
|
case *syntax.DeclClause:
|
|
local := 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.
|
|
local = r.inFunc
|
|
case "local":
|
|
if !r.inFunc {
|
|
r.errf("local: can only be used in a function\n")
|
|
r.exit = 1
|
|
return
|
|
}
|
|
local = true
|
|
case "export":
|
|
modes = append(modes, "-x")
|
|
case "readonly":
|
|
modes = append(modes, "-r")
|
|
case "nameref":
|
|
modes = append(modes, "-n")
|
|
}
|
|
for _, opt := range x.Opts {
|
|
switch s := r.loneWord(opt); s {
|
|
case "-x", "-r", "-n":
|
|
modes = append(modes, s)
|
|
case "-a", "-A":
|
|
valType = s
|
|
case "-g":
|
|
local = false
|
|
default:
|
|
r.errf("declare: invalid option %q\n", s)
|
|
r.exit = 2
|
|
return
|
|
}
|
|
}
|
|
for _, as := range x.Assigns {
|
|
for _, as := range r.expandAssigns(as) {
|
|
name := as.Name.Value
|
|
vr, _ := r.lookupVar(as.Name.Value)
|
|
vr.Value = r.assignVal(as, valType)
|
|
vr.Local = local
|
|
for _, mode := range modes {
|
|
switch mode {
|
|
case "-x":
|
|
vr.Exported = true
|
|
case "-r":
|
|
vr.ReadOnly = true
|
|
case "-n":
|
|
vr.NameRef = true
|
|
}
|
|
}
|
|
r.setVar(name, as.Index, vr)
|
|
}
|
|
}
|
|
case *syntax.TimeClause:
|
|
start := time.Now()
|
|
if x.Stmt != nil {
|
|
r.stmt(x.Stmt)
|
|
}
|
|
format := "%s\t%s\n"
|
|
if x.PosixFormat {
|
|
format = "%s %s\n"
|
|
} else {
|
|
r.outf("\n")
|
|
}
|
|
real := time.Since(start)
|
|
r.outf(format, "real", elapsedString(real, x.PosixFormat))
|
|
// TODO: can we do these?
|
|
r.outf(format, "user", elapsedString(0, x.PosixFormat))
|
|
r.outf(format, "sys", elapsedString(0, x.PosixFormat))
|
|
default:
|
|
panic(fmt.Sprintf("unhandled command node: %T", x))
|
|
}
|
|
}
|
|
|
|
func elapsedString(d time.Duration, posix bool) string {
|
|
if posix {
|
|
return fmt.Sprintf("%.2f", d.Seconds())
|
|
}
|
|
min := int(d.Minutes())
|
|
sec := math.Remainder(d.Seconds(), 60.0)
|
|
return fmt.Sprintf("%dm%.3fs", min, sec)
|
|
}
|
|
|
|
func (r *Runner) stmts(sl syntax.StmtList) {
|
|
for _, stmt := range sl.Stmts {
|
|
r.stmt(stmt)
|
|
}
|
|
}
|
|
|
|
func (r *Runner) redir(rd *syntax.Redirect) (io.Closer, error) {
|
|
if rd.Hdoc != nil {
|
|
hdoc := r.loneWord(rd.Hdoc)
|
|
r.Stdin = strings.NewReader(hdoc)
|
|
return nil, nil
|
|
}
|
|
orig := &r.Stdout
|
|
if rd.N != nil {
|
|
switch rd.N.Value {
|
|
case "1":
|
|
case "2":
|
|
orig = &r.Stderr
|
|
}
|
|
}
|
|
arg := r.loneWord(rd.Word)
|
|
switch rd.Op {
|
|
case syntax.WordHdoc:
|
|
r.Stdin = strings.NewReader(arg + "\n")
|
|
return nil, nil
|
|
case syntax.DplOut:
|
|
switch arg {
|
|
case "1":
|
|
*orig = r.Stdout
|
|
case "2":
|
|
*orig = r.Stderr
|
|
}
|
|
return nil, nil
|
|
case syntax.RdrIn, syntax.RdrOut, syntax.AppOut,
|
|
syntax.RdrAll, syntax.AppAll:
|
|
// done further below
|
|
// case syntax.DplIn:
|
|
default:
|
|
panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op))
|
|
}
|
|
mode := os.O_RDONLY
|
|
switch rd.Op {
|
|
case syntax.AppOut, syntax.AppAll:
|
|
mode = os.O_RDWR | os.O_CREATE | os.O_APPEND
|
|
case syntax.RdrOut, syntax.RdrAll:
|
|
mode = os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
|
}
|
|
f, err := r.open(r.relPath(arg), mode, 0644, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch rd.Op {
|
|
case syntax.RdrIn:
|
|
r.Stdin = f
|
|
case syntax.RdrOut, syntax.AppOut:
|
|
*orig = f
|
|
case syntax.RdrAll, syntax.AppAll:
|
|
r.Stdout = f
|
|
r.Stderr = f
|
|
default:
|
|
panic(fmt.Sprintf("unhandled redirect op: %v", rd.Op))
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (r *Runner) loopStmtsBroken(sl syntax.StmtList) bool {
|
|
oldInLoop := r.inLoop
|
|
r.inLoop = true
|
|
defer func() { r.inLoop = oldInLoop }()
|
|
for _, stmt := range sl.Stmts {
|
|
r.stmt(stmt)
|
|
if r.contnEnclosing > 0 {
|
|
r.contnEnclosing--
|
|
return r.contnEnclosing > 0
|
|
}
|
|
if r.breakEnclosing > 0 {
|
|
r.breakEnclosing--
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type returnCode uint8
|
|
|
|
func (returnCode) Error() string { return "returned" }
|
|
|
|
func (r *Runner) call(pos syntax.Pos, args []string) {
|
|
if r.stop() {
|
|
return
|
|
}
|
|
name := args[0]
|
|
if body := r.Funcs[name]; body != nil {
|
|
// stack them to support nested func calls
|
|
oldParams := r.Params
|
|
r.Params = args[1:]
|
|
oldInFunc := r.inFunc
|
|
oldFuncVars := r.funcVars
|
|
r.funcVars = nil
|
|
r.inFunc = true
|
|
|
|
r.stmt(body)
|
|
|
|
r.Params = oldParams
|
|
r.funcVars = oldFuncVars
|
|
r.inFunc = oldInFunc
|
|
if code, ok := r.err.(returnCode); ok {
|
|
r.err = nil
|
|
r.exit = int(code)
|
|
}
|
|
return
|
|
}
|
|
if isBuiltin(name) {
|
|
r.exit = r.builtinCode(pos, name, args[1:])
|
|
return
|
|
}
|
|
r.exec(args)
|
|
}
|
|
|
|
func (r *Runner) exec(args []string) {
|
|
path := r.lookPath(args[0])
|
|
err := r.Exec(r.ctx(), path, args)
|
|
switch x := err.(type) {
|
|
case nil:
|
|
r.exit = 0
|
|
case ExitCode:
|
|
r.exit = int(x)
|
|
default: // module's custom fatal error
|
|
r.setErr(err)
|
|
}
|
|
}
|
|
|
|
func (r *Runner) open(path string, flags int, mode os.FileMode, print bool) (io.ReadWriteCloser, error) {
|
|
f, err := r.Open(r.ctx(), path, flags, mode)
|
|
switch err.(type) {
|
|
case nil:
|
|
case *os.PathError:
|
|
if print {
|
|
r.errf("%v\n", err)
|
|
}
|
|
default: // module's custom fatal error
|
|
r.setErr(err)
|
|
}
|
|
return f, err
|
|
}
|
|
|
|
func (r *Runner) stat(name string) (os.FileInfo, error) {
|
|
return os.Stat(r.relPath(name))
|
|
}
|
|
|
|
func (r *Runner) checkStat(file string) string {
|
|
d, err := r.stat(file)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
m := d.Mode()
|
|
if m.IsDir() {
|
|
return ""
|
|
}
|
|
if runtime.GOOS != "windows" && m&0111 == 0 {
|
|
return ""
|
|
}
|
|
return file
|
|
}
|
|
|
|
func winHasExt(file string) bool {
|
|
i := strings.LastIndex(file, ".")
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
return strings.LastIndexAny(file, `:\/`) < i
|
|
}
|
|
|
|
func (r *Runner) findExecutable(file string, exts []string) string {
|
|
if len(exts) == 0 {
|
|
// non-windows
|
|
return r.checkStat(file)
|
|
}
|
|
if winHasExt(file) && r.checkStat(file) != "" {
|
|
return file
|
|
}
|
|
for _, e := range exts {
|
|
if f := file + e; r.checkStat(f) != "" {
|
|
return f
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitList is like filepath.SplitList, but always using the unix path
|
|
// list separator ':'. On Windows, it also makes sure not to split
|
|
// [A-Z]:[/\].
|
|
func splitList(path string) []string {
|
|
if path == "" {
|
|
return []string{""}
|
|
}
|
|
list := strings.Split(path, ":")
|
|
if runtime.GOOS != "windows" {
|
|
return list
|
|
}
|
|
// join "C", "/foo" into "C:/foo"
|
|
var fixed []string
|
|
for i := 0; i < len(list); i++ {
|
|
s := list[i]
|
|
switch {
|
|
case len(s) != 1, s[0] < 'A', s[0] > 'Z':
|
|
// not a disk name
|
|
case i+1 >= len(list):
|
|
// last element
|
|
case strings.IndexAny(list[i+1], `/\`) != 0:
|
|
// next element doesn't start with / or \
|
|
default:
|
|
fixed = append(fixed, s+":"+list[i+1])
|
|
i++
|
|
continue
|
|
}
|
|
fixed = append(fixed, s)
|
|
}
|
|
return fixed
|
|
}
|
|
|
|
func (r *Runner) lookPath(file string) string {
|
|
pathList := splitList(r.getVar("PATH"))
|
|
chars := `/`
|
|
if runtime.GOOS == "windows" {
|
|
chars = `:\/`
|
|
// so that "foo" always tries "./foo"
|
|
pathList = append([]string{"."}, pathList...)
|
|
}
|
|
exts := r.pathExts()
|
|
if strings.ContainsAny(file, chars) {
|
|
return r.findExecutable(file, exts)
|
|
}
|
|
for _, dir := range pathList {
|
|
var path string
|
|
switch dir {
|
|
case "", ".":
|
|
// otherwise "foo" won't be "./foo"
|
|
path = "." + string(filepath.Separator) + file
|
|
default:
|
|
path = filepath.Join(dir, file)
|
|
}
|
|
if f := r.findExecutable(path, exts); f != "" {
|
|
return f
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *Runner) pathExts() []string {
|
|
if runtime.GOOS != "windows" {
|
|
return nil
|
|
}
|
|
pathext := r.getVar("PATHEXT")
|
|
if pathext == "" {
|
|
return []string{".com", ".exe", ".bat", ".cmd"}
|
|
}
|
|
var exts []string
|
|
for _, e := range strings.Split(strings.ToLower(pathext), `;`) {
|
|
if e == "" {
|
|
continue
|
|
}
|
|
if e[0] != '.' {
|
|
e = "." + e
|
|
}
|
|
exts = append(exts, e)
|
|
}
|
|
return exts
|
|
}
|