add subshells, here-documents and redirections/pipes for control structures

- control structures can have redirections (while read l; do ...; done
  < file), which apply to all commands in them; they run in the shell
- in pipes and in the background they run in a subshell (cmd | while
  read l; do ...; done, for ...; done &, a && b &)
- ( list ) subshells; ( and ) are operators now
- here-documents <<EOF, <<-EOF, <<'EOF' and here-strings <<<word
- redirections of source apply to the commands of the file
- fix non-ASCII characters in quotes being mangled ('für')

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Florian WaltherandClaude Opus 5.5 committed 2026-10-04 20:57:15 +02:00
1 parent 443367f66f
commit f66cf538c0
7 files changed
+1172 -221

No files matched your search

+49 -5
View File
@@ -1437,6 +1437,26 @@ func pipelineString(pl *pipelineNode) string {
s += "while "
}
s += treeString(n.cond) + " do " + treeString(n.body) + " done"
case *caseNode:
s += "case " + n.word + " in"
for _, c := range n.clauses {
s += " (" + strings.Join(c.patterns, "|") + ") " + treeString(c.body) + " ;;"
}
s += " esac"
case *groupNode:
s += "{ " + treeString(n.body) + " }"
case *subshellNode:
s += "(" + n.text + ")"
case *redirNode:
s += pipelineString(&pipelineNode{cmd: n.cmd}) + " <" + n.redirs + ">"
case *pipeNode:
var elems []string
for _, e := range n.elems {
elems = append(elems, "<"+e.text+">")
}
s += strings.Join(elems, " | ")
case *funcDefNode:
s += n.def.name + "() " + pipelineString(&pipelineNode{cmd: n.def.body})
}
return s
}
@@ -1504,6 +1524,27 @@ func TestParse_ControlStructures(t *testing.T) {
{"while a; do if b; then break; fi; done", "while [a] do if [b] then [break] fi done"},
{"for i in 1 2; do for j in 3 4; do echo $i$j; done; done", "for i in (1 2) do for j in (3 4) do [echo $i$j] done done"},
{"if a; then if b; then c; fi fi", "if [a] then if [b] then [c] fi fi"},
// Compound commands in pipelines, with redirections, in the background
{"for x in a; do b; done | cat", "<for x in a; do b; done> | <cat>"},
{"a | while b; do c; done | d", "<a> | <while b; do c; done> | <d>"},
{"if a; then b; fi > out 2>&1", "if [a] then [b] fi <> out 2>&1>"},
{"while read l; do b; done < in", "while [read l] do [b] done << in>"},
{"{ a; b; } 2> err", "{ [a]; [b] } <2> err>"},
{"for x in a; do b; done &", "for x in (a) do [b] done &"},
{"a && b &", "[a] && [b] &"},
// Subshells
{"(a; b)", "(a; b)"},
{"( a ) | b", "<( a )> | <b>"},
{"(a) > f", "(a) <> f>"},
{"(a; (b)) && c", "(a; (b)) && [c]"},
// case, groups and functions
{"case $x in a|b) c;; (d) e; f;; esac", "case $x in (a|b) [c] ;; (d) [e]; [f] ;; esac"},
{"case x in\n a)\n b\n ;;\nesac", "case x in (a) [b] ;; esac"},
{"{ a; }", "{ [a] }"},
{"f() { a; }", "f() { [a] }"},
{"f ( ) { a; }", "f() { [a] }"},
{"function f { a; }", "f() { [a] }"},
{"f() (a)", "f() (a)"},
}
for _, tc := range tests {
tree, err := parse(tc.input)
@@ -1522,6 +1563,8 @@ func TestParse_Incomplete(t *testing.T) {
"if a", "if a; then", "if a; then b", "if a; then b; else", "if a; then b; elif c",
"for x", "for x in a b", "for x in a; do", "for x in a; do b",
"while a", "while a; do b", "until a; do",
"(a", "(a; b", "{ a;", "case x in", "case x in a) b;;", "f() {", "f()",
"cat <<EOF", "cat <<EOF\nbody", "cat <<'EOF'\nbody\nEO",
"a &&", "a ||", "a |", "echo 'open", `echo "open`, "echo $(a", "echo a \\",
} {
if _, err := parse(input); !errors.Is(err, errIncomplete) {
@@ -1536,6 +1579,9 @@ func TestParse_SyntaxErrors(t *testing.T) {
"fi", "then", "done", "if a; then fi", "if ; then a; fi", "while a; do done",
"if a; then b; done", "for 1x in a; do b; done", "for x in a do b; done; x",
"a | | b", "| a",
"()", "a (", "a )", "echo (a)", ")", "(a) b", "if a; then b; fi c", "{ a; } b",
"case x in a b) c;; esac", "case x in ) a;; esac", "cat <<", "cat << ;",
"f() { a; } | b",
} {
_, err := parse(input)
if err == nil || errors.Is(err, errIncomplete) {
@@ -1543,11 +1589,9 @@ func TestParse_SyntaxErrors(t *testing.T) {
}
}
for input, want := range map[string]string{
"for x in a; do b; done | cat": "control structures in pipelines are not supported",
"echo | while a; do b; done": "control structures in pipelines are not supported",
"if a; then b; fi > out": "redirections of control structures are not supported (near `>')",
"for x in a; do b; done &": "only simple commands and pipelines can run in the background",
"a && b &": "only simple commands and pipelines can run in the background",
"f() { a; } | b": "function definitions in pipelines are not supported",
"f() { a; } > out": "redirections of function definitions are not supported",
"if a; then b; fi c": "syntax error near unexpected token `c'",
} {
if _, err := parse(input); err == nil || err.Error() != want {
t.Errorf("parse(%q) = %v, want %q", input, err, want)