add functions, positional parameters, case and read
- functions: name() { ...; }, function name { ...; }, return, local,
unset -f; in pipes, & and $(...) they run in a subshell that gets all
function definitions and the positional parameters
- positional parameters $0, $1 ... ${10}, $#, "$@", "$*", $$; set by
gosh script args, gosh -c command name args, function calls,
source file args and set --; shift; for x without "in"
- case word in pattern|pattern) ...;; esac and { list; } groups
- read [-r] [-p prompt] [name ...] with $IFS splitting and $REPLY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9fe8b06fba
commit
443367f66f
9 files changed
+1629
-98
No files matched your search
@@ -1,8 +1,8 @@
|
||||
package main
|
||||
|
||||
// Parser and executor for command lines and scripts: lists (; & && || and
|
||||
// newlines), pipelines, ! and the control structures if, for, while and
|
||||
// until. Simple commands (with their pipes and redirections) are kept as
|
||||
// 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.
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -48,7 +50,13 @@ 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}
|
||||
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
|
||||
@@ -57,7 +65,7 @@ type tokenKind int
|
||||
|
||||
const (
|
||||
tWord tokenKind = iota
|
||||
tOp // ; & && || | or newline
|
||||
tOp // ; ;; & && || | or newline
|
||||
)
|
||||
|
||||
type rawToken struct {
|
||||
@@ -126,6 +134,9 @@ func lex(src string) ([]rawToken, error) {
|
||||
switch {
|
||||
case ch == ' ' || ch == '\t':
|
||||
i++
|
||||
case ch == ';' && next == ';':
|
||||
op(";;", i)
|
||||
i += 2
|
||||
case ch == '\n' || ch == ';':
|
||||
op(string(ch), i)
|
||||
i++
|
||||
@@ -259,6 +270,27 @@ type whileNode struct {
|
||||
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 {
|
||||
@@ -356,7 +388,7 @@ func (p *parser) parseList(terms map[string]bool) (*listNode, error) {
|
||||
for {
|
||||
p.skipNewlines()
|
||||
t := p.peek()
|
||||
if t == nil || t.kind == tWord && terms[t.text] {
|
||||
if t == nil || terms[t.text] {
|
||||
return list, nil
|
||||
}
|
||||
ao, err := p.parseAndOr()
|
||||
@@ -371,8 +403,9 @@ func (p *parser) parseList(terms map[string]bool) (*listNode, error) {
|
||||
case t.kind == tOp && (t.text == ";" || t.text == "&" || t.text == "\n"):
|
||||
p.pos++
|
||||
entry.background = t.text == "&"
|
||||
case t.kind == tWord && terms[t.text]:
|
||||
// e.g. "fi" right after the end of a nested control structure
|
||||
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)
|
||||
}
|
||||
@@ -436,14 +469,12 @@ func (p *parser) parsePipeline() (*pipelineNode, error) {
|
||||
|
||||
var err error
|
||||
compound := true
|
||||
switch t.text {
|
||||
case "if":
|
||||
pl.cmd, err = p.parseIf()
|
||||
case "for":
|
||||
pl.cmd, err = p.parseFor()
|
||||
case "while", "until":
|
||||
pl.cmd, err = p.parseWhile()
|
||||
default:
|
||||
start := t.start // funcDefName may split the token
|
||||
if name, ok := p.funcDefName(); ok {
|
||||
pl.cmd, err = p.parseFuncDef(name, start)
|
||||
} else if compoundStarts[t.text] {
|
||||
pl.cmd, err = p.parseCompound()
|
||||
} else {
|
||||
compound = false
|
||||
pl.cmd, err = p.parseSimple()
|
||||
}
|
||||
@@ -486,13 +517,30 @@ func (p *parser) parseSimple() (*simpleNode, error) {
|
||||
return nil, errIncomplete
|
||||
case next.kind == tOp || terminators[next.text]:
|
||||
return nil, p.unexpected(next)
|
||||
case next.text == "if" || next.text == "for" || next.text == "while" || next.text == "until":
|
||||
case compoundStarts[next.text]:
|
||||
return nil, errors.New("control structures in pipelines are not supported")
|
||||
}
|
||||
}
|
||||
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()
|
||||
default: // {
|
||||
return p.parseGroup()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseIf() (*ifNode, error) {
|
||||
p.pos++ // if
|
||||
n := &ifNode{}
|
||||
@@ -604,6 +652,212 @@ func (p *parser) parseWhile() (*whileNode, error) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
// ). Since ( and ) are not operators for the lexer, they can be part of a
|
||||
// word token ("a)", "(a", "*)echo"); such tokens are split here.
|
||||
func (p *parser) parsePatterns() ([]string, error) {
|
||||
var patterns []string
|
||||
if t := p.peek(); t.kind == tWord && strings.HasPrefix(t.text, "(") {
|
||||
// The optional ( before the first pattern
|
||||
if t.text == "(" {
|
||||
p.pos++
|
||||
} else {
|
||||
t.text, t.start = t.text[1:], t.start+1
|
||||
}
|
||||
}
|
||||
for {
|
||||
t := p.peek()
|
||||
if t == nil {
|
||||
return nil, errIncomplete
|
||||
}
|
||||
if t.kind != tWord {
|
||||
return nil, p.unexpected(t)
|
||||
}
|
||||
i := unquotedIndex(t.text, ')')
|
||||
if i < 0 {
|
||||
patterns = append(patterns, t.text)
|
||||
p.pos++
|
||||
next := p.peek()
|
||||
switch {
|
||||
case next == nil:
|
||||
return nil, errIncomplete
|
||||
case next.kind == tOp && next.text == "|":
|
||||
p.pos++
|
||||
case next.kind == tWord && strings.HasPrefix(next.text, ")"):
|
||||
default:
|
||||
return nil, p.unexpected(next)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
patterns = append(patterns, t.text[:i])
|
||||
}
|
||||
if len(patterns) == 0 {
|
||||
return nil, syntaxError(")")
|
||||
}
|
||||
if rest := t.text[i+1:]; rest == "" {
|
||||
p.pos++
|
||||
} else {
|
||||
// The first word of the body follows right after the )
|
||||
t.text, t.start = rest, t.start+i+1
|
||||
}
|
||||
return patterns, nil
|
||||
}
|
||||
}
|
||||
|
||||
// unquotedIndex returns the index of the first c in word that is not quoted,
|
||||
// escaped or inside $(...) or ${...}, or -1.
|
||||
func unquotedIndex(word string, c byte) int {
|
||||
var quote byte
|
||||
for i := 0; i < len(word); i++ {
|
||||
ch := word[i]
|
||||
switch {
|
||||
case quote == '\'':
|
||||
if ch == '\'' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '\\':
|
||||
i++
|
||||
case isSubstStart(word, i):
|
||||
if end := substEnd(word, i); end >= 0 {
|
||||
i = end
|
||||
}
|
||||
case ch == '$' && i+1 < len(word) && word[i+1] == '{':
|
||||
if k := strings.IndexByte(word[i:], '}'); k >= 0 {
|
||||
i += k
|
||||
}
|
||||
case quote == '"':
|
||||
if ch == '"' {
|
||||
quote = 0
|
||||
}
|
||||
case ch == '"' || ch == '\'':
|
||||
quote = ch
|
||||
case ch == c:
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// funcDefName reports whether a function definition starts at the current
|
||||
// token: name(), name () or function name [()]. It consumes the tokens up to
|
||||
// the body; a body that starts in the same token (name(){) becomes a token of
|
||||
// its own.
|
||||
func (p *parser) funcDefName() (string, bool) {
|
||||
start := p.pos
|
||||
t := p.peek()
|
||||
keyword := t.kind == tWord && t.text == "function"
|
||||
if keyword {
|
||||
p.pos++
|
||||
if t = p.peek(); t == nil || t.kind != tWord {
|
||||
p.pos = start
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
var name, rest string
|
||||
if n, r, ok := strings.Cut(t.text, "()"); ok && isFuncName(n) {
|
||||
// name() or name(){
|
||||
name, rest = n, r
|
||||
} else if next := p.pos + 1; t.kind == tWord && isFuncName(t.text) && next < len(p.toks) &&
|
||||
p.toks[next].kind == tWord && strings.HasPrefix(p.toks[next].text, "()") {
|
||||
// name ()
|
||||
name = t.text
|
||||
p.pos = next
|
||||
t = &p.toks[next]
|
||||
rest = t.text[2:]
|
||||
} else if keyword && isFuncName(t.text) {
|
||||
// function name
|
||||
name = t.text
|
||||
} else {
|
||||
p.pos = start
|
||||
return "", false
|
||||
}
|
||||
if rest == "" {
|
||||
p.pos++
|
||||
} else {
|
||||
t.text, t.start = rest, t.end-len(rest)
|
||||
}
|
||||
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 t.kind != tWord || !compoundStarts[t.text] {
|
||||
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 {
|
||||
@@ -623,11 +877,14 @@ func (p *parser) parseLoopBody() (*listNode, error) {
|
||||
// Execution
|
||||
|
||||
// isAbort reports whether err stops the execution of the enclosing lists:
|
||||
// break, continue, or a command that was interrupted with Ctrl+C (like in
|
||||
// bash, Ctrl+C aborts the whole command line, e.g. a loop).
|
||||
// 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
|
||||
return errors.As(err, &lc) || exitStatus(err) == 128+int(syscall.SIGINT)
|
||||
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
|
||||
@@ -779,7 +1036,8 @@ func (n *whileNode) exec() error {
|
||||
}
|
||||
|
||||
func (n *forNode) exec() error {
|
||||
var words []string
|
||||
// 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 {
|
||||
@@ -804,6 +1062,85 @@ func (n *forNode) exec() error {
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *groupNode) exec() error {
|
||||
return n.body.exec()
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
Reference in new issue
Block a user