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 }