package main import ( "errors" "os" "reflect" "strings" "testing" "time" ) func TestExec_CompoundRedirections(t *testing.T) { resetShell(t) for _, name := range []string{"GOSH_A", "GOSH_N"} { os.Unsetenv(name) defer os.Unsetenv(name) } tests := []struct { input string want string }{ {"if true; then echo yes; fi > out", "yes\n"}, {"for x in a b; do echo $x; done > out", "a\nb\n"}, {"for x in a b; do echo $x; done >> out; echo c >> out", "a\nb\nc\n"}, {"{ echo out; echo err >&2; } > out 2>&1", "out\nerr\n"}, {"{ echo err >&2; } 2> out", "err\n"}, {"case x in x) echo case;; esac > out", "case\n"}, // The redirections apply to all commands, also nested ones {"{ echo a; { echo b; }; } > out", "a\nb\n"}, {"{ echo a; echo b > other; echo c; } > out", "a\nc\n"}, {"f() { echo from-f; }; { f; } > out", "from-f\n"}, // Reading a file line by line in the shell itself {"printf 'l1\\nl2 x\\n' > in; while read -r l; do echo \"<$l>\" >> out; done < in", "\n\n"}, {"printf 'a b\\nc d\\n' > in; while read x y; do echo $y$x >> out; done < in", "ba\ndc\n"}, {"printf '1\\n2\\n3\\n' > in; GOSH_N=0; while read l; do GOSH_N=$(expr $GOSH_N + $l); done < in; echo $GOSH_N > out", "6\n"}, // ... with the loop's variables changed in the shell {"printf 'v\\n' > in; while read GOSH_A; do :; done < in; echo ${GOSH_A:-empty} > out", "empty\n"}, {"printf 'v\\n' > in; { read GOSH_A; } < in; echo $GOSH_A > out", "v\n"}, // Redirections are expanded {"GOSH_A=out; { echo var; } > $GOSH_A", "var\n"}, // Files are closed and stdout is restored afterwards {"{ echo a; } > out; echo b >> out", "a\nb\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } if ioOverride != [3]*os.File{} { t.Fatalf("%q: ioOverride not restored", tc.input) } } for input, want := range map[string]string{ "{ :; } < gosh-no-such-file": "gosh-no-such-file: no such file or directory", "{ :; } > {a,b}": "{a,b}: ambiguous redirect", } { if err := runInErr(t, input); err == nil || err.Error() != want { t.Errorf("%q: err = %v, want %q", input, err, want) } } } // runInErr runs input in a temporary directory and returns its error. func runInErr(t *testing.T, input string) error { t.Helper() origDir, _ := os.Getwd() defer os.Chdir(origDir) os.Chdir(t.TempDir()) return execInput(input) } func TestExec_CompoundPipelines(t *testing.T) { resetShell(t) os.Unsetenv("GOSH_A") defer os.Unsetenv("GOSH_A") tests := []struct { input string want string }{ {"for x in 3 1 2; do echo $x; done | sort > out", "1\n2\n3\n"}, {"printf 'a b\\nc d\\n' | while read x y; do echo $y$x; done > out", "ba\ndc\n"}, {"echo hi | { read l; echo got $l; } > out", "got hi\n"}, {"if true; then echo yes; fi | tr a-z A-Z > out", "YES\n"}, {"echo x | case y in y) cat;; esac | tr x X > out", "X\n"}, {"for x in a b; do echo $x; done > /dev/null | cat > out", ""}, {"{ echo b; echo a; } | sort | head -1 > out", "a\n"}, // The parts of a pipeline run in subshells: the shell is not changed {"echo v | while read GOSH_A; do :; done; echo ${GOSH_A:-unset} > out", "unset\n"}, // Functions and positional parameters are known in the subshell {"f() { echo f-$1; }; set -- p; for x in 1; do f $1; done | cat > out", "f-p\n"}, // The status is the one of the last command {"echo | while read l; do false; done; echo $? > out", "1\n"}, {"true | if false; then :; else sh -c 'exit 3'; fi; echo $? > out", "3\n"}, {"for x in 1; do sh -c 'exit 4'; done | true; echo $? > out", "0\n"}, {"! echo | while read l; do false; done; echo $? > out", "0\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } } } func TestExec_Subshells(t *testing.T) { resetShell(t) os.Unsetenv("GOSH_A") defer os.Unsetenv("GOSH_A") tests := []struct { input string want string }{ {"(echo a; echo b) > out", "a\nb\n"}, {"(echo sub) | tr a-z A-Z > out", "SUB\n"}, // Changes stay in the subshell {"mkdir d; (cd d; pwd > ../in); cd d 2>/dev/null && echo still-outside > out", "still-outside\n"}, {"GOSH_A=1; (GOSH_A=2; echo $GOSH_A > out); echo $GOSH_A >> out", "2\n1\n"}, {"(exit 3); echo $? > out", "3\n"}, {"(exit 3) || echo failed > out", "failed\n"}, {"(f() { echo in-sub; }; f) > out; f 2>> out || echo no-f >> out", "in-sub\nno-f\n"}, {"(echo a; (echo b)) > out", "a\nb\n"}, {"( echo spaced ) > out", "spaced\n"}, {"(\n echo multi\n echo line\n) > out", "multi\nline\n"}, // The subshell knows the functions and positional parameters {"f() { echo f-$1; }; set -- p; (f $1) > out", "f-p\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } } } func TestExec_CompoundBackground(t *testing.T) { resetShell(t) tests := []struct { input string want string }{ {"for x in a b; do echo $x; done > out & wait", "a\nb\n"}, {"true && echo and > out & wait", "and\n"}, {"false || echo or > out & wait", "or\n"}, {"(echo sub > out) & wait", "sub\n"}, {"{ echo group; } > out & wait", "group\n"}, {"! false && echo negated > out & wait", "negated\n"}, {"for x in 3 1; do echo $x; done | sort > out & wait", "1\n3\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } } // The shell does not wait for a background loop start := time.Now() runIn(t, "for x in 1; do sleep 1; done & echo $? > out") if lastStatus != 0 || time.Since(start) > 500*time.Millisecond { t.Errorf("background loop: $? = %d after %v", lastStatus, time.Since(start)) } execInput("wait") } func TestExtractHeredocs(t *testing.T) { tests := []struct { input string want string }{ {"cat < out\nhello\nworld\nEOF", "hello\nworld\n"}, {"cat < out\n$GOSH_A ${GOSH_A} $(echo sub) \\$GOSH_A\nEOF", "value value sub $GOSH_A\n"}, {"cat <<'EOF' > out\n$GOSH_A $(echo sub)\nEOF", "$GOSH_A $(echo sub)\n"}, {"cat < out\n\"quotes\" 'single' back\\slash\nEOF", "\"quotes\" 'single' back\\slash\n"}, {"cat < out\nline \\\ncontinued\nEOF", "line continued\n"}, {"cat <<'EOF' > out\nline \\\nkept\nEOF", "line \\\nkept\n"}, {"cat <<-EOF > out\n\tindented\n\t\tmore\n\tEOF", "indented\nmore\n"}, {"cat < out\n *.go {a,b} ~\nEOF", " *.go {a,b} ~\n"}, {"cat < out\nEOF", ""}, {"cat < out\npiped\nEOF", "PIPED\n"}, {"cat > out < out; cat <> out\na\nA\nb\nB", "a\nb\n"}, {"while read l; do echo \"<$l>\" >> out; done <\n\n"}, {"f() { cat; }; f < out\nto f\nEOF", "to f\n"}, {"set -- a 'b c'; cat < out\n$# $1 $@\nEOF", "2 a a b c\n"}, {"for x in 1 2; do\n cat <> out\nx=$x\nEOF\ndone", "x=1\nx=2\n"}, {"echo \"$(cat < out", "in subst\n"}, // Here-strings {"cat << out", "word\n"}, {"cat <<< \"two words $GOSH_A\" > out", "two words value\n"}, {"cat <<<'*.go' > out", "*.go\n"}, {"read x y <<<'a b c'; echo $y > out", "b c\n"}, } for _, tc := range tests { if got := runIn(t, tc.input); got != tc.want { t.Errorf("%q: out = %q, want %q", tc.input, got, tc.want) } } } // TestRunScript_Heredoc checks that a script reads a here-document and // continues after it, with correct line numbers in error messages. func TestRunScript_Heredoc(t *testing.T) { resetShell(t) got := runIn(t, `printf 'cat < out\nbody\nEOF\necho after >> out\n' > s; . ./s`) if got != "body\nafter\n" { t.Errorf("script with here-document: out = %q", got) } got = runIn(t, `printf 'cat < s; . ./s 2> out`) if !strings.Contains(got, "s:1: syntax error: unexpected end of file") { t.Errorf("unterminated here-document: %q", got) } } // TestTokenize_UTF8 checks that non-ASCII characters survive quoting and // escaping (each byte used to be converted to a rune of its own). func TestTokenize_UTF8(t *testing.T) { got, err := splitWords(`echo 'wörtlich' "für" über \ä 'a'"ö"`) want := []string{"echo", "wörtlich", "für", "über", "ä", "aö"} if err != nil || !reflect.DeepEqual(got, want) { t.Errorf("got %q, %v, want %q", got, err, want) } if got := runIn(t, "cat <<'EOF' > out\nGrüße\nEOF"); got != "Grüße\n" { t.Errorf("here-document: %q", got) } }