package main // Shell options (set -e, set -x, set -o pipefail) and the builtins eval and // exec. import ( "errors" "fmt" "io" "os" "os/exec" "slices" "sort" "strings" "syscall" ) var ( // errexit (set -e): exit when a command fails errexit bool // xtrace (set -x): print commands before they run xtrace bool // pipefail (set -o pipefail): the status of a pipeline is the one of // the last command that failed pipefail bool // errexitOff is > 0 while commands run whose failure does not end the // shell with set -e: conditions of if, while and until, commands // negated with ! and all but the last command of && and || lists. errexitOff int ) // shellOptions are the long names of the options for set -o. var shellOptions = []struct { name string letter byte value *bool }{ {"errexit", 'e', &errexit}, {"nounset", 'u', nil}, // always on in gosh {"pipefail", 0, &pipefail}, {"xtrace", 'x', &xtrace}, } // setOption sets an option by letter or long name. func setOption(letter byte, name string, on bool) error { for _, o := range shellOptions { if letter != 0 && o.letter == letter || name != "" && o.name == name { if o.value == nil { if !on { return errors.New("set: +u: unset variables are always an error in gosh") } return nil } *o.value = on return nil } } if name != "" { return fmt.Errorf("set: %s: invalid option name", sanitize(name)) } return fmt.Errorf("set: -%c: invalid option", letter) } // optionFlags returns the command line flags of gosh that pass the options // on to a subshell. func optionFlags() []string { var flags []string if errexit { flags = append(flags, "-e") } if xtrace { flags = append(flags, "-x") } if pipefail { flags = append(flags, "-o", "pipefail") } return flags } // builtinSet implements set: set [-+ex] [-+o name] [--] [args], set -o and // set +o (show the options), set without arguments (show the variables) and // gosh's set VAR=value. func builtinSet(args []string, stdout io.Writer) error { args = args[1:] if len(args) == 0 { env := os.Environ() sort.Strings(env) for _, kv := range env { name, value, _ := strings.Cut(kv, "=") if isVarName(name) { fmt.Fprintf(stdout, "%s=%s\n", name, shellQuote(value)) } } return nil } if len(args) == 1 && isAssignment(args[0]) { // set VAR=value name, value, _ := strings.Cut(args[0], "=") os.Setenv(name, value) return nil } for len(args) > 0 { arg := args[0] if arg == "--" { positional = slices.Clone(args[1:]) return nil } if len(arg) < 2 || arg[0] != '-' && arg[0] != '+' { break } on := arg[0] == '-' args = args[1:] if arg[1:] == "o" { if len(args) == 0 { printOptions(stdout, on) return nil } if err := setOption(0, args[0], on); err != nil { return err } args = args[1:] continue } for k := 1; k < len(arg); k++ { if arg[k] == 'o' { // set -euo pipefail: the name is the next argument if len(args) == 0 { return fmt.Errorf("set: %co: option requires an argument", arg[0]) } if err := setOption(0, args[0], on); err != nil { return err } args = args[1:] continue } if err := setOption(arg[k], "", on); err != nil { return err } } } if len(args) > 0 { positional = slices.Clone(args) } return nil } // onlyShows reports whether a builtin that changes the shell is called in a // way that only shows something (trap -p, set -o), so that it can be used in // a pipeline. func onlyShows(args []string) bool { switch args[0] { case "trap": return len(args) == 1 || args[1] == "-p" || args[1] == "-l" case "set": return len(args) == 1 || len(args) == 2 && (args[1] == "-o" || args[1] == "+o") } return false } // printOptions shows the options like set -o (human readable) or set +o // (as commands). func printOptions(w io.Writer, human bool) { for _, o := range shellOptions { on := o.value == nil || *o.value switch { case human && on: fmt.Fprintf(w, "%-15s on\n", o.name) case human: fmt.Fprintf(w, "%-15s off\n", o.name) case on: fmt.Fprintf(w, "set -o %s\n", o.name) default: fmt.Fprintf(w, "set +o %s\n", o.name) } } } // shellQuote quotes s for the shell if it contains special characters. func shellQuote(s string) string { if s != "" && strings.Trim(s, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-+=.,:/@%") == "" { return s } return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } // traceCommand prints a simple command for set -x. func traceCommand(st stage) { var words []string for _, a := range st.assigns { name, value, _ := strings.Cut(a, "=") words = append(words, name+"="+shellQuote(value)) } for _, arg := range st.args { words = append(words, shellQuote(arg)) } if len(words) > 0 { fmt.Fprintln(stdio(2), "+", sanitize(strings.Join(words, " "))) } } // checkErrexit ends the shell with set -e if the pipeline pl, the last one // of its and-or list, failed. Compound commands are not checked themselves // (the failing command in them is), but subshells and pipelines are. func checkErrexit(pl *pipelineNode, err error) { status := exitStatus(err) if !errexit || errexitOff > 0 || pl.negate || status == 0 { return } var ( lc loopControl ret returnSignal ) if errors.As(err, &lc) || errors.As(err, &ret) { return } cmd := pl.cmd if r, ok := cmd.(*redirNode); ok { cmd = r.cmd } switch cmd.(type) { case *simpleNode, *pipeNode, *subshellNode: printError(err) exitShell(status) } } // builtinEval implements eval: the arguments, joined with blanks, run as a // command in the shell. func builtinEval(args []string) error { text := strings.Join(args[1:], " ") if strings.TrimSpace(text) == "" { return nil } tree, err := parse(text) if errors.Is(err, errIncomplete) { return errors.New("eval: syntax error: unexpected end of input") } if err != nil { return err } return tree.exec() } // builtinExec implements exec: without a command its redirections apply to // the shell itself (exec > log), otherwise the command replaces the shell. func builtinExec(args []string, fds [3]*os.File, assigns []string) error { for fd, f := range fds { if f != stdio(fd) { if err := dupTo(f, fd); err != nil { return fmt.Errorf("exec: %w", err) } } } if len(args) == 1 { for _, a := range assigns { name, value, _ := strings.Cut(a, "=") os.Setenv(name, value) } return nil } path, err := exec.LookPath(args[1]) if err != nil { err = fmt.Errorf("%s: %w", sanitize(args[1]), errCommandNotFound) return execFailed(err) } releaseTerminal() err = syscall.Exec(path, args[1:], append(os.Environ(), assigns...)) // Exec only returns if it failed return execFailed(fmt.Errorf("exec: %s: %w", sanitize(args[1]), err)) } // execFailed reports a failed exec. A non-interactive shell exits then, like // sh. func execFailed(err error) error { if interactiveShell { return err } printError(err) exitShell(exitStatus(err)) return err }