- ${#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>
285 lines
6.9 KiB
Go
285 lines
6.9 KiB
Go
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
|
|
}
|