Add control flow, I/O redirection, functions, coreutils, history/completion
- I/O redirection: >, >>, <, 2>, 2>&1, &>
- Job control: background & operator
- if/elif/else/fi, for/do/done, while/until loops
- Shell functions with local/declare, positional param save/restore
- Exit code propagation via exitCodeErr sentinel
- Arithmetic expansion $((expr)) with bare variable names
- Command substitution $(cmd) with pipeline support
- Glob expansion, tilde expansion, ${VAR:-default} and other forms
- Tab completion and command history via chzyer/readline
- Inline comment stripping (# outside quotes)
- Builtins: test/[, read, printf, tr, sed, cut, tail, tee, xargs,
basename, dirname, date, sleep, uniq, sort, wc, head, grep, find,
true, false, break, continue, return, shift, set, unset, export,
declare/local, source, alias, jobs, command, which, env
- Bug fixes: tokenizer parenDepth double-count for $((,
splitPipe not paren-aware (broke pipelines in $()),
local/declare TrimLeft stripping valid var name chars,
parseBlocks missing nested keywords after do/then/else
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
352
internal/shell/control.go
Normal file
352
internal/shell/control.go
Normal file
@@ -0,0 +1,352 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// executeIf handles: if COND; then BODY; [elif COND; then BODY;]* [else BODY;] fi
|
||||
func (s *Shell) executeIf(block string) error {
|
||||
stmts := splitStatements(block)
|
||||
|
||||
type branch struct {
|
||||
cond []string
|
||||
body []string
|
||||
}
|
||||
|
||||
var branches []branch
|
||||
var elseBody []string
|
||||
|
||||
phase := "if_cond"
|
||||
var curCond []string
|
||||
var curBody []string
|
||||
|
||||
for _, stmt := range stmts {
|
||||
w := firstWord(stmt)
|
||||
rest := afterWord(stmt)
|
||||
|
||||
switch {
|
||||
case w == "if" && phase == "if_cond":
|
||||
if rest != "" {
|
||||
curCond = append(curCond, rest)
|
||||
}
|
||||
case w == "then":
|
||||
if rest != "" {
|
||||
curBody = append(curBody, rest)
|
||||
}
|
||||
phase = "body"
|
||||
case w == "elif":
|
||||
branches = append(branches, branch{curCond, curBody})
|
||||
curCond = nil
|
||||
curBody = nil
|
||||
if rest != "" {
|
||||
curCond = append(curCond, rest)
|
||||
}
|
||||
phase = "elif_cond"
|
||||
case w == "else":
|
||||
branches = append(branches, branch{curCond, curBody})
|
||||
curCond = nil
|
||||
curBody = nil
|
||||
if rest != "" {
|
||||
elseBody = append(elseBody, rest)
|
||||
}
|
||||
phase = "else"
|
||||
case w == "fi":
|
||||
switch phase {
|
||||
case "body":
|
||||
branches = append(branches, branch{curCond, curBody})
|
||||
case "else":
|
||||
if rest != "" {
|
||||
elseBody = append(elseBody, rest)
|
||||
}
|
||||
}
|
||||
default:
|
||||
switch phase {
|
||||
case "if_cond", "elif_cond":
|
||||
curCond = append(curCond, stmt)
|
||||
case "body":
|
||||
curBody = append(curBody, stmt)
|
||||
case "else":
|
||||
elseBody = append(elseBody, stmt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range branches {
|
||||
cond := strings.Join(b.cond, "\n")
|
||||
s.Execute(cond) //nolint — we only care about $?
|
||||
if s.vars["?"] == "0" {
|
||||
return s.Execute(strings.Join(b.body, "\n"))
|
||||
}
|
||||
}
|
||||
if len(elseBody) > 0 {
|
||||
return s.Execute(strings.Join(elseBody, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeFor handles: for VAR in WORDS; do BODY; done
|
||||
// (also: for VAR; do BODY; done — iterates positional params)
|
||||
func (s *Shell) executeFor(block string) error {
|
||||
stmts := splitStatements(block)
|
||||
if len(stmts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse "for VAR in WORDS"
|
||||
header := stmts[0]
|
||||
fields := strings.Fields(header)
|
||||
if len(fields) < 2 {
|
||||
return fmt.Errorf("for: bad syntax")
|
||||
}
|
||||
varName := fields[1]
|
||||
|
||||
var items []string
|
||||
inIdx := -1
|
||||
for i, w := range fields {
|
||||
if w == "in" {
|
||||
inIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if inIdx >= 0 {
|
||||
for _, raw := range fields[inIdx+1:] {
|
||||
expanded := s.expandWord(raw)
|
||||
items = append(items, s.expandGlob(expanded)...)
|
||||
}
|
||||
} else {
|
||||
// for var; do ... → iterate positional params
|
||||
items = s.args
|
||||
}
|
||||
|
||||
// Collect body between "do" and "done"
|
||||
var bodyStmts []string
|
||||
inBody := false
|
||||
for _, stmt := range stmts[1:] {
|
||||
w := firstWord(stmt)
|
||||
if !inBody {
|
||||
if w == "do" {
|
||||
inBody = true
|
||||
if rest := afterWord(stmt); rest != "" {
|
||||
bodyStmts = append(bodyStmts, rest)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if w == "done" {
|
||||
break
|
||||
}
|
||||
bodyStmts = append(bodyStmts, stmt)
|
||||
}
|
||||
|
||||
body := strings.Join(bodyStmts, "\n")
|
||||
|
||||
for _, item := range items {
|
||||
s.vars[varName] = item
|
||||
if err := s.Execute(body); err != nil {
|
||||
if be, ok := err.(breakErr); ok {
|
||||
if be.n <= 1 {
|
||||
break
|
||||
}
|
||||
return breakErr{be.n - 1}
|
||||
}
|
||||
if ce, ok := err.(continueErr); ok {
|
||||
if ce.n <= 1 {
|
||||
continue
|
||||
}
|
||||
return continueErr{ce.n - 1}
|
||||
}
|
||||
if _, ok := err.(returnErr); ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// executeWhileUntil handles while/until loops.
|
||||
func (s *Shell) executeWhileUntil(block string, isUntil bool) error {
|
||||
stmts := splitStatements(block)
|
||||
if len(stmts) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse condition (everything from "while/until COND" up to "do")
|
||||
var condStmts []string
|
||||
if rest := afterWord(stmts[0]); rest != "" {
|
||||
condStmts = append(condStmts, rest)
|
||||
}
|
||||
|
||||
var bodyStmts []string
|
||||
inBody := false
|
||||
for _, stmt := range stmts[1:] {
|
||||
w := firstWord(stmt)
|
||||
if !inBody {
|
||||
if w == "do" {
|
||||
inBody = true
|
||||
if rest := afterWord(stmt); rest != "" {
|
||||
bodyStmts = append(bodyStmts, rest)
|
||||
}
|
||||
} else {
|
||||
condStmts = append(condStmts, stmt)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if w == "done" {
|
||||
break
|
||||
}
|
||||
bodyStmts = append(bodyStmts, stmt)
|
||||
}
|
||||
|
||||
cond := strings.Join(condStmts, "\n")
|
||||
body := strings.Join(bodyStmts, "\n")
|
||||
|
||||
for {
|
||||
s.Execute(cond) //nolint
|
||||
condOk := s.vars["?"] == "0"
|
||||
|
||||
if (isUntil && condOk) || (!isUntil && !condOk) {
|
||||
break
|
||||
}
|
||||
|
||||
if err := s.Execute(body); err != nil {
|
||||
if be, ok := err.(breakErr); ok {
|
||||
if be.n <= 1 {
|
||||
break
|
||||
}
|
||||
return breakErr{be.n - 1}
|
||||
}
|
||||
if ce, ok := err.(continueErr); ok {
|
||||
if ce.n <= 1 {
|
||||
continue
|
||||
}
|
||||
return continueErr{ce.n - 1}
|
||||
}
|
||||
if _, ok := err.(returnErr); ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// defineFunction parses and registers a shell function definition.
|
||||
func (s *Shell) defineFunction(block string) error {
|
||||
stmts := splitStatements(block)
|
||||
if len(stmts) == 0 {
|
||||
return fmt.Errorf("syntax error: empty function")
|
||||
}
|
||||
|
||||
first := stmts[0]
|
||||
var name string
|
||||
|
||||
if strings.HasPrefix(first, "function ") {
|
||||
rest := strings.TrimPrefix(first, "function ")
|
||||
rest = strings.TrimSpace(rest)
|
||||
// Strip trailing () and {
|
||||
rest = strings.TrimSuffix(strings.TrimSpace(rest), "{")
|
||||
rest = strings.TrimSuffix(strings.TrimSpace(rest), "()")
|
||||
name = strings.TrimSpace(rest)
|
||||
} else {
|
||||
parenIdx := strings.Index(first, "(")
|
||||
if parenIdx < 0 {
|
||||
return fmt.Errorf("syntax error: bad function definition")
|
||||
}
|
||||
name = strings.TrimSpace(first[:parenIdx])
|
||||
}
|
||||
|
||||
if !isValidIdentifier(name) {
|
||||
return fmt.Errorf("syntax error: invalid function name %q", name)
|
||||
}
|
||||
|
||||
// Find the opening { in the block — it may be on the same line as the name
|
||||
// or on a following stmt. Everything after { (up to closing }) is the body.
|
||||
var bodyStmts []string
|
||||
inBody := false
|
||||
|
||||
for _, stmt := range stmts {
|
||||
trimmed := strings.TrimSpace(stmt)
|
||||
|
||||
if !inBody {
|
||||
// Look for { in this stmt
|
||||
braceIdx := strings.Index(trimmed, "{")
|
||||
if braceIdx >= 0 {
|
||||
inBody = true
|
||||
rest := strings.TrimSpace(trimmed[braceIdx+1:])
|
||||
if rest != "" && rest != "}" {
|
||||
bodyStmts = append(bodyStmts, rest)
|
||||
}
|
||||
// Check if } is also on this line (single-liner like name() { cmd; })
|
||||
if strings.HasSuffix(trimmed, "}") && braceIdx < len(trimmed)-1 {
|
||||
// body is between { and }
|
||||
inner := strings.TrimSpace(trimmed[braceIdx+1 : len(trimmed)-1])
|
||||
bodyStmts = nil
|
||||
if inner != "" {
|
||||
bodyStmts = append(bodyStmts, inner)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if trimmed == "}" {
|
||||
break
|
||||
}
|
||||
bodyStmts = append(bodyStmts, stmt)
|
||||
}
|
||||
|
||||
funcBody := strings.Join(bodyStmts, "\n")
|
||||
s.funcs[name] = funcBody
|
||||
|
||||
funcName := name
|
||||
s.builtins[funcName] = func(args []string) error {
|
||||
return s.callFunction(funcName, args)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Shell) callFunction(name string, args []string) error {
|
||||
body, ok := s.funcs[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s: function not found", name)
|
||||
}
|
||||
|
||||
// Save positional params and exit code
|
||||
oldArgs := s.args
|
||||
savedPos := map[string]string{}
|
||||
for k, v := range s.vars {
|
||||
if k == "#" || k == "@" || k == "*" || (len(k) == 1 && k[0] >= '1' && k[0] <= '9') {
|
||||
savedPos[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
s.SetArgs(args)
|
||||
s.vars["?"] = "0" // reset before running body
|
||||
|
||||
err := s.Execute(body)
|
||||
|
||||
// Capture the function's exit code BEFORE restoring params (which might not include ?)
|
||||
funcExitCode := s.lastExit
|
||||
|
||||
// Restore positional params
|
||||
s.args = oldArgs
|
||||
for k, v := range savedPos {
|
||||
s.vars[k] = v
|
||||
}
|
||||
|
||||
if re, ok := err.(returnErr); ok {
|
||||
if re.code != 0 {
|
||||
return exitCodeErr{re.code}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Propagate last command's exit code from the function body
|
||||
if funcExitCode != 0 {
|
||||
return exitCodeErr{funcExitCode}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user