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
@@ -131,6 +131,8 @@ type token struct {
|
||||
// - '...' is taken literally, nothing is expanded inside
|
||||
// - "..." expands $VAR and ${VAR}; \ only escapes $ " \ and `
|
||||
// - outside quotes \ escapes any character
|
||||
// - $(command) is replaced by the output of command (see commandSubst);
|
||||
// unquoted, the output is split into words at blanks and newlines
|
||||
// - $VAR, ${VAR} and ${VAR:-default} are expanded outside single quotes;
|
||||
// an unset variable is an error, so that e.g. "rm -rf $DIR/x" never runs
|
||||
// with an empty $DIR
|
||||
@@ -190,6 +192,32 @@ func tokenize(input string) ([]token, error) {
|
||||
case ch == '\\':
|
||||
startRaw(i)
|
||||
escaped = true
|
||||
case isSubstStart(input, i):
|
||||
startRaw(i)
|
||||
end := substEnd(input, i)
|
||||
if end < 0 {
|
||||
return nil, errors.New("missing ) in command substitution")
|
||||
}
|
||||
out, err := commandSubst(input[i+2 : end])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if quote == '"' {
|
||||
literal(out)
|
||||
inWord = true
|
||||
} else {
|
||||
// Unquoted, the output is split into words at blanks and
|
||||
// newlines (but, unlike in sh, wildcards in it are not
|
||||
// expanded)
|
||||
for k, field := range strings.Fields(out) {
|
||||
if k > 0 {
|
||||
endWord()
|
||||
}
|
||||
literal(field)
|
||||
inWord = true
|
||||
}
|
||||
}
|
||||
i = end
|
||||
case ch == '$':
|
||||
startRaw(i)
|
||||
value, n, err := expandVar(input[i:])
|
||||
@@ -1287,6 +1315,11 @@ scan:
|
||||
}
|
||||
case ch == '\\':
|
||||
escaped = true
|
||||
case isSubstStart(input, i):
|
||||
// Skip $(...) as a whole, its ; | & belong to the subshell
|
||||
if j := substEnd(input, i); j >= 0 {
|
||||
i = j
|
||||
}
|
||||
case ch == '$' && i+1 < len(input) && input[i+1] == '{':
|
||||
// Skip ${...} as a whole, like expandVar does
|
||||
if j := strings.IndexByte(input[i:], '}'); j >= 0 {
|
||||
@@ -1367,6 +1400,7 @@ func execInput(input string) error {
|
||||
// the background if background is set.
|
||||
func execCommand(input string, background bool) error {
|
||||
// Brace expansion comes first, so that its results are expanded further
|
||||
substStatus = 0
|
||||
input, err := expandBraceWords(input)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1375,8 +1409,11 @@ func execCommand(input string, background bool) error {
|
||||
// Split the input into words and operators, expanding variables and ~ on
|
||||
// the way.
|
||||
tokens, err := tokenize(input)
|
||||
var synErr syntaxError
|
||||
if errors.As(err, &synErr) {
|
||||
var (
|
||||
synErr syntaxError
|
||||
statusErr statusError
|
||||
)
|
||||
if errors.As(err, &synErr) || errors.As(err, &statusErr) {
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
@@ -1387,6 +1424,10 @@ func execCommand(input string, background bool) error {
|
||||
return err
|
||||
}
|
||||
if len(stages) == 0 {
|
||||
// A command that consists only of $(...) has its status
|
||||
if substStatus != 0 {
|
||||
return statusError(substStatus)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// "exit" with stopped jobs only warns, unless it is repeated right away
|
||||
@@ -1814,6 +1855,9 @@ Features:
|
||||
TERM=dumb or when output is not a terminal
|
||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||
using an unset variable is an error, use ${VAR:-} for an empty default
|
||||
- Command substitution: $(command) is replaced by the output of command
|
||||
(run in a subshell); unquoted it is split into words, "$(command)"
|
||||
stays one word
|
||||
- Command lists: a ; b (run both), a && b (b only if a succeeded),
|
||||
a || b (b only if a failed); $? is the exit status of the last command
|
||||
- Pipes (a | b) and redirections: < file, > file, >> file, 2> file,
|
||||
|
||||
Reference in new issue
Block a user