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
+284
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user