add parameter expansion, arithmetic, set -e/-x/pipefail, trap, eval, exec

- ${#v}, ${v#p} ${v##p} ${v%p} ${v%%p}, ${v/p/r} ${v//p/r} ${v/#p/r}
  ${v/%p/r}, ${v:off:len}, ${v-w} ${v:=w} ${v:+w} ${v:?msg}; nested ${}
- arithmetic expansion $((...)) with the C operators, ** and assignments
- set -e, set -x, set -o pipefail (also as gosh options, passed on to
  subshells); set shows variables, set -o the options
- trap for EXIT and signals; scripts with an EXIT trap run it on
  SIGINT/SIGTERM/SIGHUP too
- eval, exec (also exec > file without a command), $!, wait pid

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian WaltherandClaude Opus 5.5 committed 2026-10-04 21:15:24 +02:00
1 parent f66cf538c0
commit 1f507f6562
20 files changed
+2408 -93

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
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
}