- ${#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>
287 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|