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 // lastBackgroundPid is $!: the process ID of the last background job // (of its last process). lastBackgroundPid int // 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 == "!": if lastBackgroundPid == 0 { return "", false } return strconv.Itoa(lastBackgroundPid), 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 }