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:
1 parent
f66cf538c0
commit
1f507f6562
20 files changed
+2408
-93
No files matched your search
@@ -0,0 +1,286 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user