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
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user