- control structures can have redirections (while read l; do ...; done
< file), which apply to all commands in them; they run in the shell
- in pipes and in the background they run in a subshell (cmd | while
read l; do ...; done, for ...; done &, a && b &)
- ( list ) subshells; ( and ) are operators now
- here-documents <<EOF, <<-EOF, <<'EOF' and here-strings <<<word
- redirections of source apply to the commands of the file
- fix non-ASCII characters in quotes being mangled ('für')
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1338 lines
30 KiB
Go
1338 lines
30 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"
|
|
"path"
|
|
"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 k := strings.IndexByte(src[j:], '}'); k >= 0 {
|
|
j += k
|
|
}
|
|
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)
|
|
if isAbort(err) {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (a *andOrNode) exec() error {
|
|
err := a.first.exec()
|
|
for _, part := range a.rest {
|
|
lastStatus = exitStatus(err)
|
|
if isAbort(err) {
|
|
return err
|
|
}
|
|
if (part.op == "&&") != (lastStatus == 0) {
|
|
continue
|
|
}
|
|
printError(err)
|
|
err = part.pipeline.exec()
|
|
}
|
|
return err
|
|
}
|
|
|
|
// 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 {
|
|
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 {
|
|
err := cond.exec()
|
|
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 {
|
|
err := n.cond.exec()
|
|
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
|
|
}
|
|
// path.Match does not let * match a /; NUL can be in neither string
|
|
toNUL := strings.NewReplacer("/", "\x00")
|
|
ok, err := path.Match(toNUL.Replace(strings.Join(glob, " ")), toNUL.Replace(word))
|
|
if err != nil {
|
|
// An invalid pattern only matches itself
|
|
return word == strings.Join(literal, " "), nil
|
|
}
|
|
return ok, 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)}
|
|
}
|