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:
Florian WaltherandClaude Opus 5.5 committed 2026-10-04 17:20:59 +02:00
1 parent 9fe8b06fba
commit 443367f66f
9 files changed
+1629 -98

No files matched your search

+358 -21
View File
@@ -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) {