- functions: name() { ...; }, function name { ...; }, return, local,
unset -f; in pipes, & and $(...) they run in a subshell that gets all
function definitions and the positional parameters
- positional parameters $0, $1 ... ${10}, $#, "$@", "$*", $$; set by
gosh script args, gosh -c command name args, function calls,
source file args and set --; shift; for x without "in"
- case word in pattern|pattern) ...;; esac and { list; } groups
- read [-r] [-p prompt] [name ...] with $IFS splitting and $REPLY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
2.9 KiB
Go
110 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([]string{"-c", functionDefs() + script, scriptName}, 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) {
|
|
if strings.HasPrefix(command, "(") {
|
|
return "", errors.New("arithmetic expansion $((...)) is not supported")
|
|
}
|
|
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
|
|
}
|