Files
Florian WaltherandClaude Opus 5.5 1f507f6562 add parameter expansion, arithmetic, set -e/-x/pipefail, trap, eval, exec
- ${#v}, ${v#p} ${v##p} ${v%p} ${v%%p}, ${v/p/r} ${v//p/r} ${v/#p/r}
  ${v/%p/r}, ${v:off:len}, ${v-w} ${v:=w} ${v:+w} ${v:?msg}; nested ${}
- arithmetic expansion $((...)) with the C operators, ** and assignments
- set -e, set -x, set -o pipefail (also as gosh options, passed on to
  subshells); set shows variables, set -o the options
- trap for EXIT and signals; scripts with an EXIT trap run it on
  SIGINT/SIGTERM/SIGHUP too
- eval, exec (also exec > file without a command), $!, wait pid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:15:24 +02:00

1366 lines
31 KiB
Go

package main
// Parser and executor for command lines and scripts: lists (; & && || and
// newlines), pipelines, ! and the compound commands if, for, while, until,
// case, { ...; } and ( ... ), and function definitions. Simple commands (with their pipes and redirections) are kept as
// text; they are expanded and run by execCommand only when they are
// executed, so that e.g. the variable of a for loop is expanded anew in
// every iteration.
import (
"errors"
"fmt"
"os"
"slices"
"strconv"
"strings"
"syscall"
)
// incompleteError is returned by parse if the input ends inside a
// construct (if without fi, an open quote, a trailing && ...): an
// interactive shell then reads another line.
type incompleteError struct{}
func (incompleteError) Error() string { return "syntax error: unexpected end of input" }
var errIncomplete error = incompleteError{}
// loopControl is returned by break and continue and ends the given number of
// enclosing loops (break) or iterations (continue).
type loopControl struct {
isBreak bool
n int
}
func (c loopControl) Error() string {
if c.isBreak {
return "break"
}
return "continue"
}
// loopDepth is the number of loops that are currently running.
var loopDepth int
// aliasExpansion is false while the result of an alias expansion runs, so
// that it is not expanded again.
var aliasExpansion = true
// reserved words that end a list; they are only recognized as the first word
// of a command.
var terminators = map[string]bool{
"then": true, "elif": true, "else": true, "fi": true, "do": true, "done": true,
"esac": true, "}": true,
}
// compoundStarts are the reserved words that start a compound command.
var compoundStarts = map[string]bool{"if": true, "for": true, "while": true, "until": true, "case": true, "{": true}
// ---------------------------------------------------------------------------
// Lexer
type tokenKind int
const (
tWord tokenKind = iota
tOp // ; ;; & && || | ( ) or newline
)
type rawToken struct {
kind tokenKind
text string
start, end int
}
// joinContinuationLines removes backslash-newline (outside single quotes),
// which continues a line. continued reports whether src ends with such a
// continuation (so that the command goes on in the next line).
func joinContinuationLines(src string) (joined string, continued bool) {
if !strings.Contains(src, "\\\n") {
return src, false
}
var b strings.Builder
var quote byte
for i := 0; i < len(src); i++ {
ch := src[i]
switch {
case quote == '\'':
if ch == '\'' {
quote = 0
}
case ch == '\\' && i+1 < len(src):
if src[i+1] == '\n' {
i++
continued = i == len(src)-1
continue
}
continued = false
b.WriteByte(ch)
i++
ch = src[i]
case ch == '"':
if quote == '"' {
quote = 0
} else {
quote = '"'
}
case ch == '\'' && quote == 0:
quote = '\''
}
if ch != '\n' && ch != ' ' && ch != '\t' {
continued = false
}
b.WriteByte(ch)
}
return b.String(), continued
}
// lex splits src into words and list operators. Words keep their quotes,
// escapes, $(...) and redirections; they are only split at blanks and list
// operators.
func lex(src string) ([]rawToken, error) {
var toks []rawToken
op := func(text string, i int) {
toks = append(toks, rawToken{tOp, text, i, i + len(text)})
}
for i := 0; i < len(src); {
ch := src[i]
next := byte(0)
if i+1 < len(src) {
next = src[i+1]
}
switch {
case ch == ' ' || ch == '\t':
i++
case ch == ';' && next == ';':
op(";;", i)
i += 2
case ch == '\n' || ch == ';':
op(string(ch), i)
i++
case ch == '#':
// Comment until the end of the line
for i < len(src) && src[i] != '\n' {
i++
}
case ch == '|' && next == '|', ch == '&' && next == '&':
op(src[i:i+2], i)
i += 2
case ch == '|', ch == '&' && next != '>', ch == '(', ch == ')':
op(string(ch), i)
i++
default:
end, err := lexWord(src, i)
if err != nil {
return nil, err
}
toks = append(toks, rawToken{tWord, src[i:end], i, end})
i = end
}
}
return toks, nil
}
// lexWord returns the end of the word that starts at src[i].
func lexWord(src string, i int) (int, error) {
var quote byte
for j := i; j < len(src); j++ {
ch := src[j]
switch {
case quote == '\'':
if ch == '\'' {
quote = 0
}
case ch == '\\':
if j+1 == len(src) {
return 0, errIncomplete
}
j++
if isSubstStart(src, j) {
// \$(...) is literal text, but its ( and ) belong to the word
if end := substEnd(src, j); end >= 0 {
j = end
}
}
case isSubstStart(src, j):
end := substEnd(src, j)
if end < 0 {
return 0, errIncomplete
}
j = end
case ch == '$' && j+1 < len(src) && src[j+1] == '{':
if end := paramEnd(src, j); end >= 0 {
j = end
}
case quote == '"':
if ch == '"' {
quote = 0
}
case ch == '"' || ch == '\'':
quote = ch
case ch == ' ' || ch == '\t' || ch == '\n' || ch == ';' || ch == '|' || ch == '(' || ch == ')':
return j, nil
case ch == '&':
// &>, >& and <& are redirections, not the & operator
if !(j+1 < len(src) && src[j+1] == '>') && !(j > i && (src[j-1] == '>' || src[j-1] == '<')) {
return j, nil
}
}
}
if quote != 0 {
return 0, errIncomplete
}
return len(src), nil
}
// ---------------------------------------------------------------------------
// Syntax tree
// node is a command that can be executed: it returns the error of the last
// command it ran (so that exitStatus gives its status).
type node interface {
exec() error
}
// listNode is a list of and-or lists separated by ;, & or newlines.
type listNode struct {
items []listEntry
}
type listEntry struct {
andOr *andOrNode
background bool
}
// andOrNode is a pipeline followed by && and || pipelines.
type andOrNode struct {
first *pipelineNode
rest []andOrPart
text string // the source, to run it in a subshell in the background
}
type andOrPart struct {
op string // && or ||
pipeline *pipelineNode
}
// pipelineNode is a pipeline (a simpleNode or pipeNode) or a single
// compound command, optionally negated with !.
type pipelineNode struct {
negate bool
cmd node
}
// simpleNode is the text of a pipeline of simple commands.
type simpleNode struct {
text string
}
// pipeNode is a pipeline that contains compound commands. Like all commands
// of a pipeline in sh, they run in subshells (new gosh processes), since
// the commands of a pipeline run concurrently.
type pipeNode struct {
elems []pipeElem
text string
}
type pipeElem struct {
text string // the source of the command, with its redirections
compound bool
subshell string // for ( list ): the list, which runs in the subshell directly
}
// redirNode is a compound command with redirections; they apply to all
// commands in it.
type redirNode struct {
cmd node
redirs string // the source text of the redirections
}
// subshellNode is ( list ): the list runs in a new gosh process, so that it
// cannot change the shell.
type subshellNode struct {
text string // the source of the list
}
type ifNode struct {
conds []*listNode
bodies []*listNode
elseBody *listNode
}
type forNode struct {
name string
words string // the words after "in", expanded when the loop starts
hasIn bool
body *listNode
}
type whileNode struct {
until bool
cond *listNode
body *listNode
}
// caseNode is case word in pattern|pattern) list ;; ... esac.
type caseNode struct {
word string // expanded when the case command runs
clauses []caseClause
}
type caseClause struct {
patterns []string // expanded when they are tried
body *listNode
}
// groupNode is { list; }.
type groupNode struct {
body *listNode
}
// funcDefNode is a function definition name() compound-command.
type funcDefNode struct {
def *funcDef
}
// singleSimple returns the simple command if the list is just that.
func (l *listNode) singleSimple() *simpleNode {
if len(l.items) != 1 || l.items[0].background {
return nil
}
ao := l.items[0].andOr
if len(ao.rest) > 0 || ao.first.negate {
return nil
}
s, _ := ao.first.cmd.(*simpleNode)
return s
}
// ---------------------------------------------------------------------------
// Parser
type parser struct {
src string
toks []rawToken
pos int
}
// parse parses a command line or script. It returns errIncomplete if the
// input ends inside a construct.
func parse(src string) (*listNode, error) {
src, err := extractHeredocs(src)
if err != nil {
return nil, err
}
src, continued := joinContinuationLines(src)
if continued {
return nil, errIncomplete
}
toks, err := lex(src)
if err != nil {
return nil, err
}
p := &parser{src: src, toks: toks}
list, err := p.parseList(nil)
if err != nil {
return nil, err
}
if t := p.peek(); t != nil {
return nil, p.unexpected(t)
}
return list, nil
}
func (p *parser) peek() *rawToken {
if p.pos < len(p.toks) {
return &p.toks[p.pos]
}
return nil
}
func (p *parser) peekWord(words ...string) bool {
t := p.peek()
if t == nil || t.kind != tWord {
return false
}
for _, w := range words {
if t.text == w {
return true
}
}
return false
}
func (p *parser) skipNewlines() {
for t := p.peek(); t != nil && t.kind == tOp && t.text == "\n"; t = p.peek() {
p.pos++
}
}
func (p *parser) unexpected(t *rawToken) error {
if t.text == "\n" {
return syntaxError("newline")
}
return syntaxError(t.text)
}
// expect consumes the reserved word w.
func (p *parser) expect(w string) error {
t := p.peek()
if t == nil {
return errIncomplete
}
if t.kind != tWord || t.text != w {
return p.unexpected(t)
}
p.pos++
return nil
}
// parseList parses and-or lists until the end of the input or one of the
// reserved words in terms at the start of a command.
func (p *parser) parseList(terms map[string]bool) (*listNode, error) {
list := &listNode{}
for {
p.skipNewlines()
t := p.peek()
if t == nil || terms[t.text] {
return list, nil
}
ao, err := p.parseAndOr()
if err != nil {
return nil, err
}
entry := listEntry{andOr: ao}
t = p.peek()
switch {
case t == nil:
case t.kind == tOp && (t.text == ";" || t.text == "&" || t.text == "\n"):
p.pos++
entry.background = t.text == "&"
case terms[t.text]:
// e.g. "fi" right after the end of a nested control structure,
// or ;; after a command in a case clause
default:
return nil, p.unexpected(t)
}
list.items = append(list.items, entry)
}
}
// nonEmpty returns an error if a list that must contain commands is empty.
func (p *parser) nonEmpty(list *listNode) error {
if len(list.items) > 0 {
return nil
}
if t := p.peek(); t != nil {
return p.unexpected(t)
}
return errIncomplete
}
func (p *parser) parseAndOr() (*andOrNode, error) {
start := p.peek().start
first, err := p.parsePipeline()
if err != nil {
return nil, err
}
ao := &andOrNode{first: first}
for {
t := p.peek()
if t == nil || t.kind != tOp || (t.text != "&&" && t.text != "||") {
ao.text = p.src[start:p.toks[p.pos-1].end]
return ao, nil
}
p.pos++
p.skipNewlines()
if p.peek() == nil {
return nil, errIncomplete
}
next, err := p.parsePipeline()
if err != nil {
return nil, err
}
ao.rest = append(ao.rest, andOrPart{op: t.text, pipeline: next})
}
}
// parsePipeline parses [!] command [| command]... A pipeline of simple
// commands stays one simpleNode (its text is split into the commands when it
// runs); a pipeline with compound commands becomes a pipeNode.
func (p *parser) parsePipeline() (*pipelineNode, error) {
pl := &pipelineNode{}
if p.peekWord("!") {
pl.negate = true
p.pos++
}
var (
elems []pipeElem
cmds []node
start = -1
)
for {
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind == tOp && t.text != "(" || t.kind == tWord && terminators[t.text] {
return nil, p.unexpected(t)
}
if start < 0 {
start = t.start
}
cmd, compound, err := p.parseCommand()
if err != nil {
return nil, err
}
elem := pipeElem{text: p.src[t.start:p.toks[p.pos-1].end], compound: compound}
if sub, ok := cmd.(*subshellNode); ok {
elem.subshell = sub.text
}
elems = append(elems, elem)
cmds = append(cmds, cmd)
if t := p.peek(); t == nil || t.kind != tOp || t.text != "|" {
break
}
p.pos++
p.skipNewlines()
if p.peek() == nil {
return nil, errIncomplete
}
}
text := p.src[start:p.toks[p.pos-1].end]
compound := false
for i, e := range elems {
if _, ok := cmds[i].(*funcDefNode); ok && len(elems) > 1 {
return nil, errors.New("function definitions in pipelines are not supported")
}
compound = compound || e.compound
}
switch {
case len(elems) == 1:
pl.cmd = cmds[0]
case !compound:
pl.cmd = &simpleNode{text: text}
default:
pl.cmd = &pipeNode{elems: elems, text: text}
}
return pl, nil
}
// parseCommand parses a simple command, a compound command with its
// redirections or a function definition, and reports whether it is not a
// simple command.
func (p *parser) parseCommand() (node, bool, error) {
t := p.peek()
start := t.start
if name, ok := p.funcDefName(); ok {
def, err := p.parseFuncDef(name, start)
if err != nil {
return nil, true, err
}
if t := p.peek(); t != nil && t.kind == tWord && !terminators[t.text] {
return nil, true, errors.New("redirections of function definitions are not supported")
}
return def, true, nil
}
if !isCompoundStart(t) {
cmd, err := p.parseSimple()
return cmd, false, err
}
cmd, err := p.parseCompound()
if err != nil {
return nil, true, err
}
redirs, err := p.parseRedirections()
if err != nil {
return nil, true, err
}
if redirs != "" {
cmd = &redirNode{cmd: cmd, redirs: redirs}
}
return cmd, true, nil
}
// isCompoundStart reports whether a compound command starts at t.
func isCompoundStart(t *rawToken) bool {
return t.kind == tWord && compoundStarts[t.text] || t.kind == tOp && t.text == "("
}
// parseRedirections parses the redirections after a compound command and
// returns their source text (expanded when the command runs).
func (p *parser) parseRedirections() (string, error) {
start, end := -1, -1
for t := p.peek(); t != nil && t.kind == tWord && !terminators[t.text]; t = p.peek() {
needsTarget, ok := redirectionWord(t.text)
if !ok {
return "", syntaxError(t.text)
}
if start < 0 {
start = t.start
}
end = t.end
p.pos++
if needsTarget {
t = p.peek()
if t == nil || t.kind != tWord {
if t == nil {
return "", syntaxError("newline")
}
return "", p.unexpected(t)
}
end = t.end
p.pos++
}
}
if start < 0 {
return "", nil
}
return p.src[start:end], nil
}
// redirectionWord reports whether word starts with a redirection operator
// (optionally after a file descriptor), and whether the target is the next
// word (2> file instead of 2>file).
func redirectionWord(word string) (needsTarget, ok bool) {
w := strings.TrimLeft(word, "0123456789")
for _, op := range []string{"&>>", "&>", "<<<", ">>", ">&", "<&", ">", "<"} {
if strings.HasPrefix(w, op) {
if op[0] == '&' && len(w) != len(word) {
return false, false
}
return w == op, true
}
}
return false, false
}
// parseSimple parses a simple command (without pipes) and returns its text.
func (p *parser) parseSimple() (*simpleNode, error) {
start := p.toks[p.pos].start
end := start
for t := p.peek(); t != nil && t.kind == tWord; t = p.peek() {
end = t.end
p.pos++
}
return &simpleNode{text: p.src[start:end]}, nil
}
// parseCompound parses the compound command that starts at the current
// token.
func (p *parser) parseCompound() (node, error) {
switch p.peek().text {
case "if":
return p.parseIf()
case "for":
return p.parseFor()
case "while", "until":
return p.parseWhile()
case "case":
return p.parseCase()
case "(":
return p.parseSubshell()
default: // {
return p.parseGroup()
}
}
func (p *parser) parseIf() (*ifNode, error) {
p.pos++ // if
n := &ifNode{}
for {
cond, err := p.parseList(map[string]bool{"then": true})
if err == nil {
err = p.nonEmpty(cond)
}
if err == nil {
err = p.expect("then")
}
if err != nil {
return nil, err
}
body, err := p.parseList(map[string]bool{"elif": true, "else": true, "fi": true})
if err == nil {
err = p.nonEmpty(body)
}
if err != nil {
return nil, err
}
n.conds = append(n.conds, cond)
n.bodies = append(n.bodies, body)
t := p.peek()
if t == nil {
return nil, errIncomplete
}
p.pos++
switch t.text {
case "elif":
continue
case "else":
body, err := p.parseList(map[string]bool{"fi": true})
if err == nil {
err = p.nonEmpty(body)
}
if err == nil {
err = p.expect("fi")
}
if err != nil {
return nil, err
}
n.elseBody = body
}
return n, nil // fi
}
}
func (p *parser) parseFor() (*forNode, error) {
p.pos++ // for
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind != tWord || !isVarName(t.text) {
return nil, fmt.Errorf("for: `%s': not a valid identifier", sanitize(t.text))
}
n := &forNode{name: t.text}
p.pos++
p.skipNewlines()
if p.peekWord("in") {
p.pos++
n.hasIn = true
var words []string
for t := p.peek(); t != nil && t.kind == tWord; t = p.peek() {
words = append(words, t.text)
p.pos++
}
n.words = strings.Join(words, " ")
}
t = p.peek()
switch {
case t == nil:
return nil, errIncomplete
case t.kind == tOp && (t.text == ";" || t.text == "\n"):
p.pos++
case !n.hasIn && t.kind == tWord && t.text == "do":
default:
return nil, p.unexpected(t)
}
p.skipNewlines()
body, err := p.parseLoopBody()
if err != nil {
return nil, err
}
n.body = body
return n, nil
}
func (p *parser) parseWhile() (*whileNode, error) {
n := &whileNode{until: p.peek().text == "until"}
p.pos++
cond, err := p.parseList(map[string]bool{"do": true})
if err == nil {
err = p.nonEmpty(cond)
}
if err != nil {
return nil, err
}
n.cond = cond
body, err := p.parseLoopBody()
if err != nil {
return nil, err
}
n.body = body
return n, nil
}
// parseGroup parses { list; }.
func (p *parser) parseGroup() (*groupNode, error) {
p.pos++ // {
body, err := p.parseList(map[string]bool{"}": true})
if err == nil {
err = p.nonEmpty(body)
}
if err == nil {
err = p.expect("}")
}
if err != nil {
return nil, err
}
return &groupNode{body: body}, nil
}
// parseSubshell parses ( list ).
func (p *parser) parseSubshell() (*subshellNode, error) {
p.pos++ // (
start := p.toks[p.pos-1].end
body, err := p.parseList(map[string]bool{")": true})
if err == nil {
err = p.nonEmpty(body)
}
if err != nil {
return nil, err
}
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind != tOp || t.text != ")" {
return nil, p.unexpected(t)
}
p.pos++
return &subshellNode{text: p.src[start:t.start]}, nil
}
// parseCase parses case word in [(]pattern[|pattern]...) list ;; ... esac.
// The last clause may omit the ;;.
func (p *parser) parseCase() (*caseNode, error) {
p.pos++ // case
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind != tWord {
return nil, p.unexpected(t)
}
n := &caseNode{word: t.text}
p.pos++
p.skipNewlines()
if err := p.expect("in"); err != nil {
return nil, err
}
for {
p.skipNewlines()
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind == tWord && t.text == "esac" {
p.pos++
return n, nil
}
patterns, err := p.parsePatterns()
if err != nil {
return nil, err
}
body, err := p.parseList(map[string]bool{";;": true, "esac": true})
if err != nil {
return nil, err
}
n.clauses = append(n.clauses, caseClause{patterns: patterns, body: body})
if t := p.peek(); t == nil {
return nil, errIncomplete
} else if t.text == ";;" {
p.pos++
}
}
}
// parsePatterns parses the patterns of a case clause up to and including the
// ).
func (p *parser) parsePatterns() ([]string, error) {
if t := p.peek(); t.kind == tOp && t.text == "(" {
p.pos++ // the optional ( before the first pattern
}
var patterns []string
for {
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if t.kind != tWord {
return nil, p.unexpected(t)
}
patterns = append(patterns, t.text)
p.pos++
t = p.peek()
switch {
case t == nil:
return nil, errIncomplete
case t.kind == tOp && t.text == "|":
p.pos++
case t.kind == tOp && t.text == ")":
p.pos++
return patterns, nil
default:
return nil, p.unexpected(t)
}
}
}
// funcDefName reports whether a function definition starts at the current
// token: name(), or function name [()]. It consumes the tokens up to the
// body.
func (p *parser) funcDefName() (string, bool) {
start := p.pos
if t := p.peek(); t.kind != tWord {
return "", false
} else if t.text == "function" {
p.pos++
if t := p.peek(); t == nil || t.kind != tWord || !isFuncName(t.text) {
p.pos = start
return "", false
}
}
name := p.peek().text
isOp := func(i int, text string) bool {
return i < len(p.toks) && p.toks[i].kind == tOp && p.toks[i].text == text
}
switch {
case isOp(p.pos+1, "(") && isOp(p.pos+2, ")") && isFuncName(name):
p.pos += 3
case p.pos > start:
p.pos++ // function name
default:
return "", false
}
return name, true
}
// parseFuncDef parses the body of a function definition that started at
// src[start]: a compound command.
func (p *parser) parseFuncDef(name string, start int) (*funcDefNode, error) {
p.skipNewlines()
t := p.peek()
if t == nil {
return nil, errIncomplete
}
if !isCompoundStart(t) {
return nil, fmt.Errorf("%s: the body of a function must be a compound command like { ...; }", sanitize(name))
}
body, err := p.parseCompound()
if err != nil {
return nil, err
}
return &funcDefNode{def: &funcDef{name: name, body: body, text: p.src[start:p.toks[p.pos-1].end]}}, nil
}
// parseLoopBody parses "do list done".
func (p *parser) parseLoopBody() (*listNode, error) {
if err := p.expect("do"); err != nil {
return nil, err
}
body, err := p.parseList(map[string]bool{"done": true})
if err == nil {
err = p.nonEmpty(body)
}
if err == nil {
err = p.expect("done")
}
return body, err
}
// ---------------------------------------------------------------------------
// Execution
// isAbort reports whether err stops the execution of the enclosing lists:
// break, continue, return, or a command that was interrupted with Ctrl+C
// (like in bash, Ctrl+C aborts the whole command line, e.g. a loop).
func isAbort(err error) bool {
var (
lc loopControl
ret returnSignal
)
return errors.As(err, &lc) || errors.As(err, &ret) || exitStatus(err) == 128+int(syscall.SIGINT)
}
// silent returns an error with only the exit status of err, for errors that
// were already reported.
func silent(err error) error {
if err == nil {
return nil
}
return statusError(exitStatus(err))
}
func (l *listNode) exec() error {
var err error
for _, entry := range l.items {
printError(err)
if entry.background {
err = entry.andOr.runBackground()
} else {
err = entry.andOr.exec()
}
lastStatus = exitStatus(err)
runPendingTraps()
if isAbort(err) {
return err
}
}
return err
}
// exec runs the and-or list. With set -e, only a failure of the last
// pipeline ends the shell.
func (a *andOrNode) exec() error {
last := a.first
if len(a.rest) > 0 {
errexitOff++
}
err := a.first.exec()
if len(a.rest) > 0 {
errexitOff--
}
for i, part := range a.rest {
lastStatus = exitStatus(err)
if isAbort(err) {
return err
}
if (part.op == "&&") != (lastStatus == 0) {
continue
}
printError(err)
isLast := i == len(a.rest)-1
if !isLast {
errexitOff++
}
err = part.pipeline.exec()
if !isLast {
errexitOff--
}
last = part.pipeline
}
if last == a.lastPipeline() {
checkErrexit(last, err)
}
return err
}
func (a *andOrNode) lastPipeline() *pipelineNode {
if len(a.rest) == 0 {
return a.first
}
return a.rest[len(a.rest)-1].pipeline
}
// runBackground starts the and-or list as a background job. Pipelines run
// like in the foreground, everything else in a subshell.
func (a *andOrNode) runBackground() error {
if len(a.rest) == 0 && !a.first.negate {
switch cmd := a.first.cmd.(type) {
case *simpleNode:
return cmd.run(true)
case *pipeNode:
return cmd.run(true)
case *subshellNode:
return runPipeline([]stage{{script: cmd.text}}, a.text, true)
}
}
return runPipeline([]stage{{script: a.text}}, a.text, true)
}
func (pl *pipelineNode) exec() error {
if pl.negate {
errexitOff++
defer func() { errexitOff-- }()
}
err := pl.cmd.exec()
if !pl.negate || isAbort(err) {
return err
}
printError(err)
if exitStatus(err) == 0 {
return statusError(1)
}
return nil
}
func (s *simpleNode) exec() error {
return s.run(false)
}
// run executes the simple command, in the background if background is set.
// Aliases are expanded here (in interactive shells), so that they work at
// the start of every command, also inside control structures.
func (s *simpleNode) run(background bool) error {
text := s.text
if interactiveShell && aliasExpansion {
if expanded := expandAliases(text); expanded != text {
tree, err := parse(expanded)
if err != nil {
return err
}
if simple := tree.singleSimple(); simple != nil {
text = simple.text
} else {
if background {
return errors.New("an alias with a command list cannot run in the background")
}
aliasExpansion = false
defer func() { aliasExpansion = true }()
return tree.exec()
}
}
}
return execCommand(text, background)
}
func (n *ifNode) exec() error {
for i, cond := range n.conds {
errexitOff++
err := cond.exec()
errexitOff--
if isAbort(err) {
return err
}
printError(err)
if exitStatus(err) == 0 {
return n.bodies[i].exec()
}
}
if n.elseBody != nil {
return n.elseBody.exec()
}
lastStatus = 0
return nil
}
// runLoopBody runs one iteration of a loop. It returns whether the loop
// must stop, and the error to return from the loop.
func runLoopBody(body *listNode, result *error) (stop bool, err error) {
err = body.exec()
var lc loopControl
if errors.As(err, &lc) {
*result = nil
if lc.n > 1 {
lc.n--
return true, lc // ends an outer loop too
}
return lc.isBreak, nil
}
if isAbort(err) {
return true, err
}
printError(err)
*result = silent(err)
return false, nil
}
func (n *whileNode) exec() error {
loopDepth++
defer func() { loopDepth-- }()
var result error
for {
errexitOff++
err := n.cond.exec()
errexitOff--
if isAbort(err) {
return err
}
printError(err)
if (exitStatus(err) == 0) == n.until {
break
}
if stop, err := runLoopBody(n.body, &result); stop {
if err != nil {
return err
}
break
}
}
lastStatus = exitStatus(result)
return result
}
func (n *forNode) exec() error {
// Without "in", the loop runs over the positional parameters
words := slices.Clone(positional)
if n.hasIn {
var err error
if words, err = expandWordList(n.words); err != nil {
return err
}
}
loopDepth++
defer func() { loopDepth-- }()
var result error
for _, word := range words {
os.Setenv(n.name, word)
if stop, err := runLoopBody(n.body, &result); stop {
if err != nil {
return err
}
break
}
}
lastStatus = exitStatus(result)
return result
}
func (n *groupNode) exec() error {
return n.body.exec()
}
func (n *subshellNode) exec() error {
return runPipeline([]stage{{script: n.text}}, "("+strings.TrimSpace(n.text)+")", false)
}
// exec runs the compound command with stdin, stdout and stderr of the shell
// redirected.
func (n *redirNode) exec() error {
fds, opened, err := openRedirections(n.redirs)
if err != nil {
return err
}
saved := ioOverride
ioOverride = fds
defer func() {
ioOverride = saved
for _, f := range opened {
f.Close()
}
}()
return n.cmd.exec()
}
func (n *pipeNode) exec() error {
return n.run(false)
}
// run builds the stages of the pipeline: simple commands are expanded,
// compound commands run in subshells.
func (n *pipeNode) run(background bool) error {
substStatus = 0
var stages []stage
for _, e := range n.elems {
switch {
case e.subshell != "":
stages = append(stages, stage{script: e.subshell})
case e.compound:
stages = append(stages, stage{script: e.text})
default:
text := e.text
if interactiveShell && aliasExpansion {
if tree, err := parse(expandAliases(text)); err == nil {
if simple := tree.singleSimple(); simple != nil {
text = simple.text
}
}
}
st, err := expandStages(text)
if err != nil {
return err
}
if len(st) == 0 {
st = []stage{{}}
}
stages = append(stages, st...)
}
}
return runPipeline(stages, n.text, background)
}
func (n *funcDefNode) exec() error {
defineFunction(n.def)
return nil
}
// exec runs the body of the first clause with a pattern that matches the
// word. The status is 0 if no pattern matches.
func (n *caseNode) exec() error {
word, err := expandCaseWord(n.word)
if err != nil {
return err
}
for _, clause := range n.clauses {
for _, pattern := range clause.patterns {
ok, err := caseMatch(pattern, word)
if err != nil {
return err
}
if ok {
return clause.body.exec()
}
}
}
lastStatus = 0
return nil
}
// expandCaseWord expands the word of a case command: like an argument, but
// without brace expansion, wildcards and splitting.
func expandCaseWord(text string) (string, error) {
tokens, err := tokenize(text)
if err != nil {
return "", fmt.Errorf("invalid input: %w", err)
}
var words []string
for _, t := range tokens {
if t.op != "" {
return "", syntaxError(t.op)
}
words = append(words, t.word)
}
return strings.Join(words, " "), nil
}
// caseMatch reports whether word matches the pattern of a case clause. The
// pattern is expanded first; quoted characters in it match literally. Unlike
// for file names, * and ? also match a /.
func caseMatch(pattern, word string) (bool, error) {
tokens, err := tokenize(pattern)
if err != nil {
return false, fmt.Errorf("invalid input: %w", err)
}
var literal, glob []string
isGlob := false
for _, t := range tokens {
if t.op != "" {
return false, syntaxError(t.op)
}
literal = append(literal, t.word)
glob = append(glob, t.pattern)
isGlob = isGlob || t.glob
}
if !isGlob {
return word == strings.Join(literal, " "), nil
}
return globMatch(strings.Join(glob, " "), word), nil
}
// expandWordList expands the words of a for loop like the arguments of a
// command (braces, ~, variables, $(...), wildcards).
func expandWordList(text string) ([]string, error) {
text, err := expandBraceWords(text)
if err != nil {
return nil, err
}
tokens, err := tokenize(text)
if err != nil {
return nil, fmt.Errorf("invalid input: %w", err)
}
var words []string
for _, t := range tokens {
if t.op != "" {
return nil, syntaxError(t.op)
}
words = append(words, expandWord(t)...)
}
return words, nil
}
// builtinLoopControl implements break [n] and continue [n].
func builtinLoopControl(args []string) error {
n := 1
if len(args) > 1 {
var err error
if n, err = strconv.Atoi(args[1]); err != nil || n < 1 {
return fmt.Errorf("%s: %s: loop count out of range", args[0], sanitize(args[1]))
}
}
if loopDepth == 0 {
return fmt.Errorf("%s: only meaningful in a loop", args[0])
}
return loopControl{isBreak: args[0] == "break", n: min(n, loopDepth)}
}