add control structures: if, for, while, until
- 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>
This commit is contained in:
1 parent
e49fe6fe11
commit
f79fb0b8d5
7 files changed
+1362
-237
No files matched your search
+164
@@ -0,0 +1,164 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user