- new parser (parser.go) for lists, pipelines, ! and if/elif/else/fi, for ... in ... do/done, while/until ... do/done; simple commands are kept as text and expanded when they run (in every iteration) - break [n], continue [n] and the : builtin - incomplete input (open control structure, trailing &&, |, quote or backslash) continues on the next line: prompt "> " in interactive shells, scripts and startup files read on - Ctrl+C aborts the whole command line, including loops - aliases are expanded at the start of every simple command, also inside control structures - control structures in pipelines, with redirections or in the background are reported as not supported - completion: commands after if, then, do etc. - main_test.go reformatted with gofmt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
105 lines
3.1 KiB
Go
105 lines
3.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"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 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": "syntax error: unexpected end of input",
|
|
"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)
|
|
}
|
|
}
|
|
}
|