Skip to main content

frost_exec/
execute.rs

1//! The main execution engine.
2//!
3//! Walks the AST and executes commands by forking child processes,
4//! setting up pipes, and applying redirections. All platform-specific
5//! system calls go through [`crate::sys`].
6
7use std::ffi::CString;
8use std::os::fd::RawFd;
9
10use nix::unistd::Pid;
11
12use frost_builtins::BuiltinRegistry;
13use frost_parser::ast::{
14    AlwaysClause, ArithForClause, BraceGroup, CaseClause, CaseTerminator, Command,
15    CompleteCommand, ForClause, FunctionDef, GlobKind, IfClause, List, ListOp, Pipeline, Program,
16    Redirect, RedirectOp, RepeatClause, SelectClause, SimpleCommand, Subshell, UntilClause,
17    WhileClause, Word, WordPart,
18};
19
20use crate::env::ShellEnv;
21use crate::job::JobTable;
22use crate::redirect;
23use crate::sys;
24
25/// Execution errors.
26#[derive(Debug, thiserror::Error)]
27pub enum ExecError {
28    #[error("command not found: {0}")]
29    CommandNotFound(String),
30
31    #[error("fork failed: {0}")]
32    Fork(nix::errno::Errno),
33
34    #[error("exec failed: {0}")]
35    Exec(nix::errno::Errno),
36
37    #[error("pipe failed: {0}")]
38    Pipe(nix::errno::Errno),
39
40    #[error("wait failed: {0}")]
41    Wait(nix::errno::Errno),
42
43    #[error("redirect error: {0}")]
44    Redirect(#[from] redirect::RedirectError),
45}
46
47/// Result alias for execution operations.
48pub type ExecResult = Result<i32, ExecError>;
49
50/// The command executor.
51///
52/// Holds a mutable reference to the shell environment and a builtin
53/// registry. Create one per top-level evaluation.
54pub struct Executor<'env> {
55    pub env: &'env mut ShellEnv,
56    pub builtins: BuiltinRegistry,
57    pub jobs: JobTable,
58}
59
60impl<'env> Executor<'env> {
61    /// Create a new executor with the default builtins.
62    pub fn new(env: &'env mut ShellEnv) -> Self {
63        Self {
64            env,
65            builtins: frost_builtins::default_builtins(),
66            jobs: JobTable::new(),
67        }
68    }
69
70    // ── Top-level entry ──────────────────────────────────────────
71
72    /// Execute an entire program (a list of complete commands).
73    pub fn execute_program(&mut self, program: &Program) -> ExecResult {
74        let mut status = 0;
75        for cmd in &program.commands {
76            status = self.execute_complete_command(cmd)?;
77        }
78        Ok(status)
79    }
80
81    /// Execute a single complete command (which may be async / `&`).
82    fn execute_complete_command(&mut self, cmd: &CompleteCommand) -> ExecResult {
83        let status = self.execute_list(&cmd.list)?;
84
85        if cmd.is_async {
86            self.env.exit_status = 0;
87            Ok(0)
88        } else {
89            self.env.exit_status = status;
90            Ok(status)
91        }
92    }
93
94    /// Execute a list (pipelines joined by `&&` / `||`).
95    fn execute_list(&mut self, list: &List) -> ExecResult {
96        let mut status = self.execute_pipeline(&list.first)?;
97
98        for (op, pipeline) in &list.rest {
99            match op {
100                ListOp::And if status == 0 => {
101                    status = self.execute_pipeline(pipeline)?;
102                }
103                ListOp::Or if status != 0 => {
104                    status = self.execute_pipeline(pipeline)?;
105                }
106                _ => {}
107            }
108        }
109
110        Ok(status)
111    }
112
113    // ── Pipeline ─────────────────────────────────────────────────
114
115    /// Execute a pipeline of one or more commands connected by pipes.
116    pub fn execute_pipeline(&mut self, pipeline: &Pipeline) -> ExecResult {
117        let cmds = &pipeline.commands;
118
119        if cmds.len() == 1 {
120            let status = self.execute_command(&cmds[0])?;
121            return Ok(if pipeline.bang { invert(status) } else { status });
122        }
123
124        // Multi-command pipeline: create N-1 pipes via sys abstraction.
125        let mut pipes = Vec::with_capacity(cmds.len() - 1);
126        for _ in 0..cmds.len() - 1 {
127            let p = sys::pipe().map_err(ExecError::Pipe)?;
128            pipes.push((p.read, p.write));
129        }
130
131        let mut children: Vec<Pid> = Vec::with_capacity(cmds.len());
132
133        for (i, cmd) in cmds.iter().enumerate() {
134            match unsafe { sys::fork() }.map_err(ExecError::Fork)? {
135                sys::ForkOutcome::Child => {
136                    // Wire stdin from previous pipe.
137                    if i > 0 {
138                        let (rd, _) = pipes[i - 1];
139                        sys::dup2(rd, 0).ok();
140                    }
141                    // Wire stdout to next pipe.
142                    if i < cmds.len() - 1 {
143                        let (_, wr) = pipes[i];
144                        sys::dup2(wr, 1).ok();
145
146                        if pipeline.pipe_stderr.get(i).copied().unwrap_or(false) {
147                            sys::dup2(wr, 2).ok();
148                        }
149                    }
150                    // Close all pipe fds in the child.
151                    for &(rd, wr) in &pipes {
152                        sys::close(rd).ok();
153                        sys::close(wr).ok();
154                    }
155                    let status = self.execute_command(cmd).unwrap_or(127);
156                    std::process::exit(status);
157                }
158                sys::ForkOutcome::Parent { child_pid } => {
159                    children.push(child_pid);
160                }
161            }
162        }
163
164        // Parent: close all pipe fds.
165        for (rd, wr) in pipes {
166            sys::close(rd).ok();
167            sys::close(wr).ok();
168        }
169
170        // Wait for all children, return the exit status of the last.
171        let mut last_status = 0;
172        for pid in children {
173            match sys::wait_pid(pid).map_err(ExecError::Wait)? {
174                sys::ChildStatus::Exited(code) => last_status = code,
175                sys::ChildStatus::Signaled(code) => last_status = code,
176                _ => {}
177            }
178        }
179
180        Ok(if pipeline.bang {
181            invert(last_status)
182        } else {
183            last_status
184        })
185    }
186
187    // ── Command dispatch ─────────────────────────────────────────
188
189    /// Execute a single command node from the AST.
190    pub fn execute_command(&mut self, cmd: &Command) -> ExecResult {
191        match cmd {
192            Command::Simple(simple) => self.execute_simple(simple),
193            Command::Subshell(sub) => self.execute_subshell(sub),
194            Command::BraceGroup(bg) => self.execute_brace_group(bg),
195            Command::If(clause) => self.execute_if(clause),
196            Command::For(clause) => self.execute_for(clause),
197            Command::ArithFor(clause) => self.execute_arith_for(clause),
198            Command::While(clause) => self.execute_while(clause),
199            Command::Until(clause) => self.execute_until(clause),
200            Command::Case(clause) => self.execute_case(clause),
201            Command::Select(clause) => self.execute_select(clause),
202            Command::Repeat(clause) => self.execute_repeat(clause),
203            Command::Always(clause) => self.execute_always(clause),
204            Command::FunctionDef(fdef) => self.execute_function_def(fdef),
205            Command::Coproc(_) => {
206                // Coproc is complex — stub for now.
207                eprintln!("frost: coproc: not yet implemented");
208                Ok(1)
209            }
210            Command::Time(tc) => {
211                let start = std::time::Instant::now();
212                let status = self.execute_pipeline(&tc.pipeline)?;
213                let elapsed = start.elapsed();
214                eprintln!(
215                    "\nreal\t{:.3}s\nuser\t0.000s\nsys\t0.000s",
216                    elapsed.as_secs_f64()
217                );
218                Ok(status)
219            }
220        }
221    }
222
223    // ── Simple command ───────────────────────────────────────────
224
225    /// Execute a simple command (assignments + words + redirects).
226    pub fn execute_simple(&mut self, cmd: &SimpleCommand) -> ExecResult {
227        // Process assignments.
228        for assign in &cmd.assignments {
229            let value = assign
230                .value
231                .as_ref()
232                .map(|w| self.expand_word(w))
233                .unwrap_or_default();
234            self.env.set_var(&assign.name, &value);
235        }
236
237        if cmd.words.is_empty() {
238            return Ok(0);
239        }
240
241        let argv: Vec<String> = cmd
242            .words
243            .iter()
244            .flat_map(|w| self.expand_word_glob(w))
245            .filter(|s| !s.is_empty())
246            .collect();
247
248        // If all words expanded to empty, it's a no-op (reset exit status to 0).
249        if argv.is_empty() {
250            self.env.exit_status = 0;
251            return Ok(0);
252        }
253
254        let name = &argv[0];
255
256        // Check for shell functions first.
257        if let Some(func) = self.env.functions.get(name).cloned() {
258            return self.call_function(&func, &argv[1..], &cmd.redirects);
259        }
260
261        // Special builtins that need executor access.
262        match name.as_str() {
263            "eval" => return self.execute_eval(&argv[1..], &cmd.redirects),
264            "source" | "." => return self.execute_source(&argv[1..], &cmd.redirects),
265            "shift" => return self.execute_shift(&argv[1..]),
266            "[[" => return self.execute_cond_bracket(&argv[1..]),
267            "((" => return self.execute_arith_command(&argv[1..]),
268            _ => {}
269        }
270
271        // Check builtins.
272        if self.builtins.contains(name) {
273            return self.execute_builtin(name, &argv[1..], &cmd.redirects);
274        }
275
276        // External command: fork + exec.
277        self.fork_exec(&argv, &cmd.redirects)
278    }
279
280    /// Execute a builtin command with redirect support.
281    fn execute_builtin(
282        &mut self,
283        name: &str,
284        args: &[String],
285        redirects: &[Redirect],
286    ) -> ExecResult {
287        let saved_fds = self.save_and_apply_redirects(redirects)?;
288
289        let arg_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
290        let status = self
291            .builtins
292            .get(name)
293            .unwrap()
294            .execute(&arg_refs, self.env);
295        self.env.exit_status = status;
296
297        self.restore_fds(saved_fds);
298        Ok(status)
299    }
300
301    /// Execute `eval` by joining args and re-parsing.
302    fn execute_eval(&mut self, args: &[String], redirects: &[Redirect]) -> ExecResult {
303        let saved_fds = self.save_and_apply_redirects(redirects)?;
304        let code = args.join(" ");
305        let status = self.eval_string(&code);
306        self.restore_fds(saved_fds);
307        Ok(status)
308    }
309
310    /// Execute `source`/`.` by reading a file and executing it.
311    fn execute_source(&mut self, args: &[String], redirects: &[Redirect]) -> ExecResult {
312        if args.is_empty() {
313            eprintln!("frost: source: filename argument required");
314            return Ok(1);
315        }
316        let saved_fds = self.save_and_apply_redirects(redirects)?;
317
318        let path = &args[0];
319        let code = match std::fs::read_to_string(path) {
320            Ok(c) => c,
321            Err(e) => {
322                eprintln!("frost: {path}: {e}");
323                self.restore_fds(saved_fds);
324                return Ok(1);
325            }
326        };
327
328        // Set positional params from remaining args.
329        let saved_params = if args.len() > 1 {
330            Some(std::mem::replace(
331                &mut self.env.positional_params,
332                args[1..].to_vec(),
333            ))
334        } else {
335            None
336        };
337
338        let status = self.eval_string(&code);
339
340        if let Some(params) = saved_params {
341            self.env.positional_params = params;
342        }
343
344        self.restore_fds(saved_fds);
345        Ok(status)
346    }
347
348    /// Execute `shift` — remove positional parameters from the front.
349    fn execute_shift(&mut self, args: &[String]) -> ExecResult {
350        let n = args
351            .first()
352            .and_then(|s| s.parse::<usize>().ok())
353            .unwrap_or(1);
354        if n <= self.env.positional_params.len() {
355            self.env.positional_params.drain(..n);
356            Ok(0)
357        } else {
358            eprintln!("shift: shift count must be <= $#");
359            Ok(1)
360        }
361    }
362
363    /// Parse and execute a string of shell code.
364    fn eval_string(&mut self, code: &str) -> i32 {
365        let tokens = tokenize(code);
366        let mut parser = frost_parser::Parser::new(&tokens);
367        let program = parser.parse();
368        self.execute_program(&program).unwrap_or(1)
369    }
370
371    /// Call a shell function.
372    fn call_function(
373        &mut self,
374        func: &FunctionDef,
375        args: &[String],
376        redirects: &[Redirect],
377    ) -> ExecResult {
378        let saved_fds = self.save_and_apply_redirects(redirects)?;
379
380        // Save and set positional parameters.
381        let saved_params = std::mem::replace(
382            &mut self.env.positional_params,
383            args.iter().map(|s| s.to_string()).collect(),
384        );
385
386        let status = self.execute_command(&func.body)?;
387
388        // Restore positional parameters.
389        self.env.positional_params = saved_params;
390        self.restore_fds(saved_fds);
391        self.env.exit_status = status;
392        Ok(status)
393    }
394
395    // ── Arithmetic (( ... )) ───────────────────────────────────
396
397    /// Execute a (( ... )) arithmetic command.
398    fn execute_arith_command(&mut self, args: &[String]) -> ExecResult {
399        // Collect the expression, stripping trailing ))
400        let expr: String = args
401            .iter()
402            .filter(|s| s.as_str() != "))")
403            .cloned()
404            .collect::<Vec<_>>()
405            .join(" ");
406
407        // Handle assignment/increment operators BEFORE expanding variables.
408        let result = self.eval_arith_with_assignment(expr.trim());
409        let status = if result == 0 { 1 } else { 0 }; // arithmetic: 0 is false, non-zero is true
410        self.env.exit_status = status;
411        Ok(status)
412    }
413
414    /// Evaluate arithmetic expression with assignment support (++, --, =, +=, -=).
415    /// NOTE: This receives the raw expression (before variable expansion) so that
416    /// it can identify variable names for assignment operators.
417    fn eval_arith_with_assignment(&mut self, expr: &str) -> i64 {
418        let expr = expr.trim();
419
420        // Handle var++ and var--
421        if expr.ends_with("++") && !expr.ends_with("+++") {
422            let var = expr[..expr.len() - 2].trim();
423            if is_valid_var_name(var) {
424                let val = self.env.get_var(var).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
425                self.env.set_var(var, &(val + 1).to_string());
426                return val; // post-increment returns old value
427            }
428        }
429        if expr.ends_with("--") && !expr.ends_with("---") {
430            let var = expr[..expr.len() - 2].trim();
431            if is_valid_var_name(var) {
432                let val = self.env.get_var(var).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
433                self.env.set_var(var, &(val - 1).to_string());
434                return val;
435            }
436        }
437
438        // Handle ++var and --var
439        if expr.starts_with("++ ") || (expr.starts_with("++") && expr.len() > 2 && expr.as_bytes()[2].is_ascii_alphabetic()) {
440            let var = expr.trim_start_matches("++").trim_start_matches(' ');
441            if is_valid_var_name(var) {
442                let val = self.env.get_var(var).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
443                let new_val = val + 1;
444                self.env.set_var(var, &new_val.to_string());
445                return new_val;
446            }
447        }
448        if expr.starts_with("-- ") || (expr.starts_with("--") && expr.len() > 2 && expr.as_bytes()[2].is_ascii_alphabetic()) {
449            let var = expr.trim_start_matches("--").trim_start_matches(' ');
450            if is_valid_var_name(var) {
451                let val = self.env.get_var(var).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
452                let new_val = val - 1;
453                self.env.set_var(var, &new_val.to_string());
454                return new_val;
455            }
456        }
457
458        // Handle var+=expr, var-=expr, var*=expr, var/=expr
459        for op in &["+=", "-=", "*=", "/=", "%="] {
460            if let Some(eq_pos) = expr.find(op) {
461                let var = expr[..eq_pos].trim();
462                if is_valid_var_name(var) {
463                    let rhs = expr[eq_pos + op.len()..].trim();
464                    let rhs_expanded = self.expand_arith_vars(rhs);
465                    let var_val = self.env.get_var(var).and_then(|v| v.parse::<i64>().ok()).unwrap_or(0);
466                    let rhs_val = eval_arith_expr(&rhs_expanded).unwrap_or(0);
467                    let new_val = match *op {
468                        "+=" => var_val + rhs_val,
469                        "-=" => var_val - rhs_val,
470                        "*=" => var_val * rhs_val,
471                        "/=" if rhs_val != 0 => var_val / rhs_val,
472                        "%=" if rhs_val != 0 => var_val % rhs_val,
473                        _ => var_val,
474                    };
475                    self.env.set_var(var, &new_val.to_string());
476                    return new_val;
477                }
478            }
479        }
480
481        // Handle simple assignment: var = expr (must check for == first)
482        // Look for single = that isn't part of ==, !=, <=, >=
483        if let Some(eq_pos) = find_assignment_eq(expr) {
484            let var = expr[..eq_pos].trim();
485            if is_valid_var_name(var) {
486                let rhs = expr[eq_pos + 1..].trim();
487                let rhs_expanded = self.expand_arith_vars(rhs);
488                let val = eval_arith_expr(&rhs_expanded).unwrap_or(0);
489                self.env.set_var(var, &val.to_string());
490                return val;
491            }
492        }
493
494        // Regular arithmetic evaluation with variable expansion.
495        let expanded = self.expand_arith_vars(expr);
496        eval_arith_expr(&expanded).unwrap_or(0)
497    }
498
499    // ── Conditional [[ ... ]] ──────────────────────────────────
500
501    /// Execute a [[ ... ]] conditional expression.
502    fn execute_cond_bracket(&mut self, args: &[String]) -> ExecResult {
503        // Strip trailing ]] if present.
504        let args: Vec<&str> = args
505            .iter()
506            .map(|s| s.as_str())
507            .filter(|s| *s != "]]")
508            .collect();
509
510        let result = eval_cond_expr(&args, self);
511        let status = if result { 0 } else { 1 };
512        self.env.exit_status = status;
513        Ok(status)
514    }
515
516    // ── Compound commands ────────────────────────────────────────
517
518    fn execute_subshell(&mut self, sub: &Subshell) -> ExecResult {
519        let saved_fds = self.save_and_apply_redirects(&sub.redirects)?;
520
521        match unsafe { sys::fork() }.map_err(ExecError::Fork)? {
522            sys::ForkOutcome::Child => {
523                let mut status = 0;
524                for cmd in &sub.body {
525                    status = self.execute_complete_command(cmd).unwrap_or(1);
526                }
527                std::process::exit(status);
528            }
529            sys::ForkOutcome::Parent { child_pid } => {
530                let status = match sys::wait_pid(child_pid).map_err(ExecError::Wait)? {
531                    sys::ChildStatus::Exited(code) => code,
532                    sys::ChildStatus::Signaled(code) => code,
533                    _ => 0,
534                };
535                self.restore_fds(saved_fds);
536                self.env.exit_status = status;
537                Ok(status)
538            }
539        }
540    }
541
542    fn execute_brace_group(&mut self, bg: &BraceGroup) -> ExecResult {
543        let saved_fds = self.save_and_apply_redirects(&bg.redirects)?;
544
545        let mut status = 0;
546        for cmd in &bg.body {
547            status = self.execute_complete_command(cmd)?;
548        }
549
550        self.restore_fds(saved_fds);
551        self.env.exit_status = status;
552        Ok(status)
553    }
554
555    fn execute_if(&mut self, clause: &IfClause) -> ExecResult {
556        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
557
558        // Evaluate condition.
559        let mut cond_status = 0;
560        for cmd in &clause.condition {
561            cond_status = self.execute_complete_command(cmd)?;
562        }
563
564        let status = if cond_status == 0 {
565            // then branch
566            let mut s = 0;
567            for cmd in &clause.then_body {
568                s = self.execute_complete_command(cmd)?;
569            }
570            s
571        } else {
572            // Try elif branches.
573            let mut found = false;
574            let mut s = 0;
575            for (elif_cond, elif_body) in &clause.elifs {
576                let mut elif_status = 0;
577                for cmd in elif_cond {
578                    elif_status = self.execute_complete_command(cmd)?;
579                }
580                if elif_status == 0 {
581                    for cmd in elif_body {
582                        s = self.execute_complete_command(cmd)?;
583                    }
584                    found = true;
585                    break;
586                }
587            }
588            if !found {
589                if let Some(else_body) = &clause.else_body {
590                    for cmd in else_body {
591                        s = self.execute_complete_command(cmd)?;
592                    }
593                }
594            }
595            s
596        };
597
598        self.restore_fds(saved_fds);
599        self.env.exit_status = status;
600        Ok(status)
601    }
602
603    fn execute_for(&mut self, clause: &ForClause) -> ExecResult {
604        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
605
606        let words = if let Some(word_list) = &clause.words {
607            word_list.iter().map(|w| self.expand_word(w)).collect()
608        } else {
609            self.env.positional_params.clone()
610        };
611
612        let mut status = 0;
613        for word in &words {
614            self.env.set_var(&clause.var, word);
615            for cmd in &clause.body {
616                status = self.execute_complete_command(cmd)?;
617            }
618        }
619
620        self.restore_fds(saved_fds);
621        self.env.exit_status = status;
622        Ok(status)
623    }
624
625    fn execute_arith_for(&mut self, clause: &ArithForClause) -> ExecResult {
626        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
627
628        // Execute the init expression.
629        if !clause.init.is_empty() {
630            self.eval_arith_with_assignment(&clause.init);
631        }
632
633        let mut status = 0;
634        loop {
635            // Evaluate the condition — empty condition means infinite loop (true).
636            if !clause.condition.is_empty() {
637                let cond_val = self.eval_arith_with_assignment(&clause.condition);
638                if cond_val == 0 {
639                    break;
640                }
641            }
642
643            // Execute the body.
644            for cmd in &clause.body {
645                status = self.execute_complete_command(cmd)?;
646            }
647
648            // Execute the step expression.
649            if !clause.step.is_empty() {
650                self.eval_arith_with_assignment(&clause.step);
651            }
652        }
653
654        self.restore_fds(saved_fds);
655        self.env.exit_status = status;
656        Ok(status)
657    }
658
659    fn execute_while(&mut self, clause: &WhileClause) -> ExecResult {
660        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
661
662        let mut status = 0;
663        loop {
664            let mut cond_status = 0;
665            for cmd in &clause.condition {
666                cond_status = self.execute_complete_command(cmd)?;
667            }
668            if cond_status != 0 {
669                break;
670            }
671            for cmd in &clause.body {
672                status = self.execute_complete_command(cmd)?;
673            }
674        }
675
676        self.restore_fds(saved_fds);
677        self.env.exit_status = status;
678        Ok(status)
679    }
680
681    fn execute_until(&mut self, clause: &UntilClause) -> ExecResult {
682        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
683
684        let mut status = 0;
685        loop {
686            let mut cond_status = 0;
687            for cmd in &clause.condition {
688                cond_status = self.execute_complete_command(cmd)?;
689            }
690            if cond_status == 0 {
691                break;
692            }
693            for cmd in &clause.body {
694                status = self.execute_complete_command(cmd)?;
695            }
696        }
697
698        self.restore_fds(saved_fds);
699        self.env.exit_status = status;
700        Ok(status)
701    }
702
703    fn execute_repeat(&mut self, clause: &RepeatClause) -> ExecResult {
704        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
705
706        let count_str = self.expand_word(&clause.count);
707        let count: i64 = count_str.trim().parse().unwrap_or(0);
708
709        let mut status = 0;
710        for _ in 0..count {
711            for cmd in &clause.body {
712                status = self.execute_complete_command(cmd)?;
713            }
714        }
715
716        self.restore_fds(saved_fds);
717        self.env.exit_status = status;
718        Ok(status)
719    }
720
721    fn execute_always(&mut self, clause: &AlwaysClause) -> ExecResult {
722        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
723
724        // Execute the try body, capturing any error/status.
725        let try_result = (|| {
726            let mut status = 0;
727            for cmd in &clause.try_body {
728                status = self.execute_complete_command(cmd)?;
729            }
730            Ok(status)
731        })();
732
733        // Always execute the always body regardless of try body outcome.
734        let mut always_status = 0;
735        for cmd in &clause.always_body {
736            always_status = self.execute_complete_command(cmd)?;
737        }
738
739        self.restore_fds(saved_fds);
740
741        // If the try body had an execution error (not just non-zero exit),
742        // propagate it. Otherwise, the final status is from the always body.
743        match try_result {
744            Ok(_try_status) => {
745                self.env.exit_status = always_status;
746                Ok(always_status)
747            }
748            Err(e) => {
749                // The always body already ran; propagate the original error.
750                self.env.exit_status = always_status;
751                Err(e)
752            }
753        }
754    }
755
756    fn execute_case(&mut self, clause: &CaseClause) -> ExecResult {
757        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
758
759        let word = self.expand_word(&clause.word);
760        let mut status = 0;
761
762        let mut i = 0;
763        while i < clause.items.len() {
764            let item = &clause.items[i];
765            let matched = item
766                .patterns
767                .iter()
768                .any(|p| glob_match_word(&self.expand_word(p), &word));
769
770            if matched {
771                for cmd in &item.body {
772                    status = self.execute_complete_command(cmd)?;
773                }
774                match item.terminator {
775                    CaseTerminator::DoubleSemi => break,
776                    CaseTerminator::SemiAnd => {
777                        // ;& — fall through to next item body unconditionally
778                        i += 1;
779                        if i < clause.items.len() {
780                            for cmd in &clause.items[i].body {
781                                status = self.execute_complete_command(cmd)?;
782                            }
783                        }
784                        break;
785                    }
786                    CaseTerminator::SemiPipe => {
787                        // ;;| — continue testing patterns
788                        i += 1;
789                        continue;
790                    }
791                }
792            }
793            i += 1;
794        }
795
796        self.restore_fds(saved_fds);
797        self.env.exit_status = status;
798        Ok(status)
799    }
800
801    fn execute_select(&mut self, clause: &SelectClause) -> ExecResult {
802        let saved_fds = self.save_and_apply_redirects(&clause.redirects)?;
803
804        let words: Vec<String> = if let Some(word_list) = &clause.words {
805            word_list.iter().map(|w| self.expand_word(w)).collect()
806        } else {
807            self.env.positional_params.clone()
808        };
809
810        // Print menu and read REPLY. Simplified — just run body once per word.
811        let mut status = 0;
812        for (i, word) in words.iter().enumerate() {
813            eprintln!("{}) {word}", i + 1);
814        }
815        // Set var to empty and run body once (simplified select).
816        self.env.set_var(&clause.var, "");
817        for cmd in &clause.body {
818            status = self.execute_complete_command(cmd)?;
819        }
820
821        self.restore_fds(saved_fds);
822        self.env.exit_status = status;
823        Ok(status)
824    }
825
826    fn execute_function_def(&mut self, fdef: &FunctionDef) -> ExecResult {
827        self.env.functions.insert(fdef.name.to_string(), fdef.clone());
828        Ok(0)
829    }
830
831    // ── Word expansion ──────────────────────────────────────────
832
833    /// Expand a Word AST node into a plain string, performing all expansions.
834    pub fn expand_word(&mut self, word: &Word) -> String {
835        let mut out = String::new();
836        for part in &word.parts {
837            self.expand_word_part(part, &mut out);
838        }
839        out
840    }
841
842    /// Expand a word, performing glob expansion if the word contains glob characters.
843    /// Returns multiple words if glob matches are found.
844    fn expand_word_glob(&mut self, word: &Word) -> Vec<String> {
845        let has_glob = word.parts.iter().any(|p| matches!(p, WordPart::Glob(_)));
846        let expanded = self.expand_word(word);
847        if has_glob {
848            let mut matches: Vec<String> = glob::glob(&expanded)
849                .ok()
850                .map(|paths| {
851                    paths
852                        .filter_map(|p| p.ok())
853                        .map(|p| p.to_string_lossy().into_owned())
854                        .collect()
855                })
856                .unwrap_or_default();
857            if matches.is_empty() {
858                // No matches — return the pattern literally (zsh behavior depends on options).
859                vec![expanded]
860            } else {
861                matches.sort();
862                matches
863            }
864        } else {
865            vec![expanded]
866        }
867    }
868
869    fn expand_word_part(&mut self, part: &WordPart, out: &mut String) {
870        match part {
871            WordPart::Literal(s) => out.push_str(s),
872            WordPart::SingleQuoted(s) => out.push_str(s),
873            WordPart::DoubleQuoted(parts) => {
874                for inner in parts {
875                    self.expand_word_part(inner, out);
876                }
877            }
878            WordPart::DollarVar(name) => {
879                out.push_str(&self.expand_special_var(name));
880            }
881            WordPart::DollarBrace {
882                param,
883                operator,
884                arg,
885            } => {
886                let base = self.expand_special_var(param);
887                if let Some(op) = operator {
888                    let arg_val = arg
889                        .as_ref()
890                        .map(|w| self.expand_word(w))
891                        .unwrap_or_default();
892                    out.push_str(&apply_param_op(&base, op, &arg_val));
893                } else {
894                    out.push_str(&base);
895                }
896            }
897            WordPart::CommandSub(program) => {
898                out.push_str(&self.execute_command_sub(program));
899            }
900            WordPart::ArithSub(expr) => {
901                // Basic arithmetic evaluation.
902                out.push_str(&self.eval_arith(expr));
903            }
904            WordPart::Tilde(user) => {
905                if user.is_empty() {
906                    if let Some(home) = self.env.get_var("HOME") {
907                        out.push_str(home);
908                    } else {
909                        out.push('~');
910                    }
911                } else {
912                    out.push('~');
913                    out.push_str(user);
914                }
915            }
916            WordPart::Glob(_) => {
917                // In command context, globs should be expanded via filesystem.
918                // For now, pass through the literal character.
919                match part {
920                    WordPart::Glob(frost_parser::ast::GlobKind::Star) => out.push('*'),
921                    WordPart::Glob(frost_parser::ast::GlobKind::Question) => out.push('?'),
922                    WordPart::Glob(frost_parser::ast::GlobKind::At) => out.push('@'),
923                    _ => {}
924                }
925            }
926        }
927    }
928
929    /// Expand special shell variables: $?, $$, $#, $@, $*, $0..$9, named vars.
930    fn expand_special_var(&self, name: &str) -> String {
931        match name {
932            "?" => self.env.exit_status.to_string(),
933            "$" => self.env.pid.to_string(),
934            "#" => self.env.positional_params.len().to_string(),
935            "@" | "*" => self.env.positional_params.join(" "),
936            "0" => "frost".to_string(),
937            "LINENO" => "0".to_string(),
938            _ => {
939                // Check if it's a positional parameter (1-9).
940                if let Ok(n) = name.parse::<usize>() {
941                    if n >= 1 {
942                        return self
943                            .env
944                            .positional_params
945                            .get(n - 1)
946                            .cloned()
947                            .unwrap_or_default();
948                    }
949                }
950                self.env.get_var(name).unwrap_or("").to_string()
951            }
952        }
953    }
954
955    /// Execute a command substitution and return its stdout.
956    fn execute_command_sub(&mut self, program: &Program) -> String {
957        // Reconstruct the command text from the AST and run it via
958        // `frost -c` in a subprocess. This avoids complex fd/fork issues
959        // when builtins use Rust stdio (which buffers above the fd level).
960        //
961        // For simple single-command programs, we can reconstruct the text.
962        // For complex ones, we serialize via the AST.
963        let code = reconstruct_program(program);
964        if code.is_empty() {
965            return String::new();
966        }
967
968        let frost_path = std::env::current_exe().unwrap_or_else(|_| "frost".into());
969        let mut cmd = std::process::Command::new(&frost_path);
970        cmd.arg("-c").arg(&code);
971        cmd.stdout(std::process::Stdio::piped());
972        cmd.stderr(std::process::Stdio::piped());
973        cmd.stdin(std::process::Stdio::null());
974
975        // Pass the current environment.
976        cmd.env_clear();
977        for (name, var) in &self.env.variables {
978            if var.export {
979                cmd.env(name, &var.value);
980            }
981        }
982
983        match cmd.output() {
984            Ok(output) => {
985                let mut stdout = String::from_utf8_lossy(&output.stdout).into_owned();
986                // Strip trailing newlines (shell convention).
987                while stdout.ends_with('\n') {
988                    stdout.pop();
989                }
990                stdout
991            }
992            Err(_) => String::new(),
993        }
994    }
995
996    /// Basic arithmetic evaluation.
997    fn eval_arith(&self, expr: &str) -> String {
998        // Expand variables in the expression first.
999        let expanded = self.expand_arith_vars(expr);
1000        // Evaluate the numeric expression.
1001        match eval_arith_expr(&expanded) {
1002            Some(val) => val.to_string(),
1003            None => "0".to_string(),
1004        }
1005    }
1006
1007    /// Replace variable names in arithmetic expressions with their values.
1008    fn expand_arith_vars(&self, expr: &str) -> String {
1009        let mut out = String::new();
1010        let chars: Vec<char> = expr.chars().collect();
1011        let mut i = 0;
1012        while i < chars.len() {
1013            if chars[i] == '$' {
1014                i += 1;
1015                let start = i;
1016                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1017                    i += 1;
1018                }
1019                let name: String = chars[start..i].iter().collect();
1020                out.push_str(&self.expand_special_var(&name));
1021            } else if chars[i].is_alphabetic() || chars[i] == '_' {
1022                let start = i;
1023                while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
1024                    i += 1;
1025                }
1026                let name: String = chars[start..i].iter().collect();
1027                // In arithmetic context, bare words are variable names.
1028                if let Some(val) = self.env.get_var(&name) {
1029                    out.push_str(val);
1030                } else {
1031                    out.push('0');
1032                }
1033            } else {
1034                out.push(chars[i]);
1035                i += 1;
1036            }
1037        }
1038        out
1039    }
1040
1041    // ── Redirect helpers ─────────────────────────────────────────
1042
1043    /// Save current fds and apply redirects. Returns saved fd pairs for restore.
1044    fn save_and_apply_redirects(
1045        &mut self,
1046        redirects: &[Redirect],
1047    ) -> Result<Vec<(RawFd, RawFd)>, ExecError> {
1048        if redirects.is_empty() {
1049            return Ok(Vec::new());
1050        }
1051
1052        // Pre-expand redirect targets (needed for herestrings with variable expansion).
1053        let expanded_targets: Vec<String> = redirects
1054            .iter()
1055            .map(|r| self.expand_word(&r.target))
1056            .collect();
1057
1058        let mut saved = Vec::new();
1059        for redir in redirects {
1060            let target_fd = redirect::target_fd_for(redir);
1061            // Save the current fd by duping it.
1062            if let Ok(saved_fd) = nix::unistd::dup(target_fd) {
1063                use std::os::fd::IntoRawFd;
1064                saved.push((target_fd, saved_fd.into_raw_fd()));
1065            }
1066        }
1067        redirect::apply_redirects_expanded(redirects, &expanded_targets)?;
1068        Ok(saved)
1069    }
1070
1071    /// Restore saved file descriptors.
1072    fn restore_fds(&self, saved: Vec<(RawFd, RawFd)>) {
1073        for (target_fd, saved_fd) in saved {
1074            sys::dup2(saved_fd, target_fd).ok();
1075            sys::close(saved_fd).ok();
1076        }
1077    }
1078
1079    /// Fork a child process and exec an external command.
1080    fn fork_exec(
1081        &mut self,
1082        argv: &[String],
1083        redirects: &[Redirect],
1084    ) -> ExecResult {
1085        let c_argv: Vec<CString> = argv
1086            .iter()
1087            .filter_map(|a| CString::new(a.as_bytes()).ok())
1088            .collect();
1089
1090        let c_envp = self.env.to_env_vec();
1091
1092        // Pre-expand redirect targets for herestrings/heredocs.
1093        let expanded_targets: Vec<String> = redirects
1094            .iter()
1095            .map(|r| self.expand_word(&r.target))
1096            .collect();
1097
1098        match unsafe { sys::fork() }.map_err(ExecError::Fork)? {
1099            sys::ForkOutcome::Child => {
1100                if let Err(e) = redirect::apply_redirects_expanded(redirects, &expanded_targets) {
1101                    eprintln!("frost: {e}");
1102                    std::process::exit(1);
1103                }
1104
1105                let err = sys::exec(&c_argv, &c_envp);
1106                eprintln!("frost: {}: {err}", argv[0]);
1107                std::process::exit(if err == nix::errno::Errno::ENOENT {
1108                    127
1109                } else {
1110                    126
1111                });
1112            }
1113            sys::ForkOutcome::Parent { child_pid } => {
1114                match sys::wait_pid(child_pid).map_err(ExecError::Wait)? {
1115                    sys::ChildStatus::Exited(code) => {
1116                        self.env.exit_status = code;
1117                        Ok(code)
1118                    }
1119                    sys::ChildStatus::Signaled(code) => {
1120                        self.env.exit_status = code;
1121                        Ok(code)
1122                    }
1123                    _ => Ok(0),
1124                }
1125            }
1126        }
1127    }
1128}
1129
1130// ── Free functions ───────────────────────────────────────────────────
1131
1132/// Invert exit status for `!` pipelines: 0 -> 1, non-zero -> 0.
1133fn invert(status: i32) -> i32 {
1134    if status == 0 { 1 } else { 0 }
1135}
1136
1137/// Check if a string is a valid shell variable name.
1138fn is_valid_var_name(s: &str) -> bool {
1139    !s.is_empty()
1140        && (s.as_bytes()[0].is_ascii_alphabetic() || s.as_bytes()[0] == b'_')
1141        && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
1142}
1143
1144/// Find the position of a simple assignment `=` in an arithmetic expression.
1145/// Skips `==`, `!=`, `<=`, `>=`, `+=`, `-=`, `*=`, `/=`, `%=`.
1146fn find_assignment_eq(expr: &str) -> Option<usize> {
1147    let bytes = expr.as_bytes();
1148    for i in 0..bytes.len() {
1149        if bytes[i] == b'=' {
1150            // Skip ==
1151            if i + 1 < bytes.len() && bytes[i + 1] == b'=' {
1152                continue;
1153            }
1154            // Skip !=, <=, >=, +=, -=, *=, /=, %=
1155            if i > 0
1156                && matches!(
1157                    bytes[i - 1],
1158                    b'!' | b'<' | b'>' | b'+' | b'-' | b'*' | b'/' | b'%'
1159                )
1160            {
1161                continue;
1162            }
1163            return Some(i);
1164        }
1165    }
1166    None
1167}
1168
1169/// Evaluate a [[ ... ]] conditional expression.
1170/// Supports:
1171///   - Unary: -n, -z, -f, -d, -e, -r, -w, -x, -s, -L, -h, -t, -b, -c, -p, -S, -o, -v
1172///   - Binary: =, ==, !=, -eq, -ne, -lt, -le, -gt, -ge, =~, <, >
1173///   - Logical: &&, ||, !
1174///   - Grouping: ( ... )
1175fn eval_cond_expr(args: &[&str], exec: &Executor<'_>) -> bool {
1176    if args.is_empty() {
1177        return false;
1178    }
1179
1180    // Simple dispatcher based on arg count and structure.
1181    let mut pos = 0;
1182    eval_cond_or(args, &mut pos, exec)
1183}
1184
1185fn eval_cond_or(args: &[&str], pos: &mut usize, exec: &Executor<'_>) -> bool {
1186    let mut result = eval_cond_and(args, pos, exec);
1187    while *pos < args.len() && args[*pos] == "||" {
1188        *pos += 1;
1189        let right = eval_cond_and(args, pos, exec);
1190        result = result || right;
1191    }
1192    result
1193}
1194
1195fn eval_cond_and(args: &[&str], pos: &mut usize, exec: &Executor<'_>) -> bool {
1196    let mut result = eval_cond_not(args, pos, exec);
1197    while *pos < args.len() && args[*pos] == "&&" {
1198        *pos += 1;
1199        let right = eval_cond_not(args, pos, exec);
1200        result = result && right;
1201    }
1202    result
1203}
1204
1205fn eval_cond_not(args: &[&str], pos: &mut usize, exec: &Executor<'_>) -> bool {
1206    if *pos < args.len() && args[*pos] == "!" {
1207        *pos += 1;
1208        return !eval_cond_primary(args, pos, exec);
1209    }
1210    eval_cond_primary(args, pos, exec)
1211}
1212
1213fn eval_cond_primary(args: &[&str], pos: &mut usize, exec: &Executor<'_>) -> bool {
1214    if *pos >= args.len() {
1215        return false;
1216    }
1217
1218    // Grouping: ( expr )
1219    if args[*pos] == "(" {
1220        *pos += 1;
1221        let result = eval_cond_or(args, pos, exec);
1222        if *pos < args.len() && args[*pos] == ")" {
1223            *pos += 1;
1224        }
1225        return result;
1226    }
1227
1228    // Unary operators
1229    if args[*pos].starts_with('-') && args[*pos].len() == 2 && *pos + 1 < args.len() {
1230        let op = args[*pos];
1231        // Check if next arg is a binary operator — if so, this isn't a unary test
1232        if *pos + 2 < args.len() {
1233            let next_next = args[*pos + 2];
1234            // If the arg after the next is a known binary op, treat this as a value
1235            if is_binary_cond_op(args[*pos + 1]) {
1236                // Fall through to binary handling below
1237            } else {
1238                return eval_unary_cond(op, args, pos);
1239            }
1240        } else {
1241            return eval_unary_cond(op, args, pos);
1242        }
1243    }
1244
1245    // If only one arg remains, -n test (true if non-empty)
1246    if *pos + 1 >= args.len() || !is_binary_cond_op_or_end(args.get(*pos + 1).copied()) {
1247        let val = args[*pos];
1248        *pos += 1;
1249        return !val.is_empty();
1250    }
1251
1252    // Binary operators
1253    let left = args[*pos];
1254    *pos += 1;
1255    let op = args[*pos];
1256    *pos += 1;
1257    let right = if *pos < args.len() {
1258        let r = args[*pos];
1259        *pos += 1;
1260        r
1261    } else {
1262        ""
1263    };
1264
1265    match op {
1266        "=" | "==" => left == right,
1267        "!=" => left != right,
1268        "-eq" => left.parse::<i64>().unwrap_or(0) == right.parse::<i64>().unwrap_or(0),
1269        "-ne" => left.parse::<i64>().unwrap_or(0) != right.parse::<i64>().unwrap_or(0),
1270        "-lt" => left.parse::<i64>().unwrap_or(0) < right.parse::<i64>().unwrap_or(0),
1271        "-le" => left.parse::<i64>().unwrap_or(0) <= right.parse::<i64>().unwrap_or(0),
1272        "-gt" => left.parse::<i64>().unwrap_or(0) > right.parse::<i64>().unwrap_or(0),
1273        "-ge" => left.parse::<i64>().unwrap_or(0) >= right.parse::<i64>().unwrap_or(0),
1274        "<" => left < right,
1275        ">" => left > right,
1276        "=~" => {
1277            // Regex match — basic implementation
1278            glob_match_word(right, left)
1279        }
1280        _ => false,
1281    }
1282}
1283
1284fn eval_unary_cond(op: &str, args: &[&str], pos: &mut usize) -> bool {
1285    *pos += 1; // skip operator
1286    let arg = if *pos < args.len() {
1287        let a = args[*pos];
1288        *pos += 1;
1289        a
1290    } else {
1291        return false;
1292    };
1293
1294    match op {
1295        "-n" => !arg.is_empty(),
1296        "-z" => arg.is_empty(),
1297        "-f" => std::path::Path::new(arg).is_file(),
1298        "-d" => std::path::Path::new(arg).is_dir(),
1299        "-e" => std::path::Path::new(arg).exists(),
1300        "-s" => std::fs::metadata(arg).map(|m| m.len() > 0).unwrap_or(false),
1301        "-r" | "-w" | "-x" => std::path::Path::new(arg).exists(), // simplified
1302        "-L" | "-h" => std::fs::symlink_metadata(arg)
1303            .map(|m| m.file_type().is_symlink())
1304            .unwrap_or(false),
1305        "-b" | "-c" | "-p" | "-S" => false, // special file types — simplified
1306        "-t" => false, // is a tty
1307        "-o" => false, // option set — simplified
1308        "-v" => false, // variable set — can't check through free function
1309        _ => false,
1310    }
1311}
1312
1313fn is_binary_cond_op(s: &str) -> bool {
1314    matches!(
1315        s,
1316        "=" | "==" | "!=" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" | "<" | ">" | "=~"
1317    )
1318}
1319
1320fn is_binary_cond_op_or_end(s: Option<&str>) -> bool {
1321    match s {
1322        Some(s) => is_binary_cond_op(s) || s == "&&" || s == "||" || s == ")" || s == "]]",
1323        None => true,
1324    }
1325}
1326
1327/// Apply a parameter expansion operator.
1328fn apply_param_op(value: &str, op: &str, arg: &str) -> String {
1329    match op {
1330        ":-" => {
1331            if value.is_empty() {
1332                arg.to_string()
1333            } else {
1334                value.to_string()
1335            }
1336        }
1337        "-" => {
1338            // ${param-word} — use word only if param is unset (we can't distinguish
1339            // unset from empty here, treat same as :-)
1340            if value.is_empty() {
1341                arg.to_string()
1342            } else {
1343                value.to_string()
1344            }
1345        }
1346        ":+" => {
1347            if value.is_empty() {
1348                String::new()
1349            } else {
1350                arg.to_string()
1351            }
1352        }
1353        "+" => {
1354            if value.is_empty() {
1355                String::new()
1356            } else {
1357                arg.to_string()
1358            }
1359        }
1360        "#" => {
1361            // ${param#pattern} — remove shortest prefix match
1362            for i in 0..=value.len() {
1363                if glob_match_word(arg, &value[..i]) {
1364                    return value[i..].to_string();
1365                }
1366            }
1367            value.to_string()
1368        }
1369        "##" => {
1370            // ${param##pattern} — remove longest prefix match
1371            for i in (0..=value.len()).rev() {
1372                if glob_match_word(arg, &value[..i]) {
1373                    return value[i..].to_string();
1374                }
1375            }
1376            value.to_string()
1377        }
1378        "%" => {
1379            // ${param%pattern} — remove shortest suffix match
1380            for i in (0..=value.len()).rev() {
1381                if glob_match_word(arg, &value[i..]) {
1382                    return value[..i].to_string();
1383                }
1384            }
1385            value.to_string()
1386        }
1387        "%%" => {
1388            // ${param%%pattern} — remove longest suffix match
1389            for i in 0..=value.len() {
1390                if glob_match_word(arg, &value[i..]) {
1391                    return value[..i].to_string();
1392                }
1393            }
1394            value.to_string()
1395        }
1396        "length" => {
1397            // ${#param} — string length
1398            value.len().to_string()
1399        }
1400        "/" => {
1401            // ${param/pattern/replacement} — first substitution
1402            if let Some(slash_pos) = arg.find('/') {
1403                let pattern = &arg[..slash_pos];
1404                let replacement = &arg[slash_pos + 1..];
1405                if let Some(pos) = find_glob_match(value, pattern) {
1406                    let match_len = glob_match_length(value, pos, pattern);
1407                    let mut result = String::new();
1408                    result.push_str(&value[..pos]);
1409                    result.push_str(replacement);
1410                    result.push_str(&value[pos + match_len..]);
1411                    result
1412                } else {
1413                    value.to_string()
1414                }
1415            } else {
1416                // ${param/pattern} — remove first match
1417                if let Some(pos) = find_glob_match(value, arg) {
1418                    let match_len = glob_match_length(value, pos, arg);
1419                    let mut result = String::new();
1420                    result.push_str(&value[..pos]);
1421                    result.push_str(&value[pos + match_len..]);
1422                    result
1423                } else {
1424                    value.to_string()
1425                }
1426            }
1427        }
1428        "//" => {
1429            // ${param//pattern/replacement} — global substitution
1430            if let Some(slash_pos) = arg.find('/') {
1431                let pattern = &arg[..slash_pos];
1432                let replacement = &arg[slash_pos + 1..];
1433                let mut result = String::new();
1434                let mut i = 0;
1435                let chars: Vec<char> = value.chars().collect();
1436                while i < chars.len() {
1437                    let remaining: String = chars[i..].iter().collect();
1438                    if glob_match_word(pattern, &remaining[..glob_match_length(&remaining, 0, pattern).max(1).min(remaining.len())]) {
1439                        let mlen = glob_match_length(&remaining, 0, pattern);
1440                        result.push_str(replacement);
1441                        i += mlen.max(1);
1442                    } else {
1443                        result.push(chars[i]);
1444                        i += 1;
1445                    }
1446                }
1447                result
1448            } else {
1449                value.to_string()
1450            }
1451        }
1452        ":="|"=" => {
1453            // ${param:=word} — assign default (handled in executor, not here)
1454            if value.is_empty() {
1455                arg.to_string()
1456            } else {
1457                value.to_string()
1458            }
1459        }
1460        ":?" | "?" => {
1461            // ${param:?msg} — error if unset/empty
1462            if value.is_empty() {
1463                let msg = if arg.is_empty() { "parameter not set" } else { arg };
1464                eprintln!("frost: {msg}");
1465                value.to_string()
1466            } else {
1467                value.to_string()
1468            }
1469        }
1470        _ => value.to_string(),
1471    }
1472}
1473
1474/// Find position of first glob match in text.
1475fn find_glob_match(text: &str, pattern: &str) -> Option<usize> {
1476    for i in 0..text.len() {
1477        for j in (i + 1)..=text.len() {
1478            if glob_match_word(pattern, &text[i..j]) {
1479                return Some(i);
1480            }
1481        }
1482    }
1483    None
1484}
1485
1486/// Find the length of a glob match starting at position.
1487fn glob_match_length(text: &str, start: usize, pattern: &str) -> usize {
1488    let mut longest = 1;
1489    for j in (start + 1)..=text.len() {
1490        if glob_match_word(pattern, &text[start..j]) {
1491            longest = j - start;
1492        }
1493    }
1494    longest
1495}
1496
1497/// Simple glob matching for case patterns and parameter expansion.
1498fn glob_match_word(pattern: &str, text: &str) -> bool {
1499    let pat: Vec<char> = pattern.chars().collect();
1500    let txt: Vec<char> = text.chars().collect();
1501    glob_match_chars(&pat, &txt)
1502}
1503
1504fn glob_match_chars(pat: &[char], txt: &[char]) -> bool {
1505    if pat.is_empty() {
1506        return txt.is_empty();
1507    }
1508    match pat[0] {
1509        '*' => {
1510            for i in 0..=txt.len() {
1511                if glob_match_chars(&pat[1..], &txt[i..]) {
1512                    return true;
1513                }
1514            }
1515            false
1516        }
1517        '?' => {
1518            if txt.is_empty() {
1519                false
1520            } else {
1521                glob_match_chars(&pat[1..], &txt[1..])
1522            }
1523        }
1524        '\\' if pat.len() > 1 => {
1525            if txt.is_empty() || txt[0] != pat[1] {
1526                false
1527            } else {
1528                glob_match_chars(&pat[2..], &txt[1..])
1529            }
1530        }
1531        c => {
1532            if txt.is_empty() || txt[0] != c {
1533                false
1534            } else {
1535                glob_match_chars(&pat[1..], &txt[1..])
1536            }
1537        }
1538    }
1539}
1540
1541/// Evaluate a simple arithmetic expression (integers, +, -, *, /, %, comparisons).
1542fn eval_arith_expr(expr: &str) -> Option<i64> {
1543    let expr = expr.trim();
1544    if expr.is_empty() {
1545        return Some(0);
1546    }
1547
1548    // Try to parse as a plain integer first.
1549    if let Ok(n) = expr.parse::<i64>() {
1550        return Some(n);
1551    }
1552
1553    // Handle comparison operators (lowest precedence).
1554    for &op in &["==", "!=", "<=", ">=", "<", ">"] {
1555        if let Some(pos) = find_binary_op(expr, op) {
1556            let left = eval_arith_expr(&expr[..pos])?;
1557            let right = eval_arith_expr(&expr[pos + op.len()..])?;
1558            return Some(match op {
1559                "==" => (left == right) as i64,
1560                "!=" => (left != right) as i64,
1561                "<=" => (left <= right) as i64,
1562                ">=" => (left >= right) as i64,
1563                "<" => (left < right) as i64,
1564                ">" => (left > right) as i64,
1565                _ => 0,
1566            });
1567        }
1568    }
1569
1570    // Handle + and - (left to right).
1571    if let Some(pos) = find_last_additive(expr) {
1572        let left = eval_arith_expr(&expr[..pos])?;
1573        let right = eval_arith_expr(&expr[pos + 1..])?;
1574        return Some(if expr.as_bytes()[pos] == b'+' {
1575            left + right
1576        } else {
1577            left - right
1578        });
1579    }
1580
1581    // Handle * / % (left to right).
1582    if let Some(pos) = find_last_multiplicative(expr) {
1583        let left = eval_arith_expr(&expr[..pos])?;
1584        let right = eval_arith_expr(&expr[pos + 1..])?;
1585        return Some(match expr.as_bytes()[pos] {
1586            b'*' => left * right,
1587            b'/' if right != 0 => left / right,
1588            b'%' if right != 0 => left % right,
1589            _ => 0,
1590        });
1591    }
1592
1593    // Handle parentheses.
1594    if expr.starts_with('(') && expr.ends_with(')') {
1595        return eval_arith_expr(&expr[1..expr.len() - 1]);
1596    }
1597
1598    // Handle unary minus.
1599    if expr.starts_with('-') {
1600        return eval_arith_expr(&expr[1..]).map(|v| -v);
1601    }
1602
1603    // Handle unary plus.
1604    if expr.starts_with('+') {
1605        return eval_arith_expr(&expr[1..]);
1606    }
1607
1608    // Handle logical not.
1609    if expr.starts_with('!') {
1610        return eval_arith_expr(&expr[1..]).map(|v| if v == 0 { 1 } else { 0 });
1611    }
1612
1613    None
1614}
1615
1616/// Find the last + or - at the top level (not inside parens).
1617fn find_last_additive(expr: &str) -> Option<usize> {
1618    let bytes = expr.as_bytes();
1619    let mut depth = 0i32;
1620    let mut last = None;
1621    for (i, &b) in bytes.iter().enumerate() {
1622        match b {
1623            b'(' => depth += 1,
1624            b')' => depth -= 1,
1625            b'+' | b'-' if depth == 0 && i > 0 => {
1626                // Don't match if preceded by another operator.
1627                let prev = bytes[i - 1];
1628                if prev != b'*' && prev != b'/' && prev != b'%' && prev != b'('
1629                    && prev != b'+' && prev != b'-'
1630                {
1631                    last = Some(i);
1632                }
1633            }
1634            _ => {}
1635        }
1636    }
1637    last
1638}
1639
1640/// Find the last * / % at the top level.
1641fn find_last_multiplicative(expr: &str) -> Option<usize> {
1642    let bytes = expr.as_bytes();
1643    let mut depth = 0i32;
1644    let mut last = None;
1645    for (i, &b) in bytes.iter().enumerate() {
1646        match b {
1647            b'(' => depth += 1,
1648            b')' => depth -= 1,
1649            b'*' | b'/' | b'%' if depth == 0 => {
1650                // Make sure it's not part of ** or ==
1651                if b == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
1652                    continue;
1653                }
1654                last = Some(i);
1655            }
1656            _ => {}
1657        }
1658    }
1659    last
1660}
1661
1662/// Find position of a binary operator string at the top level.
1663fn find_binary_op(expr: &str, op: &str) -> Option<usize> {
1664    let bytes = expr.as_bytes();
1665    let op_bytes = op.as_bytes();
1666    let mut depth = 0i32;
1667    for i in 0..bytes.len() {
1668        match bytes[i] {
1669            b'(' => depth += 1,
1670            b')' => depth -= 1,
1671            _ if depth == 0 && i + op_bytes.len() <= bytes.len() => {
1672                if &bytes[i..i + op_bytes.len()] == op_bytes && i > 0 {
1673                    return Some(i);
1674                }
1675            }
1676            _ => {}
1677        }
1678    }
1679    None
1680}
1681
1682/// Reconstruct shell code from a Program AST.
1683/// This is used for command substitution where we need to run
1684/// the code in a subprocess via `frost -c`.
1685fn reconstruct_program(program: &Program) -> String {
1686    let mut parts = Vec::new();
1687    for cmd in &program.commands {
1688        parts.push(reconstruct_complete_command(cmd));
1689    }
1690    parts.join("; ")
1691}
1692
1693fn reconstruct_complete_command(cmd: &CompleteCommand) -> String {
1694    let mut s = reconstruct_list(&cmd.list);
1695    if cmd.is_async {
1696        s.push_str(" &");
1697    }
1698    s
1699}
1700
1701fn reconstruct_list(list: &List) -> String {
1702    let mut s = reconstruct_pipeline(&list.first);
1703    for (op, pipeline) in &list.rest {
1704        match op {
1705            ListOp::And => s.push_str(" && "),
1706            ListOp::Or => s.push_str(" || "),
1707        }
1708        s.push_str(&reconstruct_pipeline(pipeline));
1709    }
1710    s
1711}
1712
1713fn reconstruct_pipeline(pipeline: &Pipeline) -> String {
1714    let mut s = String::new();
1715    if pipeline.bang {
1716        s.push_str("! ");
1717    }
1718    let cmd_strs: Vec<String> = pipeline.commands.iter().map(reconstruct_command).collect();
1719    s.push_str(&cmd_strs.join(" | "));
1720    s
1721}
1722
1723fn reconstruct_command(cmd: &Command) -> String {
1724    match cmd {
1725        Command::Simple(simple) => {
1726            let mut parts = Vec::new();
1727            for assign in &simple.assignments {
1728                let val = assign.value.as_ref().map(reconstruct_word).unwrap_or_default();
1729                parts.push(format!("{}={}", assign.name, val));
1730            }
1731            for word in &simple.words {
1732                parts.push(reconstruct_word(word));
1733            }
1734            for redir in &simple.redirects {
1735                parts.push(reconstruct_redirect(redir));
1736            }
1737            parts.join(" ")
1738        }
1739        Command::Subshell(sub) => {
1740            let body: Vec<String> = sub.body.iter().map(reconstruct_complete_command).collect();
1741            format!("( {} )", body.join("; "))
1742        }
1743        Command::BraceGroup(bg) => {
1744            let body: Vec<String> = bg.body.iter().map(reconstruct_complete_command).collect();
1745            format!("{{ {} }}", body.join("; "))
1746        }
1747        Command::If(clause) => {
1748            let mut s = String::from("if ");
1749            let cond: Vec<String> = clause.condition.iter().map(reconstruct_complete_command).collect();
1750            s.push_str(&cond.join("; "));
1751            s.push_str("; then ");
1752            let body: Vec<String> = clause.then_body.iter().map(reconstruct_complete_command).collect();
1753            s.push_str(&body.join("; "));
1754            for (elif_cond, elif_body) in &clause.elifs {
1755                s.push_str("; elif ");
1756                let ec: Vec<String> = elif_cond.iter().map(reconstruct_complete_command).collect();
1757                s.push_str(&ec.join("; "));
1758                s.push_str("; then ");
1759                let eb: Vec<String> = elif_body.iter().map(reconstruct_complete_command).collect();
1760                s.push_str(&eb.join("; "));
1761            }
1762            if let Some(else_body) = &clause.else_body {
1763                s.push_str("; else ");
1764                let eb: Vec<String> = else_body.iter().map(reconstruct_complete_command).collect();
1765                s.push_str(&eb.join("; "));
1766            }
1767            s.push_str("; fi");
1768            s
1769        }
1770        Command::For(clause) => {
1771            let mut s = format!("for {} ", clause.var);
1772            if let Some(words) = &clause.words {
1773                s.push_str("in ");
1774                let w: Vec<String> = words.iter().map(reconstruct_word).collect();
1775                s.push_str(&w.join(" "));
1776            }
1777            s.push_str("; do ");
1778            let body: Vec<String> = clause.body.iter().map(reconstruct_complete_command).collect();
1779            s.push_str(&body.join("; "));
1780            s.push_str("; done");
1781            s
1782        }
1783        Command::ArithFor(clause) => {
1784            let mut s = format!(
1785                "for (( {}; {}; {} )); do ",
1786                clause.init, clause.condition, clause.step
1787            );
1788            let body: Vec<String> = clause.body.iter().map(reconstruct_complete_command).collect();
1789            s.push_str(&body.join("; "));
1790            s.push_str("; done");
1791            s
1792        }
1793        Command::While(clause) => {
1794            let mut s = String::from("while ");
1795            let cond: Vec<String> = clause.condition.iter().map(reconstruct_complete_command).collect();
1796            s.push_str(&cond.join("; "));
1797            s.push_str("; do ");
1798            let body: Vec<String> = clause.body.iter().map(reconstruct_complete_command).collect();
1799            s.push_str(&body.join("; "));
1800            s.push_str("; done");
1801            s
1802        }
1803        Command::Until(clause) => {
1804            let mut s = String::from("until ");
1805            let cond: Vec<String> = clause.condition.iter().map(reconstruct_complete_command).collect();
1806            s.push_str(&cond.join("; "));
1807            s.push_str("; do ");
1808            let body: Vec<String> = clause.body.iter().map(reconstruct_complete_command).collect();
1809            s.push_str(&body.join("; "));
1810            s.push_str("; done");
1811            s
1812        }
1813        Command::Case(clause) => {
1814            let mut s = format!("case {} in ", reconstruct_word(&clause.word));
1815            for item in &clause.items {
1816                let pats: Vec<String> = item.patterns.iter().map(reconstruct_word).collect();
1817                s.push_str(&pats.join("|"));
1818                s.push_str(") ");
1819                let body: Vec<String> = item.body.iter().map(reconstruct_complete_command).collect();
1820                s.push_str(&body.join("; "));
1821                s.push_str(";; ");
1822            }
1823            s.push_str("esac");
1824            s
1825        }
1826        Command::FunctionDef(fdef) => {
1827            format!("{} () {}", fdef.name, reconstruct_command(&fdef.body))
1828        }
1829        Command::Repeat(clause) => {
1830            let mut s = format!("repeat {} do ", reconstruct_word(&clause.count));
1831            let body: Vec<String> = clause.body.iter().map(reconstruct_complete_command).collect();
1832            s.push_str(&body.join("; "));
1833            s.push_str("; done");
1834            s
1835        }
1836        Command::Always(clause) => {
1837            let try_body: Vec<String> = clause.try_body.iter().map(reconstruct_complete_command).collect();
1838            let always_body: Vec<String> = clause.always_body.iter().map(reconstruct_complete_command).collect();
1839            format!("{{ {} }} always {{ {} }}", try_body.join("; "), always_body.join("; "))
1840        }
1841        Command::Select(_) | Command::Coproc(_) | Command::Time(_) => {
1842            // Simplified — just return empty for unsupported constructs.
1843            String::new()
1844        }
1845    }
1846}
1847
1848fn reconstruct_word(word: &Word) -> String {
1849    let mut s = String::new();
1850    for part in &word.parts {
1851        reconstruct_word_part(part, &mut s);
1852    }
1853    s
1854}
1855
1856fn reconstruct_word_part(part: &WordPart, out: &mut String) {
1857    match part {
1858        WordPart::Literal(s) => out.push_str(s),
1859        WordPart::SingleQuoted(s) => {
1860            out.push('\'');
1861            out.push_str(s);
1862            out.push('\'');
1863        }
1864        WordPart::DoubleQuoted(parts) => {
1865            out.push('"');
1866            for p in parts {
1867                reconstruct_word_part(p, out);
1868            }
1869            out.push('"');
1870        }
1871        WordPart::DollarVar(name) => {
1872            out.push('$');
1873            out.push_str(name);
1874        }
1875        WordPart::DollarBrace { param, operator, arg } => {
1876            out.push_str("${");
1877            out.push_str(param);
1878            if let Some(op) = operator {
1879                out.push_str(op);
1880                if let Some(a) = arg {
1881                    out.push_str(&reconstruct_word(a));
1882                }
1883            }
1884            out.push('}');
1885        }
1886        WordPart::CommandSub(prog) => {
1887            out.push_str("$(");
1888            out.push_str(&reconstruct_program(prog));
1889            out.push(')');
1890        }
1891        WordPart::ArithSub(expr) => {
1892            out.push_str("$((");
1893            out.push_str(expr);
1894            out.push_str("))");
1895        }
1896        WordPart::Glob(kind) => match kind {
1897            GlobKind::Star => out.push('*'),
1898            GlobKind::Question => out.push('?'),
1899            GlobKind::At => out.push('@'),
1900        },
1901        WordPart::Tilde(user) => {
1902            out.push('~');
1903            out.push_str(user);
1904        }
1905    }
1906}
1907
1908fn reconstruct_redirect(redir: &Redirect) -> String {
1909    let mut s = String::new();
1910    if let Some(fd) = redir.fd {
1911        s.push_str(&fd.to_string());
1912    }
1913    match redir.op {
1914        RedirectOp::Less => s.push('<'),
1915        RedirectOp::Greater => s.push('>'),
1916        RedirectOp::DoubleGreater => s.push_str(">>"),
1917        RedirectOp::AmpGreater => s.push_str("&>"),
1918        RedirectOp::AmpDoubleGreater => s.push_str("&>>"),
1919        RedirectOp::FdDup => s.push_str(">&"),
1920        _ => s.push('>'),
1921    }
1922    s.push_str(&reconstruct_word(&redir.target));
1923    s
1924}
1925
1926/// Tokenize a string for eval/source.
1927fn tokenize(input: &str) -> Vec<frost_lexer::Token> {
1928    let mut lexer = frost_lexer::Lexer::new(input.as_bytes());
1929    let mut tokens = Vec::new();
1930    loop {
1931        let tok = lexer.next_token();
1932        let eof = tok.kind == frost_lexer::TokenKind::Eof;
1933        tokens.push(tok);
1934        if eof {
1935            break;
1936        }
1937    }
1938    tokens
1939}
1940
1941#[cfg(test)]
1942mod tests {
1943    use super::*;
1944    use frost_lexer::Span;
1945    use frost_parser::ast::{Assignment, AssignOp, CompleteCommand, List, Pipeline, SimpleCommand, Word, WordPart};
1946    use pretty_assertions::assert_eq;
1947
1948    fn literal_word(s: &str) -> Word {
1949        Word {
1950            parts: vec![WordPart::Literal(s.into())],
1951            span: Span::new(0, s.len() as u32),
1952        }
1953    }
1954
1955    fn simple_program(words: Vec<&str>) -> Program {
1956        Program {
1957            commands: vec![CompleteCommand {
1958                list: List {
1959                    first: Pipeline {
1960                        bang: false,
1961                        commands: vec![Command::Simple(SimpleCommand {
1962                            assignments: vec![],
1963                            words: words.into_iter().map(literal_word).collect(),
1964                            redirects: vec![],
1965                        })],
1966                        pipe_stderr: vec![],
1967                    },
1968                    rest: vec![],
1969                },
1970                is_async: false,
1971            }],
1972        }
1973    }
1974
1975    #[test]
1976    fn execute_true_builtin() {
1977        let mut env = ShellEnv::new();
1978        let mut exec = Executor::new(&mut env);
1979        let program = simple_program(vec!["true"]);
1980        let status = exec.execute_program(&program).unwrap();
1981        assert_eq!(status, 0);
1982    }
1983
1984    #[test]
1985    fn execute_false_builtin() {
1986        let mut env = ShellEnv::new();
1987        let mut exec = Executor::new(&mut env);
1988        let program = simple_program(vec!["false"]);
1989        let status = exec.execute_program(&program).unwrap();
1990        assert_eq!(status, 1);
1991    }
1992
1993    #[test]
1994    fn invert_status() {
1995        assert_eq!(invert(0), 1);
1996        assert_eq!(invert(1), 0);
1997        assert_eq!(invert(42), 0);
1998    }
1999
2000    #[test]
2001    fn bare_assignment() {
2002        let mut env = ShellEnv::new();
2003        let mut exec = Executor::new(&mut env);
2004        let program = Program {
2005            commands: vec![CompleteCommand {
2006                list: List {
2007                    first: Pipeline {
2008                        bang: false,
2009                        commands: vec![Command::Simple(SimpleCommand {
2010                            assignments: vec![Assignment {
2011                                name: "MY_VAR".into(),
2012                                op: AssignOp::Assign,
2013                                value: Some(literal_word("hello")),
2014                                span: Span::new(0, 12),
2015                            }],
2016                            words: vec![],
2017                            redirects: vec![],
2018                        })],
2019                        pipe_stderr: vec![],
2020                    },
2021                    rest: vec![],
2022                },
2023                is_async: false,
2024            }],
2025        };
2026        let status = exec.execute_program(&program).unwrap();
2027        assert_eq!(status, 0);
2028        assert_eq!(exec.env.get_var("MY_VAR"), Some("hello"));
2029    }
2030
2031    #[test]
2032    fn expand_dollar_var() {
2033        let mut env = ShellEnv::new();
2034        env.set_var("FOO", "bar");
2035        let exec = Executor::new(&mut env);
2036        let word = Word {
2037            parts: vec![WordPart::DollarVar("FOO".into())],
2038            span: Span::new(0, 4),
2039        };
2040        assert_eq!(exec.expand_special_var("FOO"), "bar");
2041        drop(exec);
2042    }
2043
2044    #[test]
2045    fn expand_special_vars() {
2046        let mut env = ShellEnv::new();
2047        env.exit_status = 42;
2048        env.positional_params = vec!["a".into(), "b".into()];
2049        let exec = Executor::new(&mut env);
2050        assert_eq!(exec.expand_special_var("?"), "42");
2051        assert_eq!(exec.expand_special_var("#"), "2");
2052        assert_eq!(exec.expand_special_var("@"), "a b");
2053        assert_eq!(exec.expand_special_var("1"), "a");
2054        assert_eq!(exec.expand_special_var("2"), "b");
2055        assert_eq!(exec.expand_special_var("3"), "");
2056        drop(exec);
2057    }
2058
2059    #[test]
2060    fn arith_basic() {
2061        assert_eq!(eval_arith_expr("1+2"), Some(3));
2062        assert_eq!(eval_arith_expr("10-3"), Some(7));
2063        assert_eq!(eval_arith_expr("4*5"), Some(20));
2064        assert_eq!(eval_arith_expr("10/3"), Some(3));
2065        assert_eq!(eval_arith_expr("10%3"), Some(1));
2066    }
2067
2068    #[test]
2069    fn arith_comparison() {
2070        assert_eq!(eval_arith_expr("1==1"), Some(1));
2071        assert_eq!(eval_arith_expr("1!=2"), Some(1));
2072        assert_eq!(eval_arith_expr("1<2"), Some(1));
2073        assert_eq!(eval_arith_expr("2>1"), Some(1));
2074    }
2075
2076    #[test]
2077    fn glob_match_basic() {
2078        assert!(glob_match_word("hello", "hello"));
2079        assert!(!glob_match_word("hello", "world"));
2080        assert!(glob_match_word("*", "anything"));
2081        assert!(glob_match_word("hel*", "hello"));
2082        assert!(glob_match_word("h?llo", "hello"));
2083    }
2084
2085    #[test]
2086    fn param_op_default() {
2087        assert_eq!(apply_param_op("", ":-", "default"), "default");
2088        assert_eq!(apply_param_op("val", ":-", "default"), "val");
2089    }
2090
2091    #[test]
2092    fn param_op_alternate() {
2093        assert_eq!(apply_param_op("", ":+", "alt"), "");
2094        assert_eq!(apply_param_op("val", ":+", "alt"), "alt");
2095    }
2096}