Files
Florian WaltherandClaude Opus 5.5 1f507f6562 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>
2026-10-04 21:15:24 +02:00

287 lines
6.5 KiB
Go

package main
// The trap builtin. A trapped signal is only noted when it arrives; its
// command runs in the shell at the next safe point: after the running
// command has finished (like in sh, a shell waiting for a foreground command
// handles the signal afterwards).
import (
"fmt"
"io"
"os"
"os/signal"
"sort"
"strconv"
"strings"
"sync"
"syscall"
)
// trapSignals are the signals that can be trapped, besides EXIT.
var trapSignals = map[string]syscall.Signal{
"HUP": syscall.SIGHUP,
"INT": syscall.SIGINT,
"QUIT": syscall.SIGQUIT,
"TERM": syscall.SIGTERM,
"USR1": syscall.SIGUSR1,
"USR2": syscall.SIGUSR2,
"ALRM": syscall.SIGALRM,
"WINCH": syscall.SIGWINCH,
}
// fatalSignals end a non-interactive shell; with an EXIT trap the shell
// catches them to run it first.
var fatalSignals = []string{"HUP", "INT", "TERM"}
var (
// traps maps EXIT and signal names to their commands ("" = ignore).
traps = map[string]string{}
// trapChans are the channels of the signals the shell catches.
trapChans = map[string]chan os.Signal{}
pendingMu sync.Mutex
pendingSignals []string
inTrap bool
// interruptChan swallows SIGINT in an interactive shell, so that Ctrl+C
// only stops the running command.
interruptChan = make(chan os.Signal, 1)
)
// signalName returns the canonical name (EXIT, INT, ...) of a signal given
// by name (with or without SIG) or number, or "".
func signalName(spec string) string {
s := strings.ToUpper(strings.TrimPrefix(strings.ToUpper(spec), "SIG"))
if s == "EXIT" || s == "0" {
return "EXIT"
}
if _, ok := trapSignals[s]; ok {
return s
}
if n, err := strconv.Atoi(spec); err == nil {
for name, sig := range trapSignals {
if int(sig) == n {
return name
}
}
}
return ""
}
// builtinTrap implements trap [-lp] [[action] signal ...].
func builtinTrap(args []string, stdout io.Writer) error {
args = args[1:]
switch {
case len(args) == 0 || args[0] == "-p":
names := args
if len(names) > 0 {
names = names[1:]
}
return printTraps(stdout, names)
case args[0] == "-l":
names := []string{"EXIT"}
for name := range trapSignals {
names = append(names, name)
}
sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) })
for _, name := range names {
fmt.Fprintf(stdout, "%2d) %s\n", signalNumber(name), name)
}
return nil
case args[0] == "--":
args = args[1:]
}
action := "-"
if len(args) > 1 || len(args) == 1 && signalName(args[0]) == "" {
action, args = args[0], args[1:]
}
if len(args) == 0 {
return fmt.Errorf("trap: usage: trap [-lp] [[action] signal ...]")
}
var err error
for _, spec := range args {
name := signalName(spec)
if name == "" {
err = fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec))
continue
}
setTrap(name, action)
}
return err
}
func signalNumber(name string) int {
if name == "EXIT" {
return 0
}
return int(trapSignals[name])
}
func printTraps(w io.Writer, only []string) error {
var names []string
for name := range traps {
names = append(names, name)
}
if len(only) > 0 {
names = nil
for _, spec := range only {
name := signalName(spec)
if name == "" {
return fmt.Errorf("trap: %s: invalid signal specification", sanitize(spec))
}
if _, ok := traps[name]; ok {
names = append(names, name)
}
}
}
sort.Slice(names, func(i, j int) bool { return signalNumber(names[i]) < signalNumber(names[j]) })
for _, name := range names {
fmt.Fprintf(w, "trap -- %s %s\n", quoteAlias(traps[name]), name)
}
return nil
}
// setTrap sets the action of a trap: "-" restores the default, "" ignores
// the signal, anything else is a command.
func setTrap(name, action string) {
if action == "-" {
delete(traps, name)
} else {
traps[name] = action
}
if name == "EXIT" {
updateFatalCatch()
return
}
sig := trapSignals[name]
stopCatching(name)
switch action {
case "-":
signal.Reset(sig)
restoreShellSignal(sig)
if _, exit := traps["EXIT"]; exit {
updateFatalCatch()
}
case "":
// Ignored signals stay ignored in the programs the shell starts
signal.Ignore(sig)
default:
catch(name)
}
}
// catch makes the shell note the signal, to run its trap later.
func catch(name string) {
if _, ok := trapChans[name]; ok {
return
}
ch := make(chan os.Signal, 4)
trapChans[name] = ch
signal.Notify(ch, trapSignals[name])
go func() {
for range ch {
pendingMu.Lock()
pendingSignals = append(pendingSignals, name)
pendingMu.Unlock()
}
}()
}
func stopCatching(name string) {
if ch, ok := trapChans[name]; ok {
signal.Stop(ch)
close(ch)
delete(trapChans, name)
}
}
// restoreShellSignal installs the handling the shell itself needs for sig
// again after signal.Reset.
func restoreShellSignal(sig syscall.Signal) {
if sig == syscall.SIGINT && interactiveShell {
signal.Notify(interruptChan, os.Interrupt)
}
}
// updateFatalCatch catches the fatal signals in a non-interactive shell with
// an EXIT trap (unless they have traps of their own), so that the EXIT trap
// runs before the shell ends.
func updateFatalCatch() {
if interactiveShell {
return
}
_, exitTrap := traps["EXIT"]
for _, name := range fatalSignals {
if _, own := traps[name]; own {
continue
}
if exitTrap {
catch(name)
} else {
stopCatching(name)
}
}
}
// hasPendingSignal reports whether a caught signal waits for its trap.
func hasPendingSignal() bool {
pendingMu.Lock()
defer pendingMu.Unlock()
return len(pendingSignals) > 0
}
// runPendingTraps runs the traps of the signals that arrived. A fatal signal
// without a trap of its own (caught for the EXIT trap) ends the shell.
func runPendingTraps() {
if inTrap {
return
}
pendingMu.Lock()
names := pendingSignals
pendingSignals = nil
pendingMu.Unlock()
for _, name := range names {
if action, ok := traps[name]; ok {
if action != "" {
runTrap(action)
}
continue
}
// Caught only for the EXIT trap: run it and die from the signal
runExitTrap()
sig := trapSignals[name]
signal.Reset(sig)
releaseTerminal()
syscall.Kill(os.Getpid(), sig)
os.Exit(128 + int(sig))
}
}
// runTrap runs the command of a trap; $? is kept.
func runTrap(action string) {
saved := lastStatus
inTrap = true
defer func() { inTrap = false }()
tree, err := parse(action)
if err == nil {
err = tree.exec()
}
printError(err)
lastStatus = saved
}
// runExitTrap runs the EXIT trap (once) before the shell exits.
func runExitTrap() {
action, ok := traps["EXIT"]
if !ok {
return
}
delete(traps, "EXIT")
if action != "" {
runTrap(action)
}
}