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) } } }