add command substitution $(...)
- $(command) is replaced by the output of command, which runs in a subshell (gosh -c), so cd/set in it do not change the shell - trailing newlines are removed; unquoted the output is split into words at blanks and newlines, "$(...)" stays one word; wildcards in the output are not expanded - nested substitutions, quotes and operators inside are handled; the list, alias and brace scanners skip $(...) as a whole - a command that consists only of a substitution has its exit status, Ctrl+C in a substitution aborts the command - $((...)) is reported as not supported Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
041406a340
commit
e49fe6fe11
7 files changed
+311
-3
No files matched your search
@@ -0,0 +1,98 @@
|
||||
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.
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
path := subshellPath
|
||||
if path == "" {
|
||||
var err error
|
||||
if path, err = os.Executable(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd := exec.Command(path, "-c", command)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, &out, os.Stderr
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user