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
6ac8d3bbe8
7 files changed
+311
-3
No files matched your search
@@ -43,6 +43,9 @@ Features:
|
|||||||
TERM=dumb or when output is not a terminal
|
TERM=dumb or when output is not a terminal
|
||||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||||
using an unset variable is an error, use ${VAR:-} for an empty 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),
|
- 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
|
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,
|
- Pipes (a | b) and redirections: < file, > file, >> file, 2> file,
|
||||||
@@ -122,6 +125,26 @@ export LANG=de_DE.UTF-8
|
|||||||
export PATH=$HOME/bin:$PATH
|
export PATH=$HOME/bin:$PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Befehlsersetzung
|
||||||
|
|
||||||
|
`$(befehl)` wird durch die Ausgabe von `befehl` ersetzt:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ echo "Heute ist $(date +%A)"
|
||||||
|
Heute ist Montag
|
||||||
|
$ kill $(pgrep -f mein-server)
|
||||||
|
$ cd $(git rev-parse --show-toplevel)
|
||||||
|
$ vim $(grep -l TODO *.go)
|
||||||
|
$ echo "Dateien: $(ls | wc -l)"
|
||||||
|
```
|
||||||
|
|
||||||
|
- Der Befehl läuft in einer eigenen Subshell (einem neuen gosh-Prozess): `cd`, `set` oder `export` darin ändern die laufende Shell nicht. Er darf alles enthalten, was gosh kann (Pipes, `;`, `&&`, Umleitungen, verschachtelte `$(...)`), und liest von der gleichen Eingabe, Fehlermeldungen gehen auf stderr.
|
||||||
|
- Abschließende Zeilenumbrüche der Ausgabe werden entfernt.
|
||||||
|
- Ohne Anführungszeichen wird die Ausgabe an Leerzeichen, Tabs und Zeilenumbrüchen in mehrere Wörter geteilt, `"$(befehl)"` bleibt ein einziges Wort (mit den Zeilenumbrüchen darin).
|
||||||
|
- Anders als in `sh` werden Wildcards in der Ausgabe nicht expandiert, und Zeichen wie `;` oder `|` darin sind normaler Text: Die Ausgabe ist immer nur Daten.
|
||||||
|
- Besteht ein Befehl nur aus einer Ersetzung (`$(false)`), ist ihr Exit-Status `$?`, sonst zählt der Status des Befehls. Ctrl+C während der Ersetzung bricht die ganze Zeile ab.
|
||||||
|
- In `'...'` oder mit `\$(` wird nichts ersetzt. Die alte Schreibweise mit Backticks (`` `befehl` ``) und Arithmetik `$((1+2))` gibt es nicht.
|
||||||
|
|
||||||
### Befehlslisten und Exit-Status
|
### Befehlslisten und Exit-Status
|
||||||
|
|
||||||
Mehrere Befehle in einer Zeile werden wie in `sh` verknüpft:
|
Mehrere Befehle in einer Zeile werden wie in `sh` verknüpft:
|
||||||
|
|||||||
@@ -96,6 +96,16 @@ func expandAliasesIn(line string, active map[string]bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if quote != '\'' && !escaped && isSubstStart(line, i) {
|
||||||
|
// $(...) is copied as it is; the subshell does not expand
|
||||||
|
// aliases
|
||||||
|
if end := substEnd(line, i); end >= 0 {
|
||||||
|
out.WriteString(line[i : end+1])
|
||||||
|
i = end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out.WriteByte(ch)
|
out.WriteByte(ch)
|
||||||
switch {
|
switch {
|
||||||
case escaped:
|
case escaped:
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import (
|
|||||||
const maxBraceWords = 100000
|
const maxBraceWords = 100000
|
||||||
|
|
||||||
// expandBraceWords applies brace expansion to every word of a command (the
|
// expandBraceWords applies brace expansion to every word of a command (the
|
||||||
// text of a pipeline). Quoted or escaped braces and ${...} are left alone.
|
// text of a pipeline). Quoted or escaped braces, ${...} and $(...) are left
|
||||||
|
// alone.
|
||||||
// A redirection target must expand to a single word.
|
// A redirection target must expand to a single word.
|
||||||
func expandBraceWords(input string) (string, error) {
|
func expandBraceWords(input string) (string, error) {
|
||||||
var (
|
var (
|
||||||
@@ -69,6 +70,10 @@ func wordEnd(s string, i int) int {
|
|||||||
}
|
}
|
||||||
case ch == '\\':
|
case ch == '\\':
|
||||||
i++
|
i++
|
||||||
|
case isSubstStart(s, i):
|
||||||
|
if j := substEnd(s, i); j >= 0 {
|
||||||
|
i = j
|
||||||
|
}
|
||||||
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
||||||
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
||||||
i += j
|
i += j
|
||||||
@@ -103,6 +108,12 @@ func braceScan(s string, fn func(i, depth int) bool) {
|
|||||||
case ch == '\\':
|
case ch == '\\':
|
||||||
i++
|
i++
|
||||||
continue
|
continue
|
||||||
|
case isSubstStart(s, i):
|
||||||
|
// The subshell does its own brace expansion
|
||||||
|
if j := substEnd(s, i); j >= 0 {
|
||||||
|
i = j
|
||||||
|
}
|
||||||
|
continue
|
||||||
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
case ch == '$' && i+1 < len(s) && s[i+1] == '{':
|
||||||
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
if j := strings.IndexByte(s[i:], '}'); j >= 0 {
|
||||||
i += j
|
i += j
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ type token struct {
|
|||||||
// - '...' is taken literally, nothing is expanded inside
|
// - '...' is taken literally, nothing is expanded inside
|
||||||
// - "..." expands $VAR and ${VAR}; \ only escapes $ " \ and `
|
// - "..." expands $VAR and ${VAR}; \ only escapes $ " \ and `
|
||||||
// - outside quotes \ escapes any character
|
// - 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;
|
// - $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
|
// an unset variable is an error, so that e.g. "rm -rf $DIR/x" never runs
|
||||||
// with an empty $DIR
|
// with an empty $DIR
|
||||||
@@ -190,6 +192,32 @@ func tokenize(input string) ([]token, error) {
|
|||||||
case ch == '\\':
|
case ch == '\\':
|
||||||
startRaw(i)
|
startRaw(i)
|
||||||
escaped = true
|
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 == '$':
|
case ch == '$':
|
||||||
startRaw(i)
|
startRaw(i)
|
||||||
value, n, err := expandVar(input[i:])
|
value, n, err := expandVar(input[i:])
|
||||||
@@ -1287,6 +1315,11 @@ scan:
|
|||||||
}
|
}
|
||||||
case ch == '\\':
|
case ch == '\\':
|
||||||
escaped = true
|
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] == '{':
|
case ch == '$' && i+1 < len(input) && input[i+1] == '{':
|
||||||
// Skip ${...} as a whole, like expandVar does
|
// Skip ${...} as a whole, like expandVar does
|
||||||
if j := strings.IndexByte(input[i:], '}'); j >= 0 {
|
if j := strings.IndexByte(input[i:], '}'); j >= 0 {
|
||||||
@@ -1367,6 +1400,7 @@ func execInput(input string) error {
|
|||||||
// the background if background is set.
|
// the background if background is set.
|
||||||
func execCommand(input string, background bool) error {
|
func execCommand(input string, background bool) error {
|
||||||
// Brace expansion comes first, so that its results are expanded further
|
// Brace expansion comes first, so that its results are expanded further
|
||||||
|
substStatus = 0
|
||||||
input, err := expandBraceWords(input)
|
input, err := expandBraceWords(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1375,8 +1409,11 @@ func execCommand(input string, background bool) error {
|
|||||||
// Split the input into words and operators, expanding variables and ~ on
|
// Split the input into words and operators, expanding variables and ~ on
|
||||||
// the way.
|
// the way.
|
||||||
tokens, err := tokenize(input)
|
tokens, err := tokenize(input)
|
||||||
var synErr syntaxError
|
var (
|
||||||
if errors.As(err, &synErr) {
|
synErr syntaxError
|
||||||
|
statusErr statusError
|
||||||
|
)
|
||||||
|
if errors.As(err, &synErr) || errors.As(err, &statusErr) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1387,6 +1424,10 @@ func execCommand(input string, background bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(stages) == 0 {
|
if len(stages) == 0 {
|
||||||
|
// A command that consists only of $(...) has its status
|
||||||
|
if substStatus != 0 {
|
||||||
|
return statusError(substStatus)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// "exit" with stopped jobs only warns, unless it is repeated right away
|
// "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
|
TERM=dumb or when output is not a terminal
|
||||||
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
- Environment variable expansion ($VAR, ${VAR}, ${VAR:-default});
|
||||||
using an unset variable is an error, use ${VAR:-} for an empty 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),
|
- 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
|
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,
|
- Pipes (a | b) and redirections: < file, > file, >> file, 2> file,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -892,6 +893,11 @@ func TestMain(m *testing.M) {
|
|||||||
// Never read the system's startup files; tests that read the user's
|
// Never read the system's startup files; tests that read the user's
|
||||||
// files set $HOME themselves
|
// files set $HOME themselves
|
||||||
sysConfDir = filepath.Join(dir, "etc")
|
sysConfDir = filepath.Join(dir, "etc")
|
||||||
|
// Command substitution runs gosh, not the test binary
|
||||||
|
subshellPath = filepath.Join(dir, "gosh")
|
||||||
|
if out, err := exec.Command("go", "build", "-o", subshellPath, ".").CombinedOutput(); err != nil {
|
||||||
|
panic(fmt.Sprintf("building gosh: %v\n%s", err, out))
|
||||||
|
}
|
||||||
code := m.Run()
|
code := m.Run()
|
||||||
os.RemoveAll(dir)
|
os.RemoveAll(dir)
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubstEnd(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
s string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"$(echo a)", 8},
|
||||||
|
{"$(echo a) b", 8},
|
||||||
|
{"$(echo $(date))x", 14},
|
||||||
|
{`$(echo ")")`, 10},
|
||||||
|
{`$(echo ')')`, 10},
|
||||||
|
{`$(echo \))`, 9},
|
||||||
|
{`$(echo "$(echo ")")")`, 20},
|
||||||
|
{"$(echo (a))", 10},
|
||||||
|
{"$(echo a", -1},
|
||||||
|
{`$(echo ")`, -1},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
if got := substEnd(tc.s, 0); got != tc.want {
|
||||||
|
t.Errorf("substEnd(%q) = %d, want %d", tc.s, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitList_SkipsSubstitution(t *testing.T) {
|
||||||
|
got, err := splitList(`echo $(a; b && c | d) "$(e || f)" & g`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
want := []listItem{{"", `echo $(a; b && c | d) "$(e || f)" `, true}, {";", " g", false}}
|
||||||
|
if !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("splitList = %+v, want %+v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpandAliasesAndBraces_SkipSubstitution(t *testing.T) {
|
||||||
|
withAliases(t, map[string]string{"ll": "ls -l"})
|
||||||
|
if got := expandAliases("echo $(ll; ll) && ll"); got != "echo $(ll; ll) && ls -l" {
|
||||||
|
t.Errorf("expandAliases = %q", got)
|
||||||
|
}
|
||||||
|
got, err := expandBraceWords("echo $(echo {a,b}) {c,d}")
|
||||||
|
if err != nil || got != "echo $(echo {a,b}) c d" {
|
||||||
|
t.Errorf("expandBraceWords = %q, %v", got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecInput_CommandSubstitution(t *testing.T) {
|
||||||
|
origDir, _ := os.Getwd()
|
||||||
|
defer os.Chdir(origDir)
|
||||||
|
dir := t.TempDir()
|
||||||
|
os.Chdir(dir)
|
||||||
|
os.WriteFile("a.go", nil, 0644)
|
||||||
|
os.WriteFile("b.go", nil, 0644)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"echo $(echo hello) > out", "hello\n"},
|
||||||
|
// Unquoted, the output is split into words
|
||||||
|
{"printf '[%s]' $(printf 'a b\\nc\\n\\n') > out", "[a][b][c]"},
|
||||||
|
{"printf '[%s]' x$(echo a b)y > out", "[xa][by]"},
|
||||||
|
// Quoted, it is a single word, only trailing newlines are removed
|
||||||
|
{`printf '[%s]' "$(printf 'a b\nc\n\n')" > out`, "[a b\nc]"},
|
||||||
|
{`printf '[%s]' "$(true)" > out`, "[]"},
|
||||||
|
{"printf '[%s]' $(true) x > out", "[x]"},
|
||||||
|
// Nested, with quotes and operators inside
|
||||||
|
{"echo $(echo $(echo nested)) > out", "nested\n"},
|
||||||
|
{`echo "$(echo "a;b|c")" > out`, "a;b|c\n"},
|
||||||
|
{"echo $(echo a | tr a-z A-Z) > out", "A\n"},
|
||||||
|
// Wildcards in the output are not expanded, in the command they are
|
||||||
|
{`echo $(echo "*") > out`, "*\n"},
|
||||||
|
{"echo $(echo *.go) > out", "a.go b.go\n"},
|
||||||
|
// Quoted or escaped, $( is literal
|
||||||
|
{`echo '$(echo a)' \$(echo b) > out`, "$(echo a) $(echo b)\n"},
|
||||||
|
// The subshell does not change the shell
|
||||||
|
{"echo $(cd /; pwd) > out; pwd >> out", "/\n" + dir + "\n"},
|
||||||
|
{"set GOSH_SUBST=$(echo value); echo $GOSH_SUBST > out", "value\n"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
if err := execInput(tc.input); err != nil {
|
||||||
|
t.Errorf("%q: %v", tc.input, err)
|
||||||
|
}
|
||||||
|
if got, _ := os.ReadFile("out"); string(got) != tc.want {
|
||||||
|
t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Unsetenv("GOSH_SUBST")
|
||||||
|
|
||||||
|
// A command that consists only of a substitution has its status
|
||||||
|
execInput("$(sh -c 'exit 3')")
|
||||||
|
if lastStatus != 3 {
|
||||||
|
t.Errorf("$? = %d after $(exit 3), want 3", lastStatus)
|
||||||
|
}
|
||||||
|
// Otherwise the status of the command counts
|
||||||
|
execInput("echo $(false) > out")
|
||||||
|
if lastStatus != 0 {
|
||||||
|
t.Errorf("$? = %d after echo $(false), want 0", lastStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
for input, wantErr := range map[string]string{
|
||||||
|
"echo $(echo a": "invalid input: missing ) in command substitution",
|
||||||
|
"echo $((1 + 2))": "invalid input: arithmetic expansion $((...)) is not supported",
|
||||||
|
} {
|
||||||
|
if err := execInput(input); err == nil || err.Error() != wantErr {
|
||||||
|
t.Errorf("%q: error %v, want %q", input, err, wantErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user