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

+231
View File
@@ -0,0 +1,231 @@
package main
// Functions and positional parameters. A function runs in the shell itself
// (it can change variables and the directory), with its arguments as the
// positional parameters $1, $2, ... In a pipeline, in the background and in
// $(...) it runs in a subshell, which gets the definitions of all functions.
import (
"errors"
"fmt"
"os"
"slices"
"sort"
"strconv"
"strings"
)
// funcDef is a function defined with name() { ...; }.
type funcDef struct {
name string
body node
text string // the source of the definition, passed to subshells
}
var (
functions = map[string]*funcDef{}
// scriptName is $0: the script, the name given after gosh -c command,
// or the name gosh was started with.
scriptName = "gosh"
// positional are the positional parameters $1, $2, ...
positional []string
// funcDepth is the number of functions that are currently running.
funcDepth int
// localFrames holds, for every running function, the previous values
// of the variables it declared with local.
localFrames []map[string]savedVar
// ioOverride replaces stdin, stdout and stderr of the shell while a
// function with redirections runs (f > file), so that the commands in
// it use the redirected files.
ioOverride [3]*os.File
)
// maxFuncDepth limits the nesting of function calls, so that endless
// recursion ends with an error.
const maxFuncDepth = 1000
type savedVar struct {
value string
ok bool
}
// returnSignal is returned by the return builtin and ends the running
// function or sourced file with the given status.
type returnSignal struct {
status int
}
func (r returnSignal) Error() string { return "return" }
// stdio returns stdin (0), stdout (1) or stderr (2) of the shell, which are
// redirected while a function with redirections runs.
func stdio(fd int) *os.File {
if f := ioOverride[fd]; f != nil {
return f
}
return [3]*os.File{os.Stdin, os.Stdout, os.Stderr}[fd]
}
// isFuncName reports whether name can be the name of a function: letters,
// digits, _, - and ., not starting with a digit or -.
func isFuncName(name string) bool {
if name == "" || name[0] == '-' || name[0] >= '0' && name[0] <= '9' || terminators[name] || compoundStarts[name] {
return false
}
for i := 0; i < len(name); i++ {
if c := name[i]; !isVarNameByte(c, false) && c != '-' && c != '.' {
return false
}
}
return true
}
func defineFunction(def *funcDef) {
functions[def.name] = def
}
// functionNames returns the names of all functions, sorted.
func functionNames() []string {
names := make([]string, 0, len(functions))
for name := range functions {
names = append(names, name)
}
sort.Strings(names)
return names
}
// functionDefs returns the definitions of all functions as source text, to
// run them in a subshell.
func functionDefs() string {
var b strings.Builder
for _, name := range functionNames() {
b.WriteString(functions[name].text)
b.WriteByte('\n')
}
return b.String()
}
// callFunction runs a function in the shell with args[1:] as positional
// parameters, the files fds as stdin, stdout and stderr and the assignments
// that came before the call set while it runs.
func callFunction(fn *funcDef, args []string, fds [3]*os.File, assigns []string) error {
if funcDepth >= maxFuncDepth {
return fmt.Errorf("%s: maximum function nesting level exceeded (%d)", sanitize(fn.name), maxFuncDepth)
}
defer setTemporarily(assigns)()
savedIO, savedPositional, savedLoopDepth := ioOverride, positional, loopDepth
ioOverride, positional, loopDepth = fds, slices.Clone(args[1:]), 0
funcDepth++
localFrames = append(localFrames, map[string]savedVar{})
defer func() {
// Restore the variables declared with local
for name, v := range localFrames[len(localFrames)-1] {
if v.ok {
os.Setenv(name, v.value)
} else {
os.Unsetenv(name)
}
}
localFrames = localFrames[:len(localFrames)-1]
funcDepth--
ioOverride, positional, loopDepth = savedIO, savedPositional, savedLoopDepth
}()
err := fn.body.exec()
var ret returnSignal
if errors.As(err, &ret) {
if ret.status == 0 {
return nil
}
return statusError(ret.status)
}
return err
}
// lookupParam returns the value of a special or positional parameter ($?,
// $#, $$, $0, $1, ..., $@ and $* joined with blanks) or of a variable.
func lookupParam(name string) (string, bool) {
switch {
case name == "?":
return strconv.Itoa(lastStatus), true
case name == "#":
return strconv.Itoa(len(positional)), true
case name == "$":
return strconv.Itoa(os.Getpid()), true
case name == "@" || name == "*":
return strings.Join(positional, " "), true
case name == "0":
return scriptName, true
case isDigits(name):
n, err := strconv.Atoi(name)
if err != nil || n > len(positional) {
return "", false
}
return positional[n-1], true
}
return os.LookupEnv(name)
}
// builtinReturn implements return [n].
func builtinReturn(args []string) error {
if funcDepth == 0 && sourceDepth == 0 {
return errors.New("return: can only `return' from a function or sourced script")
}
status := lastStatus
if len(args) > 1 {
n, err := strconv.Atoi(args[1])
if err != nil {
return fmt.Errorf("return: %s: numeric argument required", sanitize(args[1]))
}
status = n & 0xff
}
return returnSignal{status}
}
// builtinLocal implements local name[=value] ...: the variables get their
// previous values back when the function returns. Without a value the
// variable is unset in the function.
func builtinLocal(args []string) error {
if funcDepth == 0 {
return errors.New("local: can only be used in a function")
}
frame := localFrames[len(localFrames)-1]
var err error
for _, arg := range args[1:] {
name, value, hasValue := strings.Cut(arg, "=")
if !isVarName(name) {
err = fmt.Errorf("local: `%s': not a valid identifier", sanitize(arg))
continue
}
if _, saved := frame[name]; !saved {
old, ok := os.LookupEnv(name)
frame[name] = savedVar{old, ok}
}
if hasValue {
os.Setenv(name, value)
} else {
os.Unsetenv(name)
}
}
return err
}
// builtinShift implements shift [n].
func builtinShift(args []string) error {
n := 1
if len(args) > 1 {
var err error
if n, err = strconv.Atoi(args[1]); err != nil || n < 0 {
return fmt.Errorf("shift: %s: numeric argument required", sanitize(args[1]))
}
}
if n > len(positional) {
return fmt.Errorf("shift: %d: shift count out of range", n)
}
positional = positional[n:]
return nil
}