package main import ( "os" "strings" "testing" ) // runIn runs input in a temporary directory and returns the content of the // file "out" there. func runIn(t *testing.T, input string) string { t.Helper() origDir, _ := os.Getwd() defer os.Chdir(origDir) os.Chdir(t.TempDir()) execInput(input) data, _ := os.ReadFile("out") return string(data) } func TestExec_If(t *testing.T) { tests := []struct { input string want string }{ {"if true; then echo yes >> out; fi", "yes\n"}, {"if false; then echo yes >> out; fi", ""}, {"if false; then echo a >> out; else echo b >> out; fi", "b\n"}, {"if false; then echo a >> out; elif true; then echo b >> out; else echo c >> out; fi", "b\n"}, {"if false; then echo a >> out; elif false; then echo b >> out; else echo c >> out; fi", "c\n"}, {"if ! false; then echo negated >> out; fi", "negated\n"}, {"if true && false; then echo a >> out; else echo b >> out; fi", "b\n"}, {"if false || true; then echo a >> out; fi", "a\n"}, {"if echo cond >> out; then echo body >> out; fi", "cond\nbody\n"}, {"if true\nthen\n echo multi >> out\n echo line >> out\nfi", "multi\nline\n"}, {"if true; then if false; then echo a >> out; else echo nested >> out; fi; fi", "nested\n"}, {"if [ -d / ]; then echo dir >> out; fi", "dir\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_Loops(t *testing.T) { defer os.Unsetenv("GOSH_I") tests := []struct { input string want string }{ {"for x in a b c; do echo $x >> out; done", "a\nb\nc\n"}, {"for x in {1..3}; do echo $x >> out; done", "1\n2\n3\n"}, {"for x in $(echo a b); do echo $x >> out; done", "a\nb\n"}, {"for x in \"a b\" c; do echo $x >> out; done", "a b\nc\n"}, {"touch f1 f2; for f in f*; do echo $f >> out; done", "f1\nf2\n"}, {"for x in; do echo never >> out; done", ""}, {"for x; do echo never >> out; done", ""}, {"set GOSH_I=0; while [ $GOSH_I -lt 3 ]; do echo $GOSH_I >> out; set GOSH_I=$(expr $GOSH_I + 1); done", "0\n1\n2\n"}, {"set GOSH_I=3; until [ $GOSH_I -eq 0 ]; do echo $GOSH_I >> out; set GOSH_I=$(expr $GOSH_I - 1); done", "3\n2\n1\n"}, {"while false; do echo never >> out; done", ""}, // The loop variable keeps the last value {"for GOSH_I in a b; do :; done; echo $GOSH_I > out", "b\n"}, // break and continue {"for x in 1 2 3 4; do if [ $x = 3 ]; then break; fi; echo $x >> out; done", "1\n2\n"}, {"for x in 1 2 3; do if [ $x = 2 ]; then continue; fi; echo $x >> out; done", "1\n3\n"}, {"for a in 1 2; do for b in x y; do if [ $b = y ]; then continue 2; fi; echo $a$b >> out; done; done", "1x\n2x\n"}, {"for a in 1 2; do for b in x y; do echo $a$b >> out; break 2; done; done", "1x\n"}, {"for a in 1 2; do for b in x y; do echo $a$b >> out; break 5; done; done", "1x\n"}, {"while true; do echo once >> out; break; done", "once\n"}, {"for x in 1 2; do echo $x >> out; break; echo never >> out; done; echo after >> out", "1\nafter\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 loopDepth != 0 { t.Errorf("loopDepth = %d after the loops", loopDepth) } } func TestExec_Status(t *testing.T) { tests := []struct { input string status int }{ {"if false; then :; fi", 0}, {"if true; then false; fi", 1}, {"if false; then :; else sh -c 'exit 3'; fi", 3}, {"for x in a; do false; done", 1}, {"for x in a; do false; done; true", 0}, {"for x in; do false; done", 0}, {"while false; do :; done", 0}, {"for x in a b; do sh -c 'exit 4'; break; done", 0}, {"! true", 1}, {"! false", 0}, {"! gosh-no-such-command", 0}, {":", 0}, {"break", 1}, {"continue", 1}, {"for x in a; do break x; done", 1}, {"if a; then", 2}, {"fi", 2}, } for _, tc := range tests { runIn(t, tc.input) if lastStatus != tc.status { t.Errorf("%q: $? = %d, want %d", tc.input, lastStatus, tc.status) } } if err := execInput("break"); err == nil || err.Error() != "break: only meaningful in a loop" { t.Errorf("break outside a loop: %v", err) } } func TestRunScript_MultiLine(t *testing.T) { origDir, _ := os.Getwd() defer os.Chdir(origDir) os.Chdir(t.TempDir()) script := `# a script with control structures for x in a b do if [ $x = a ]; then echo first >> out else echo second >> out fi done echo continued \ line >> out ` if status := runScript(strings.NewReader(script), ""); status != 0 { t.Errorf("status = %d", status) } if got, _ := os.ReadFile("out"); string(got) != "first\nsecond\ncontinued line\n" { t.Errorf("out = %q", got) } // A script that ends inside a control structure is a syntax error if status := runScript(strings.NewReader("echo ok >> out2\nif true; then\n echo never >> out2\n"), ""); status != 2 { t.Errorf("status = %d for an unterminated if, want 2", status) } if got, _ := os.ReadFile("out2"); string(got) != "ok\n" { t.Errorf("out2 = %q", got) } } func TestExec_AliasesInControlStructures(t *testing.T) { withAliases(t, map[string]string{"say": "echo said", "both": "echo one >> out; echo two"}) defer func(saved bool) { interactiveShell = saved }(interactiveShell) interactiveShell = true if got := runIn(t, "for x in a b; do say $x >> out; done"); got != "said a\nsaid b\n" { t.Errorf("alias in for: out = %q", got) } if got := runIn(t, "if true; then both >> out; fi"); got != "one\ntwo\n" { t.Errorf("alias with a list in if: out = %q", got) } if got := runIn(t, "if say x > out; then :; fi"); got != "said x\n" { t.Errorf("alias in condition: out = %q", got) } }