- ${#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>
108 lines
2.9 KiB
Go
108 lines
2.9 KiB
Go
package main
|
|
|
|
// Command substitution: $(command) is replaced by the output of command,
|
|
// which runs in a subshell (a new gosh process), so that cd, set etc. in it
|
|
// do not change the shell. Functions also run in a subshell in pipelines and
|
|
// in the background.
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
// subshellPath is the gosh binary used for command substitution; empty means
|
|
// the running executable. Tests set it, since there the running executable is
|
|
// the test binary.
|
|
var subshellPath string
|
|
|
|
// substStatus is the exit status of the last command substitution of the
|
|
// current command (a command that consists only of a substitution has this
|
|
// status, like in sh).
|
|
var substStatus int
|
|
|
|
// isSubstStart reports whether a command substitution $( starts at s[i].
|
|
func isSubstStart(s string, i int) bool {
|
|
return s[i] == '$' && i+1 < len(s) && s[i+1] == '('
|
|
}
|
|
|
|
// substEnd returns the index of the ) that ends the command substitution
|
|
// starting with $( at s[i], or -1 if it is not terminated. Quotes, escapes
|
|
// and nested substitutions inside are taken into account.
|
|
func substEnd(s string, i int) int {
|
|
depth := 0
|
|
var quote byte
|
|
for j := i + 1; j < len(s); j++ {
|
|
ch := s[j]
|
|
switch {
|
|
case quote == '\'':
|
|
if ch == '\'' {
|
|
quote = 0
|
|
}
|
|
case ch == '\\':
|
|
j++
|
|
case quote == '"':
|
|
if ch == '"' {
|
|
quote = 0
|
|
} else if isSubstStart(s, j) {
|
|
end := substEnd(s, j)
|
|
if end < 0 {
|
|
return -1
|
|
}
|
|
j = end
|
|
}
|
|
case ch == '"' || ch == '\'':
|
|
quote = ch
|
|
case ch == '(':
|
|
depth++
|
|
case ch == ')':
|
|
depth--
|
|
if depth == 0 {
|
|
return j
|
|
}
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// subshellCommand returns a command that runs script in a new gosh process
|
|
// with the same $0, the given positional parameters and the functions of the
|
|
// shell. (Variables need not be passed: they are all in the environment.)
|
|
func subshellCommand(script string, params []string) (*exec.Cmd, error) {
|
|
path := subshellPath
|
|
if path == "" {
|
|
var err error
|
|
if path, err = os.Executable(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
args := append(optionFlags(), "-c", functionDefs()+script, scriptName)
|
|
args = append(args, params...)
|
|
return exec.Command(path, args...), nil
|
|
}
|
|
|
|
// commandSubst runs command in a subshell and returns its output without
|
|
// trailing newlines. stdin and stderr are those of the shell.
|
|
func commandSubst(command string) (string, error) {
|
|
var out bytes.Buffer
|
|
cmd, err := subshellCommand(command, positional)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
cmd.Stdin, cmd.Stdout, cmd.Stderr = stdio(0), &out, stdio(2)
|
|
err = cmd.Run()
|
|
substStatus = exitStatus(err)
|
|
if substStatus == 128+int(syscall.SIGINT) {
|
|
// Ctrl+C aborts the whole command, like in bash
|
|
return "", statusError(substStatus)
|
|
}
|
|
var exitErr *exec.ExitError
|
|
if err != nil && !errors.As(err, &exitErr) {
|
|
return "", err
|
|
}
|
|
return strings.TrimRight(out.String(), "\n"), nil
|
|
}
|