Skip to main content

wasmsh_runtime/
lib.rs

1//! Shared shell runtime core for wasmsh.
2//!
3//! Platform-agnostic execution engine: parse → HIR → expand → execute.
4//! Used by `wasmsh-browser` (standalone WASM) and future embedding crates.
5
6use indexmap::IndexMap;
7
8use wasmsh_ast::CaseTerminator;
9use wasmsh_ast::RedirectionOp;
10use wasmsh_expand::expand_words_argv;
11use wasmsh_fs::{BackendFs, OpenOptions, Vfs};
12use wasmsh_hir::{
13    HirAndOr, HirAndOrOp, HirCommand, HirCompleteCommand, HirPipeline, HirRedirection,
14};
15use wasmsh_protocol::{DiagnosticLevel, HostCommand, WorkerEvent, PROTOCOL_VERSION};
16use wasmsh_state::ShellState;
17use wasmsh_utils::{UtilContext, UtilRegistry, VecOutput as UtilOutput};
18use wasmsh_vm::Vm;
19
20/// Sentinel FD value for `&>` (redirect both stdout and stderr).
21const FD_BOTH: u32 = u32::MAX;
22
23// Runtime-level command names dispatched before builtins.
24const CMD_LOCAL: &str = "local";
25const CMD_BREAK: &str = "break";
26const CMD_CONTINUE: &str = "continue";
27const CMD_EXIT: &str = "exit";
28const CMD_EVAL: &str = "eval";
29const CMD_SOURCE: &str = "source";
30const CMD_DOT: &str = ".";
31const CMD_DECLARE: &str = "declare";
32const CMD_TYPESET: &str = "typeset";
33const CMD_LET: &str = "let";
34const CMD_SHOPT: &str = "shopt";
35const CMD_ALIAS: &str = "alias";
36const CMD_UNALIAS: &str = "unalias";
37const CMD_BUILTIN: &str = "builtin";
38const CMD_MAPFILE: &str = "mapfile";
39const CMD_READARRAY: &str = "readarray";
40const CMD_TYPE: &str = "type";
41
42/// Configuration for the browser runtime.
43#[derive(Debug, Clone)]
44pub struct BrowserConfig {
45    pub step_budget: u64,
46    /// Hostnames/IPs allowed for network access (empty = no network).
47    pub allowed_hosts: Vec<String>,
48}
49
50impl Default for BrowserConfig {
51    fn default() -> Self {
52        Self {
53            step_budget: 100_000,
54            allowed_hosts: Vec::new(),
55        }
56    }
57}
58
59/// Maximum recursion depth for eval, source, and command substitution.
60const MAX_RECURSION_DEPTH: u32 = 100;
61
62/// Transient execution state, reset between top-level commands.
63struct ExecState {
64    break_depth: u32,
65    loop_continue: bool,
66    exit_requested: Option<i32>,
67    errexit_suppressed: bool,
68    local_save_stack: Vec<(smol_str::SmolStr, Option<smol_str::SmolStr>)>,
69    recursion_depth: u32,
70    /// Set when a resource limit (step budget, output limit, cancel) is hit.
71    resource_exhausted: bool,
72}
73
74impl ExecState {
75    fn new() -> Self {
76        Self {
77            break_depth: 0,
78            loop_continue: false,
79            exit_requested: None,
80            errexit_suppressed: false,
81            local_save_stack: Vec::new(),
82            recursion_depth: 0,
83            resource_exhausted: false,
84        }
85    }
86
87    fn reset(&mut self) {
88        self.break_depth = 0;
89        self.loop_continue = false;
90        self.exit_requested = None;
91        self.errexit_suppressed = false;
92        self.resource_exhausted = false;
93    }
94}
95
96/// Result from an external command handler.
97#[derive(Debug)]
98pub struct ExternalCommandResult {
99    /// Data written to stdout.
100    pub stdout: Vec<u8>,
101    /// Data written to stderr.
102    pub stderr: Vec<u8>,
103    /// Exit code (0 = success).
104    pub status: i32,
105}
106
107/// Callback type for external (host-provided) commands.
108///
109/// Called with `(command_name, argv, stdin)`. Returns `Some(result)` if
110/// the command was handled, `None` to fall through to "command not found".
111pub type ExternalCommandHandler =
112    Box<dyn FnMut(&str, &[String], Option<&[u8]>) -> Option<ExternalCommandResult>>;
113
114/// The worker-side runtime that processes host commands.
115#[allow(missing_debug_implementations)]
116pub struct WorkerRuntime {
117    config: BrowserConfig,
118    vm: Vm,
119    fs: BackendFs,
120    utils: UtilRegistry,
121    builtins: wasmsh_builtins::BuiltinRegistry,
122    initialized: bool,
123    /// Pending stdin data for the next command (from here-doc or pipe).
124    pending_stdin: Option<Vec<u8>>,
125    /// Registered shell functions (name → HIR body).
126    functions: IndexMap<String, HirCommand>,
127    /// Transient execution state (loop control, exit, locals).
128    exec: ExecState,
129    /// Shell aliases (name → replacement text).
130    aliases: IndexMap<String, String>,
131    /// Optional handler for external commands (e.g. python3 in Pyodide).
132    external_handler: Option<ExternalCommandHandler>,
133    /// Optional network backend for curl/wget utilities.
134    network: Option<Box<dyn wasmsh_utils::net_types::NetworkBackend>>,
135}
136
137/// Action to take for a character during array element parsing.
138enum ArrayCharAction {
139    Append(char),
140    Skip,
141    SplitField,
142}
143
144/// Quoting state for parsing array elements.
145#[derive(Default)]
146struct ArrayParseState {
147    in_single_quote: bool,
148    in_double_quote: bool,
149    escape_next: bool,
150}
151
152impl ArrayParseState {
153    fn process_char(&mut self, ch: char) -> ArrayCharAction {
154        if self.escape_next {
155            self.escape_next = false;
156            return ArrayCharAction::Append(ch);
157        }
158        if ch == '\\' && !self.in_single_quote {
159            self.escape_next = true;
160            return ArrayCharAction::Skip;
161        }
162        if ch == '\'' && !self.in_double_quote {
163            self.in_single_quote = !self.in_single_quote;
164            return ArrayCharAction::Skip;
165        }
166        if ch == '"' && !self.in_single_quote {
167            self.in_double_quote = !self.in_double_quote;
168            return ArrayCharAction::Skip;
169        }
170        if ch.is_ascii_whitespace() && !self.in_single_quote && !self.in_double_quote {
171            return ArrayCharAction::SplitField;
172        }
173        ArrayCharAction::Append(ch)
174    }
175}
176
177/// Parsed flags for `declare`/`typeset`.
178#[allow(clippy::struct_excessive_bools)]
179struct DeclareFlags {
180    is_assoc: bool,
181    is_indexed: bool,
182    is_integer: bool,
183    is_export: bool,
184    is_readonly: bool,
185    is_lower: bool,
186    is_upper: bool,
187    is_print: bool,
188    is_nameref: bool,
189}
190
191/// Parse declare/typeset flags from argv, returning (flags, `name_indices`).
192fn parse_declare_flags(argv: &[String]) -> (DeclareFlags, Vec<usize>) {
193    let mut flags = DeclareFlags {
194        is_assoc: false,
195        is_indexed: false,
196        is_integer: false,
197        is_export: false,
198        is_readonly: false,
199        is_lower: false,
200        is_upper: false,
201        is_print: false,
202        is_nameref: false,
203    };
204    let mut names = Vec::new();
205
206    for (i, arg) in argv[1..].iter().enumerate() {
207        if arg.starts_with('-') && arg.len() > 1 {
208            for ch in arg[1..].chars() {
209                match ch {
210                    'A' => flags.is_assoc = true,
211                    'a' => flags.is_indexed = true,
212                    'i' => flags.is_integer = true,
213                    'x' => flags.is_export = true,
214                    'r' => flags.is_readonly = true,
215                    'l' => flags.is_lower = true,
216                    'u' => flags.is_upper = true,
217                    'p' => flags.is_print = true,
218                    'n' => flags.is_nameref = true,
219                    _ => {}
220                }
221            }
222        } else {
223            names.push(i + 1);
224        }
225    }
226    (flags, names)
227}
228
229impl WorkerRuntime {
230    #[must_use]
231    pub fn new() -> Self {
232        Self {
233            config: BrowserConfig::default(),
234            vm: Vm::new(ShellState::new(), 0),
235            fs: BackendFs::new(),
236            utils: UtilRegistry::new(),
237            builtins: wasmsh_builtins::BuiltinRegistry::new(),
238            initialized: false,
239            pending_stdin: None,
240            functions: IndexMap::new(),
241            exec: ExecState::new(),
242            aliases: IndexMap::new(),
243            external_handler: None,
244            network: None,
245        }
246    }
247
248    /// Register a handler for external commands (e.g. `python3` in Pyodide).
249    pub fn set_external_handler(&mut self, handler: ExternalCommandHandler) {
250        self.external_handler = Some(handler);
251    }
252
253    /// Register a network backend for `curl`/`wget` utilities.
254    pub fn set_network_backend(
255        &mut self,
256        backend: Box<dyn wasmsh_utils::net_types::NetworkBackend>,
257    ) {
258        self.network = Some(backend);
259    }
260
261    /// Process a host command and return a list of events to send back.
262    pub fn handle_command(&mut self, cmd: HostCommand) -> Vec<WorkerEvent> {
263        match cmd {
264            HostCommand::Init {
265                step_budget,
266                allowed_hosts,
267            } => {
268                self.config.step_budget = step_budget;
269                self.config.allowed_hosts = allowed_hosts;
270                self.vm = Vm::new(ShellState::new(), step_budget);
271                self.fs = BackendFs::new();
272                self.pending_stdin = None;
273                self.functions = IndexMap::new();
274                self.exec.reset();
275                self.aliases = IndexMap::new();
276                self.initialized = true;
277                // Set default shopt options (bash defaults)
278                self.vm.state.set_var("SHOPT_extglob".into(), "1".into());
279                vec![WorkerEvent::Version(PROTOCOL_VERSION.to_string())]
280            }
281            HostCommand::Run { input } => {
282                if !self.initialized {
283                    return vec![WorkerEvent::Diagnostic(
284                        DiagnosticLevel::Error,
285                        "runtime not initialized".into(),
286                    )];
287                }
288                self.execute_input(&input)
289            }
290            HostCommand::Cancel => {
291                self.vm.cancellation_token().cancel();
292                vec![WorkerEvent::Diagnostic(
293                    DiagnosticLevel::Info,
294                    "cancel received".into(),
295                )]
296            }
297            HostCommand::ReadFile { path } => {
298                use wasmsh_fs::OpenOptions;
299                match self.fs.open(&path, OpenOptions::read()) {
300                    Ok(h) => match self.fs.read_file(h) {
301                        Ok(data) => {
302                            self.fs.close(h);
303                            vec![WorkerEvent::Stdout(data)]
304                        }
305                        Err(e) => {
306                            self.fs.close(h);
307                            vec![WorkerEvent::Diagnostic(
308                                DiagnosticLevel::Error,
309                                format!("read error: {path}: {e}"),
310                            )]
311                        }
312                    },
313                    Err(e) => vec![WorkerEvent::Diagnostic(
314                        DiagnosticLevel::Error,
315                        format!("read error: {e}"),
316                    )],
317                }
318            }
319            HostCommand::WriteFile { path, data } => {
320                use wasmsh_fs::OpenOptions;
321                match self.fs.open(&path, OpenOptions::write()) {
322                    Ok(h) => {
323                        if let Err(e) = self.fs.write_file(h, &data) {
324                            self.vm.stderr.extend_from_slice(
325                                format!("wasmsh: write error: {e}\n").as_bytes(),
326                            );
327                        }
328                        self.fs.close(h);
329                        vec![WorkerEvent::FsChanged(path)]
330                    }
331                    Err(e) => vec![WorkerEvent::Diagnostic(
332                        DiagnosticLevel::Error,
333                        format!("write error: {e}"),
334                    )],
335                }
336            }
337            HostCommand::ListDir { path } => match self.fs.read_dir(&path) {
338                Ok(entries) => {
339                    let names: Vec<u8> = entries
340                        .iter()
341                        .map(|e| e.name.as_str())
342                        .collect::<Vec<_>>()
343                        .join("\n")
344                        .into_bytes();
345                    vec![WorkerEvent::Stdout(names)]
346                }
347                Err(e) => vec![WorkerEvent::Diagnostic(
348                    DiagnosticLevel::Error,
349                    format!("readdir error: {e}"),
350                )],
351            },
352            HostCommand::Mount { .. } => {
353                vec![WorkerEvent::Diagnostic(
354                    DiagnosticLevel::Warning,
355                    "mount not yet implemented".into(),
356                )]
357            }
358            _ => {
359                vec![WorkerEvent::Diagnostic(
360                    DiagnosticLevel::Warning,
361                    "unknown command".into(),
362                )]
363            }
364        }
365    }
366
367    /// Execute input and return collected events (used by eval/source).
368    fn execute_input_inner(&mut self, input: &str) -> Vec<WorkerEvent> {
369        self.exec.recursion_depth += 1;
370        if self.exec.recursion_depth > MAX_RECURSION_DEPTH {
371            self.exec.recursion_depth -= 1;
372            return vec![WorkerEvent::Stderr(
373                b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
374            )];
375        }
376        let result = self.execute_input_inner_impl(input);
377        self.exec.recursion_depth -= 1;
378        result
379    }
380
381    /// Inner implementation of `execute_input_inner` (after recursion check).
382    fn execute_input_inner_impl(&mut self, input: &str) -> Vec<WorkerEvent> {
383        let ast = match wasmsh_parse::parse(input) {
384            Ok(ast) => ast,
385            Err(e) => {
386                self.vm.state.last_status = 2;
387                return vec![WorkerEvent::Stderr(
388                    format!("wasmsh: parse error: {e}\n").into_bytes(),
389                )];
390            }
391        };
392        let hir = wasmsh_hir::lower(&ast);
393        for cc in &hir.items {
394            if self.exec.exit_requested.is_some() {
395                break;
396            }
397            // Update $LINENO from span position
398            let line = input
399                .as_bytes()
400                .iter()
401                .take(cc.span.start as usize)
402                .filter(|&&b| b == b'\n')
403                .count() as u32
404                + 1;
405            self.vm.state.lineno = line;
406            for and_or in &cc.list {
407                self.execute_pipeline_chain(and_or);
408                if self.exec.exit_requested.is_some() {
409                    break;
410                }
411                if self.should_errexit(and_or) {
412                    self.exec.exit_requested = Some(self.vm.state.last_status);
413                    break;
414                }
415            }
416        }
417        // Drain stdout/stderr into events
418        let mut events = Vec::new();
419        if !self.vm.stdout.is_empty() {
420            events.push(WorkerEvent::Stdout(std::mem::take(&mut self.vm.stdout)));
421        }
422        if !self.vm.stderr.is_empty() {
423            events.push(WorkerEvent::Stderr(std::mem::take(&mut self.vm.stderr)));
424        }
425        events
426    }
427
428    fn execute_input(&mut self, input: &str) -> Vec<WorkerEvent> {
429        let mut events = self.execute_input_inner(input);
430        self.run_exit_trap_if_needed(&mut events);
431        self.drain_io_events(&mut events);
432        self.drain_diagnostic_events(&mut events);
433        self.push_output_limit_warning(&mut events);
434
435        let exit_status = if self.exec.resource_exhausted {
436            // Resource exhaustion (step budget, output limit, cancellation)
437            // must produce a non-zero exit code.
438            128
439        } else {
440            self.exec
441                .exit_requested
442                .unwrap_or(self.vm.state.last_status)
443        };
444        events.push(WorkerEvent::Exit(exit_status));
445        events
446    }
447
448    fn run_exit_trap_if_needed(&mut self, events: &mut Vec<WorkerEvent>) {
449        let Some(exit_code) = self.exec.exit_requested else {
450            return;
451        };
452        let Some(handler_str) = self.take_exit_trap_handler() else {
453            return;
454        };
455        self.exec.exit_requested = None;
456        events.extend(self.execute_input_inner(&handler_str));
457        self.exec.exit_requested = Some(exit_code);
458    }
459
460    fn take_exit_trap_handler(&mut self) -> Option<String> {
461        let handler = self.vm.state.get_var("_TRAP_EXIT")?;
462        if handler.is_empty() {
463            return None;
464        }
465        let handler_str = handler.to_string();
466        self.vm.state.set_var(
467            smol_str::SmolStr::from("_TRAP_EXIT"),
468            smol_str::SmolStr::default(),
469        );
470        Some(handler_str)
471    }
472
473    fn drain_io_events(&mut self, events: &mut Vec<WorkerEvent>) {
474        self.push_buffer_event(events, true);
475        self.push_buffer_event(events, false);
476    }
477
478    fn push_buffer_event(&mut self, events: &mut Vec<WorkerEvent>, stdout: bool) {
479        let buffer = if stdout {
480            &mut self.vm.stdout
481        } else {
482            &mut self.vm.stderr
483        };
484        if buffer.is_empty() {
485            return;
486        }
487
488        let data = std::mem::take(buffer);
489        events.push(if stdout {
490            WorkerEvent::Stdout(data)
491        } else {
492            WorkerEvent::Stderr(data)
493        });
494    }
495
496    fn drain_diagnostic_events(&mut self, events: &mut Vec<WorkerEvent>) {
497        for diag in self.vm.diagnostics.drain(..) {
498            events.push(WorkerEvent::Diagnostic(
499                Self::to_protocol_diag_level(diag.level),
500                diag.message,
501            ));
502        }
503    }
504
505    fn to_protocol_diag_level(level: wasmsh_vm::DiagLevel) -> DiagnosticLevel {
506        match level {
507            wasmsh_vm::DiagLevel::Trace => DiagnosticLevel::Trace,
508            wasmsh_vm::DiagLevel::Info => DiagnosticLevel::Info,
509            wasmsh_vm::DiagLevel::Warning => DiagnosticLevel::Warning,
510            wasmsh_vm::DiagLevel::Error => DiagnosticLevel::Error,
511        }
512    }
513
514    fn push_output_limit_warning(&self, events: &mut Vec<WorkerEvent>) {
515        if self.vm.limits.output_byte_limit == 0
516            || self.vm.output_bytes <= self.vm.limits.output_byte_limit
517        {
518            return;
519        }
520
521        events.push(WorkerEvent::Diagnostic(
522            DiagnosticLevel::Error,
523            format!(
524                "output limit exceeded: {} bytes (limit: {}); execution aborted",
525                self.vm.output_bytes, self.vm.limits.output_byte_limit
526            ),
527        ));
528    }
529
530    fn execute_pipeline_chain(&mut self, and_or: &HirAndOr) {
531        self.execute_pipeline(&and_or.first);
532        for (op, pipeline) in &and_or.rest {
533            match op {
534                HirAndOrOp::And => {
535                    if self.vm.state.last_status == 0 {
536                        self.execute_pipeline(pipeline);
537                    }
538                }
539                HirAndOrOp::Or => {
540                    if self.vm.state.last_status != 0 {
541                        self.execute_pipeline(pipeline);
542                    }
543                }
544            }
545        }
546    }
547
548    fn execute_pipeline(&mut self, pipeline: &HirPipeline) {
549        let cmds = &pipeline.commands;
550        if cmds.len() == 1 {
551            self.execute_single_pipeline(&cmds[0]);
552        } else {
553            self.execute_multi_pipeline(cmds, pipeline);
554        }
555        if pipeline.negated {
556            self.vm.state.last_status = i32::from(self.vm.state.last_status == 0);
557        }
558    }
559
560    fn execute_single_pipeline(&mut self, cmd: &HirCommand) {
561        self.execute_command(cmd);
562        self.set_pipestatus(&[self.vm.state.last_status]);
563    }
564
565    fn execute_multi_pipeline(&mut self, cmds: &[HirCommand], pipeline: &HirPipeline) {
566        let pipefail = self.vm.state.get_var("SHOPT_o_pipefail").as_deref() == Some("1");
567        let mut rightmost_failure: i32 = 0;
568        let mut statuses: Vec<i32> = Vec::new();
569
570        for (i, cmd) in cmds.iter().enumerate() {
571            let is_last = i == cmds.len() - 1;
572            let stdout_before = self.vm.stdout.len();
573            let stderr_before = self.vm.stderr.len();
574
575            self.execute_command(cmd);
576            statuses.push(self.vm.state.last_status);
577
578            if pipefail && self.vm.state.last_status != 0 {
579                rightmost_failure = self.vm.state.last_status;
580            }
581
582            if !is_last {
583                self.pipe_stage_output(
584                    stdout_before,
585                    stderr_before,
586                    pipeline.pipe_stderr.get(i).copied().unwrap_or(false),
587                );
588            }
589        }
590
591        self.set_pipestatus(&statuses);
592        if pipefail && rightmost_failure != 0 {
593            self.vm.state.last_status = rightmost_failure;
594        }
595    }
596
597    fn pipe_stage_output(&mut self, stdout_before: usize, stderr_before: usize, pipe_stderr: bool) {
598        use wasmsh_vm::pipe::PipeBuffer;
599
600        let mut stage_output = self.vm.stdout[stdout_before..].to_vec();
601        self.vm.stdout.truncate(stdout_before);
602
603        if pipe_stderr {
604            let stage_stderr = self.vm.stderr[stderr_before..].to_vec();
605            self.vm.stderr.truncate(stderr_before);
606            stage_output.extend_from_slice(&stage_stderr);
607        }
608
609        let mut pipe = PipeBuffer::default_size();
610        pipe.write_all(&stage_output);
611        pipe.close_write();
612        self.pending_stdin = Some(pipe.drain());
613    }
614
615    fn set_pipestatus(&mut self, statuses: &[i32]) {
616        let status_key = smol_str::SmolStr::from("PIPESTATUS");
617        self.vm.state.init_indexed_array(status_key.clone());
618        for (i, s) in statuses.iter().enumerate() {
619            self.vm.state.set_array_element(
620                status_key.clone(),
621                &i.to_string(),
622                smol_str::SmolStr::from(s.to_string()),
623            );
624        }
625    }
626
627    /// Execute a command substitution and return the trimmed output.
628    fn execute_subst(&mut self, inner: &str) -> smol_str::SmolStr {
629        let saved_stdout = std::mem::take(&mut self.vm.stdout);
630        let events = self.execute_input_inner(inner);
631        let mut result = String::new();
632        for e in &events {
633            if let WorkerEvent::Stdout(d) = e {
634                result.push_str(&String::from_utf8_lossy(d));
635            }
636        }
637        if !self.vm.stdout.is_empty() {
638            result.push_str(&String::from_utf8_lossy(&self.vm.stdout));
639            self.vm.stdout.clear();
640        }
641        self.vm.stdout = saved_stdout;
642        smol_str::SmolStr::from(result.trim_end_matches('\n'))
643    }
644
645    /// Counter for generating unique temp file paths for process substitution.
646    fn next_proc_subst_id() -> u64 {
647        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
648        COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
649    }
650
651    /// Execute `<(cmd)` — run the command, write stdout to a temp file,
652    /// return the temp file path as the expansion result.
653    fn execute_process_subst_in(&mut self, inner: &str) -> smol_str::SmolStr {
654        let output = self.execute_subst_raw(inner);
655        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
656        let h = self.fs.open(&path, OpenOptions::write()).unwrap();
657        let _ = self.fs.write_file(h, output.as_bytes());
658        self.fs.close(h);
659        smol_str::SmolStr::from(path)
660    }
661
662    /// Execute `>(cmd)` — create a temp file, return its path.
663    /// After the outer command writes to it, read and feed to the inner command.
664    /// (Simplified: just returns a writable temp path; full pipe semantics deferred.)
665    fn execute_process_subst_out(&mut self, _inner: &str) -> smol_str::SmolStr {
666        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
667        // Create empty file
668        let h = self.fs.open(&path, OpenOptions::write()).unwrap();
669        let _ = self.fs.write_file(h, b"");
670        self.fs.close(h);
671        smol_str::SmolStr::from(path)
672    }
673
674    /// Execute a command and return its raw stdout (without trimming).
675    fn execute_subst_raw(&mut self, inner: &str) -> String {
676        let saved_stdout = std::mem::take(&mut self.vm.stdout);
677        let events = self.execute_input_inner(inner);
678        let mut result = String::new();
679        for e in &events {
680            if let WorkerEvent::Stdout(d) = e {
681                result.push_str(&String::from_utf8_lossy(d));
682            }
683        }
684        if !self.vm.stdout.is_empty() {
685            result.push_str(&String::from_utf8_lossy(&self.vm.stdout));
686            self.vm.stdout.clear();
687        }
688        self.vm.stdout = saved_stdout;
689        result
690    }
691
692    /// Resolve command substitutions in a list of words by executing them.
693    fn resolve_command_subst(&mut self, words: &[wasmsh_ast::Word]) -> Vec<wasmsh_ast::Word> {
694        words
695            .iter()
696            .map(|w| {
697                let parts: Vec<wasmsh_ast::WordPart> = w
698                    .parts
699                    .iter()
700                    .map(|p| match p {
701                        wasmsh_ast::WordPart::CommandSubstitution(inner) => {
702                            wasmsh_ast::WordPart::Literal(self.execute_subst(inner))
703                        }
704                        wasmsh_ast::WordPart::ProcessSubstIn(inner) => {
705                            wasmsh_ast::WordPart::Literal(self.execute_process_subst_in(inner))
706                        }
707                        wasmsh_ast::WordPart::ProcessSubstOut(inner) => {
708                            wasmsh_ast::WordPart::Literal(self.execute_process_subst_out(inner))
709                        }
710                        wasmsh_ast::WordPart::DoubleQuoted(inner_parts) => {
711                            let resolved: Vec<wasmsh_ast::WordPart> = inner_parts
712                                .iter()
713                                .map(|ip| match ip {
714                                    wasmsh_ast::WordPart::CommandSubstitution(inner) => {
715                                        wasmsh_ast::WordPart::Literal(self.execute_subst(inner))
716                                    }
717                                    wasmsh_ast::WordPart::ProcessSubstIn(inner) => {
718                                        wasmsh_ast::WordPart::Literal(
719                                            self.execute_process_subst_in(inner),
720                                        )
721                                    }
722                                    wasmsh_ast::WordPart::ProcessSubstOut(inner) => {
723                                        wasmsh_ast::WordPart::Literal(
724                                            self.execute_process_subst_out(inner),
725                                        )
726                                    }
727                                    other => other.clone(),
728                                })
729                                .collect();
730                            wasmsh_ast::WordPart::DoubleQuoted(resolved)
731                        }
732                        other => other.clone(),
733                    })
734                    .collect();
735                wasmsh_ast::Word {
736                    parts,
737                    span: w.span,
738                }
739            })
740            .collect()
741    }
742
743    fn execute_command(&mut self, cmd: &HirCommand) {
744        match cmd {
745            HirCommand::Exec(exec) => self.execute_exec(exec),
746            HirCommand::Assign(assign) => {
747                for a in &assign.assignments {
748                    self.execute_assignment(&a.name, a.value.as_ref());
749                }
750                let stdout_before = self.vm.stdout.len();
751                self.apply_redirections(&assign.redirections, stdout_before);
752                self.vm.state.last_status = 0;
753            }
754            HirCommand::If(if_cmd) => self.execute_if(if_cmd),
755            HirCommand::While(loop_cmd) => self.execute_while_loop(loop_cmd),
756            HirCommand::Until(loop_cmd) => self.execute_until_loop(loop_cmd),
757            HirCommand::For(for_cmd) => self.execute_for_loop(for_cmd),
758            HirCommand::Group(block) => self.execute_body(&block.body),
759            HirCommand::Subshell(block) => {
760                self.vm.state.env.push_scope();
761                self.execute_body(&block.body);
762                self.vm.state.env.pop_scope();
763            }
764            HirCommand::Case(case_cmd) => self.execute_case(case_cmd),
765            HirCommand::FunctionDef(fd) => {
766                self.functions
767                    .insert(fd.name.to_string(), (*fd.body).clone());
768                self.vm.state.last_status = 0;
769            }
770            HirCommand::RedirectOnly(ro) => {
771                let stdout_before = self.vm.stdout.len();
772                self.apply_redirections(&ro.redirections, stdout_before);
773                self.vm.state.last_status = 0;
774            }
775            HirCommand::DoubleBracket(db) => {
776                let result = self.eval_double_bracket(&db.words);
777                self.vm.state.last_status = i32::from(!result);
778            }
779            HirCommand::ArithCommand(ac) => {
780                let result = wasmsh_expand::eval_arithmetic(&ac.expr, &mut self.vm.state);
781                self.vm.state.last_status = i32::from(result == 0);
782            }
783            HirCommand::ArithFor(af) => self.execute_arith_for(af),
784            HirCommand::Select(sel) => self.execute_select(sel),
785            _ => {}
786        }
787    }
788
789    /// Execute a simple command (`HirCommand::Exec`).
790    fn execute_exec(&mut self, exec: &wasmsh_hir::HirExec) {
791        let resolved = self.resolve_command_subst(&exec.argv);
792        let expanded = expand_words_argv(&resolved, &mut self.vm.state);
793
794        if self.check_nounset_error() {
795            return;
796        }
797        if expanded.is_empty() {
798            return;
799        }
800
801        // Brace expansion must be suppressed for quoted words (POSIX + bash behavior).
802        let argv: Vec<String> = expanded
803            .into_iter()
804            .flat_map(|ew| {
805                if ew.was_quoted {
806                    vec![ew.text]
807                } else {
808                    wasmsh_expand::expand_braces(&ew.text)
809                }
810            })
811            .collect();
812        let argv = self.expand_globs(argv);
813
814        for assignment in &exec.env {
815            self.execute_assignment(&assignment.name, assignment.value.as_ref());
816        }
817
818        if self.collect_stdin_from_redirections(&exec.redirections) {
819            return;
820        }
821
822        if self.try_alias_expansion(&argv) {
823            return;
824        }
825
826        let stdout_before = self.vm.stdout.len();
827        let cmd_name = &argv[0];
828        self.trace_command(&argv);
829
830        if self.try_runtime_command(cmd_name, &argv) {
831            return;
832        }
833
834        self.dispatch_command(cmd_name, &argv);
835        self.apply_redirections(&exec.redirections, stdout_before);
836    }
837
838    /// Check for nounset errors from expansion. Returns true if an error was found.
839    fn check_nounset_error(&mut self) -> bool {
840        if let Some(var_name) = self.vm.state.get_var("_NOUNSET_ERROR") {
841            if !var_name.is_empty() {
842                let msg = format!("wasmsh: {var_name}: unbound variable\n");
843                self.vm.stderr.extend_from_slice(msg.as_bytes());
844                self.vm.state.set_var(
845                    smol_str::SmolStr::from("_NOUNSET_ERROR"),
846                    smol_str::SmolStr::default(),
847                );
848                self.vm.state.last_status = 1;
849                return true;
850            }
851        }
852        false
853    }
854
855    /// Collect stdin from here-doc bodies or input redirections. Returns true if
856    /// an error occurred and execution should stop.
857    fn collect_stdin_from_redirections(&mut self, redirections: &[HirRedirection]) -> bool {
858        for redir in redirections {
859            match redir.op {
860                RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
861                    if let Some(body) = &redir.here_doc_body {
862                        let expanded =
863                            wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
864                        self.pending_stdin = Some(expanded.into_bytes());
865                    }
866                }
867                RedirectionOp::HereString => {
868                    let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
869                    let resolved_target = resolved.first().unwrap_or(&redir.target);
870                    let content = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
871                    let mut data = content.into_bytes();
872                    data.push(b'\n');
873                    self.pending_stdin = Some(data);
874                }
875                RedirectionOp::Input => {
876                    let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
877                    let resolved_target = resolved.first().unwrap_or(&redir.target);
878                    let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
879                    let path = self.resolve_cwd_path(&target);
880                    if let Ok(h) = self.fs.open(&path, OpenOptions::read()) {
881                        match self.fs.read_file(h) {
882                            Ok(data) => {
883                                self.pending_stdin = Some(data);
884                            }
885                            Err(e) => {
886                                let msg = format!("wasmsh: {target}: read error: {e}\n");
887                                self.vm.stderr.extend_from_slice(msg.as_bytes());
888                                self.vm.state.last_status = 1;
889                                self.fs.close(h);
890                                return true;
891                            }
892                        }
893                        self.fs.close(h);
894                    } else {
895                        let msg = format!("wasmsh: {target}: No such file or directory\n");
896                        self.vm.stderr.extend_from_slice(msg.as_bytes());
897                        self.vm.state.last_status = 1;
898                        return true;
899                    }
900                }
901                _ => {}
902            }
903        }
904        false
905    }
906
907    /// Try alias expansion for the command. Returns true if an alias was expanded.
908    fn try_alias_expansion(&mut self, argv: &[String]) -> bool {
909        if let Some(alias_val) = self.aliases.get(&argv[0]).cloned() {
910            let rest = if argv.len() > 1 {
911                format!(" {}", argv[1..].join(" "))
912            } else {
913                String::new()
914            };
915            let expanded = format!("{alias_val}{rest}");
916            let sub_events = self.execute_input_inner(&expanded);
917            self.merge_sub_events(sub_events);
918            return true;
919        }
920        false
921    }
922
923    /// Print xtrace output if enabled.
924    fn trace_command(&mut self, argv: &[String]) {
925        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
926            let ps4 = self
927                .vm
928                .state
929                .get_var("PS4")
930                .unwrap_or_else(|| smol_str::SmolStr::from("+ "));
931            let trace_line = format!("{}{}\n", ps4, argv.join(" "));
932            self.vm.stderr.extend_from_slice(trace_line.as_bytes());
933        }
934    }
935
936    /// Try to handle a runtime-level command (local, break, continue, exit, eval,
937    /// source, declare, etc.). Returns true if handled.
938    fn try_runtime_command(&mut self, cmd_name: &str, argv: &[String]) -> bool {
939        match cmd_name {
940            CMD_LOCAL => {
941                self.execute_local(argv);
942                true
943            }
944            CMD_BREAK => {
945                self.exec.break_depth = argv.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
946                self.vm.state.last_status = 0;
947                true
948            }
949            CMD_CONTINUE => {
950                self.exec.loop_continue = true;
951                self.vm.state.last_status = 0;
952                true
953            }
954            CMD_EXIT => {
955                let code = argv
956                    .get(1)
957                    .and_then(|s| s.parse().ok())
958                    .unwrap_or(self.vm.state.last_status);
959                self.exec.exit_requested = Some(code);
960                self.vm.state.last_status = code;
961                true
962            }
963            CMD_EVAL => {
964                let code = argv[1..].join(" ");
965                let sub_events = self.execute_input_inner(&code);
966                self.merge_sub_events_with_diagnostics(sub_events);
967                true
968            }
969            CMD_SOURCE | CMD_DOT => {
970                self.execute_source(argv);
971                true
972            }
973            CMD_DECLARE | CMD_TYPESET => {
974                self.execute_declare(argv);
975                true
976            }
977            CMD_LET => {
978                self.execute_let(argv);
979                true
980            }
981            CMD_SHOPT => {
982                self.execute_shopt(argv);
983                true
984            }
985            CMD_ALIAS => {
986                self.execute_alias(argv);
987                true
988            }
989            CMD_UNALIAS => {
990                self.execute_unalias(argv);
991                true
992            }
993            CMD_BUILTIN => {
994                self.execute_builtin_keyword(argv);
995                true
996            }
997            CMD_MAPFILE | CMD_READARRAY => {
998                self.execute_mapfile(argv);
999                true
1000            }
1001            CMD_TYPE => {
1002                self.execute_type(argv);
1003                true
1004            }
1005            _ => false,
1006        }
1007    }
1008
1009    /// Execute `local` — save old variable values and set new ones.
1010    fn execute_local(&mut self, argv: &[String]) {
1011        for arg in &argv[1..] {
1012            let (name, value) = if let Some(eq) = arg.find('=') {
1013                (&arg[..eq], Some(&arg[eq + 1..]))
1014            } else {
1015                (arg.as_str(), None)
1016            };
1017            let old = self.vm.state.get_var(name);
1018            self.exec
1019                .local_save_stack
1020                .push((smol_str::SmolStr::from(name), old));
1021            let val = value.map_or(smol_str::SmolStr::default(), smol_str::SmolStr::from);
1022            self.vm.state.set_var(smol_str::SmolStr::from(name), val);
1023        }
1024        self.vm.state.last_status = 0;
1025    }
1026
1027    /// Execute `source`/`.` — read and execute a file.
1028    fn execute_source(&mut self, argv: &[String]) {
1029        let Some(path) = argv.get(1) else { return };
1030        let resolved = if path.contains('/') {
1031            Some(self.resolve_cwd_path(path))
1032        } else {
1033            let direct = self.resolve_cwd_path(path);
1034            if self.fs.stat(&direct).is_ok() {
1035                Some(direct)
1036            } else {
1037                self.search_path_for_file(path)
1038            }
1039        };
1040        let Some(full) = resolved else {
1041            let msg = format!("source: {path}: not found\n");
1042            self.vm.stderr.extend_from_slice(msg.as_bytes());
1043            self.vm.state.last_status = 1;
1044            return;
1045        };
1046        let Ok(h) = self.fs.open(&full, OpenOptions::read()) else {
1047            let msg = format!("source: {path}: not found\n");
1048            self.vm.stderr.extend_from_slice(msg.as_bytes());
1049            self.vm.state.last_status = 1;
1050            return;
1051        };
1052        match self.fs.read_file(h) {
1053            Ok(data) => {
1054                self.fs.close(h);
1055                self.vm
1056                    .state
1057                    .source_stack
1058                    .push(smol_str::SmolStr::from(full.as_str()));
1059                let code = String::from_utf8_lossy(&data).to_string();
1060                let sub_events = self.execute_input_inner(&code);
1061                self.vm.state.source_stack.pop();
1062                self.merge_sub_events_with_diagnostics(sub_events);
1063            }
1064            Err(e) => {
1065                self.fs.close(h);
1066                let msg = format!("source: {path}: read error: {e}\n");
1067                self.vm.stderr.extend_from_slice(msg.as_bytes());
1068                self.vm.state.last_status = 1;
1069            }
1070        }
1071    }
1072
1073    /// Merge sub-events (stdout/stderr only) into the current VM buffers.
1074    fn merge_sub_events(&mut self, events: Vec<WorkerEvent>) {
1075        for e in events {
1076            match e {
1077                WorkerEvent::Stdout(d) => self.vm.stdout.extend_from_slice(&d),
1078                WorkerEvent::Stderr(d) => self.vm.stderr.extend_from_slice(&d),
1079                _ => {}
1080            }
1081        }
1082    }
1083
1084    /// Merge sub-events including diagnostics into the current VM buffers.
1085    fn merge_sub_events_with_diagnostics(&mut self, events: Vec<WorkerEvent>) {
1086        for e in events {
1087            match e {
1088                WorkerEvent::Stdout(d) => self.vm.stdout.extend_from_slice(&d),
1089                WorkerEvent::Stderr(d) => self.vm.stderr.extend_from_slice(&d),
1090                WorkerEvent::Diagnostic(level, msg) => {
1091                    self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
1092                        level: convert_diag_level(level),
1093                        category: wasmsh_vm::DiagCategory::Runtime,
1094                        message: msg,
1095                    });
1096                }
1097                _ => {}
1098            }
1099        }
1100    }
1101
1102    /// Handle `bash`/`sh` commands by reading the script and executing it.
1103    fn call_shell_script(&mut self, argv: &[String]) {
1104        if argv.len() < 2 {
1105            // Interactive shell not supported — just return
1106            return;
1107        }
1108
1109        // Check for -c flag (inline script)
1110        if argv[1] == "-c" {
1111            if let Some(script) = argv.get(2) {
1112                let sub_events = self.execute_input_inner(script);
1113                self.merge_sub_events_with_diagnostics(sub_events);
1114            }
1115            return;
1116        }
1117
1118        // Read script file from VFS
1119        let path = if argv[1].starts_with('/') {
1120            argv[1].clone()
1121        } else {
1122            format!("{}/{}", self.vm.state.cwd, argv[1])
1123        };
1124        let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
1125            let msg = format!("{}: {}: No such file or directory\n", argv[0], argv[1]);
1126            self.vm.stderr.extend_from_slice(msg.as_bytes());
1127            self.vm.state.last_status = 127;
1128            return;
1129        };
1130        let data = self.fs.read_file(h).unwrap_or_default();
1131        self.fs.close(h);
1132        let content = String::from_utf8_lossy(&data).to_string();
1133
1134        // Set positional parameters from remaining argv
1135        let old_positional = std::mem::take(&mut self.vm.state.positional);
1136        self.vm.state.positional = argv[1..]
1137            .iter()
1138            .map(|s| smol_str::SmolStr::from(s.as_str()))
1139            .collect();
1140
1141        let sub_events = self.execute_input_inner(&content);
1142        self.merge_sub_events_with_diagnostics(sub_events);
1143
1144        self.vm.state.positional = old_positional;
1145    }
1146
1147    /// Dispatch a command to a shell function, builtin, utility, or report not found.
1148    fn dispatch_command(&mut self, cmd_name: &str, argv: &[String]) {
1149        if self.check_resource_limits() {
1150            return;
1151        }
1152        if cmd_name == "bash" || cmd_name == "sh" {
1153            self.call_shell_script(argv);
1154            return;
1155        }
1156        if let Some(body) = self.functions.get(cmd_name).cloned() {
1157            self.call_shell_function(cmd_name, argv, &body);
1158        } else if self.builtins.is_builtin(cmd_name) {
1159            self.call_builtin(cmd_name, argv);
1160        } else if self.utils.is_utility(cmd_name) {
1161            if cmd_name == "find" && argv.iter().any(|a| a == "-exec") {
1162                self.call_find_with_exec(argv);
1163            } else if cmd_name == "xargs" {
1164                self.call_xargs_with_exec(argv);
1165            } else {
1166                self.call_utility(cmd_name, argv);
1167            }
1168        } else if let Some(ref mut handler) = self.external_handler {
1169            let stdin_data = self.pending_stdin.take();
1170            if let Some(result) = handler(cmd_name, argv, stdin_data.as_deref()) {
1171                self.vm.stdout.extend_from_slice(&result.stdout);
1172                self.vm.stderr.extend_from_slice(&result.stderr);
1173                self.vm.output_bytes += (result.stdout.len() + result.stderr.len()) as u64;
1174                self.vm.state.last_status = result.status;
1175            } else {
1176                let msg = format!("wasmsh: {cmd_name}: command not found\n");
1177                self.vm.stderr.extend_from_slice(msg.as_bytes());
1178                self.vm.state.last_status = 127;
1179            }
1180        } else {
1181            let msg = format!("wasmsh: {cmd_name}: command not found\n");
1182            self.vm.stderr.extend_from_slice(msg.as_bytes());
1183            self.vm.state.last_status = 127;
1184        }
1185    }
1186
1187    /// Invoke a shell function.
1188    fn call_shell_function(&mut self, cmd_name: &str, argv: &[String], body: &HirCommand) {
1189        let old_positional = std::mem::take(&mut self.vm.state.positional);
1190        self.vm.state.positional = argv[1..]
1191            .iter()
1192            .map(|s| smol_str::SmolStr::from(s.as_str()))
1193            .collect();
1194        self.vm
1195            .state
1196            .func_stack
1197            .push(smol_str::SmolStr::from(cmd_name));
1198        let locals_before = self.exec.local_save_stack.len();
1199        self.execute_command(body);
1200        let new_locals: Vec<_> = self.exec.local_save_stack.drain(locals_before..).collect();
1201        for (name, old_val) in new_locals.into_iter().rev() {
1202            if let Some(val) = old_val {
1203                self.vm.state.set_var(name, val);
1204            } else {
1205                self.vm.state.unset_var(&name).ok();
1206            }
1207        }
1208        self.vm.state.func_stack.pop();
1209        self.vm.state.positional = old_positional;
1210    }
1211
1212    /// Invoke a builtin command.
1213    fn call_builtin(&mut self, cmd_name: &str, argv: &[String]) {
1214        let builtin_fn = self.builtins.get(cmd_name).unwrap();
1215        let stdin_data = self.pending_stdin.take();
1216        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
1217        let mut sink = wasmsh_builtins::VecSink::default();
1218        let status = {
1219            let mut ctx = wasmsh_builtins::BuiltinContext {
1220                state: &mut self.vm.state,
1221                output: &mut sink,
1222                fs: Some(&self.fs),
1223                stdin: stdin_data.as_deref(),
1224            };
1225            builtin_fn(&mut ctx, &argv_refs)
1226        };
1227        self.vm.stdout.extend_from_slice(&sink.stdout);
1228        self.vm.stderr.extend_from_slice(&sink.stderr);
1229        self.vm.output_bytes += (sink.stdout.len() + sink.stderr.len()) as u64;
1230        self.vm.state.last_status = status;
1231        self.pending_stdin = None;
1232    }
1233
1234    /// Extract `-exec CMD [args...] {} \;` from find argv.
1235    /// Returns `(exec_template, cleaned_argv)` or `None` if no `-exec` present.
1236    fn extract_find_exec(argv: &[String]) -> Option<(Vec<String>, Vec<String>)> {
1237        let exec_pos = argv.iter().position(|a| a == "-exec")?;
1238        // Find the terminator: \; or ;
1239        let term_pos = argv[exec_pos + 1..]
1240            .iter()
1241            .position(|a| a == "\\;" || a == ";")
1242            .map(|p| p + exec_pos + 1)?;
1243        let template: Vec<String> = argv[exec_pos + 1..term_pos].to_vec();
1244        if template.is_empty() {
1245            return None;
1246        }
1247        let mut cleaned: Vec<String> = argv[..exec_pos].to_vec();
1248        cleaned.extend_from_slice(&argv[term_pos + 1..]);
1249        Some((template, cleaned))
1250    }
1251
1252    /// Shell-quote a path for safe interpolation into a command string.
1253    fn shell_quote(s: &str) -> String {
1254        if s.chars()
1255            .all(|c| c.is_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'))
1256        {
1257            s.to_string()
1258        } else {
1259            format!("'{}'", s.replace('\'', "'\\''"))
1260        }
1261    }
1262
1263    /// Handle `find ... -exec CMD {} \;` by running find for paths, then executing
1264    /// the command for each matched path via the shell.
1265    fn call_find_with_exec(&mut self, argv: &[String]) {
1266        let Some((template, cleaned_argv)) = Self::extract_find_exec(argv) else {
1267            // Malformed -exec (missing \;), fall through to normal find
1268            self.call_utility("find", argv);
1269            return;
1270        };
1271
1272        // Phase 1: run find with cleaned argv, capturing stdout
1273        let saved_stdout = std::mem::take(&mut self.vm.stdout);
1274        self.call_utility("find", &cleaned_argv);
1275        let find_output = std::mem::take(&mut self.vm.stdout);
1276        self.vm.stdout = saved_stdout;
1277
1278        // Phase 2: parse matched paths
1279        let paths_str = String::from_utf8_lossy(&find_output);
1280        let paths: Vec<&str> = paths_str.lines().filter(|l| !l.is_empty()).collect();
1281
1282        // Phase 3: execute the command for each path
1283        let mut last_status = 0i32;
1284        for path in paths {
1285            let cmd_line: String = template
1286                .iter()
1287                .map(|t| {
1288                    if t == "{}" {
1289                        Self::shell_quote(path)
1290                    } else {
1291                        t.clone()
1292                    }
1293                })
1294                .collect::<Vec<_>>()
1295                .join(" ");
1296            let sub_events = self.execute_input_inner(&cmd_line);
1297            self.merge_sub_events(sub_events);
1298            if self.vm.state.last_status != 0 {
1299                last_status = self.vm.state.last_status;
1300            }
1301        }
1302        self.vm.state.last_status = last_status;
1303    }
1304
1305    /// Handle `xargs` with actual command execution for non-echo commands.
1306    /// The existing xargs utility already formats correct command lines for
1307    /// non-echo; we capture those and execute them via the shell.
1308    fn call_xargs_with_exec(&mut self, argv: &[String]) {
1309        // Determine if xargs has a non-echo command by scanning past flags
1310        let mut has_non_echo = false;
1311        let mut i = 1;
1312        while i < argv.len() {
1313            let arg = &argv[i];
1314            if matches!(arg.as_str(), "-I" | "-n" | "-d" | "-P" | "-L") && i + 1 < argv.len() {
1315                i += 2;
1316            } else if matches!(arg.as_str(), "-0" | "--null" | "-t" | "-p") || arg.starts_with('-')
1317            {
1318                i += 1;
1319            } else {
1320                // First non-flag arg is the command
1321                if arg != "echo" {
1322                    has_non_echo = true;
1323                }
1324                break;
1325            }
1326        }
1327
1328        if !has_non_echo {
1329            self.call_utility("xargs", argv);
1330            return;
1331        }
1332
1333        // Run xargs utility — it outputs formatted command lines for non-echo
1334        let saved_stdout = std::mem::take(&mut self.vm.stdout);
1335        self.call_utility("xargs", argv);
1336        let xargs_output = std::mem::take(&mut self.vm.stdout);
1337        self.vm.stdout = saved_stdout;
1338
1339        // Execute each output line as a command
1340        let output_str = String::from_utf8_lossy(&xargs_output);
1341        let mut last_status = 0i32;
1342        for line in output_str.lines().filter(|l| !l.is_empty()) {
1343            let sub_events = self.execute_input_inner(line);
1344            self.merge_sub_events(sub_events);
1345            if self.vm.state.last_status != 0 {
1346                last_status = self.vm.state.last_status;
1347            }
1348        }
1349        self.vm.state.last_status = last_status;
1350    }
1351
1352    /// Invoke a utility command.
1353    fn call_utility(&mut self, cmd_name: &str, argv: &[String]) {
1354        let stdin_data = self.pending_stdin.take();
1355        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
1356        let mut output = UtilOutput::default();
1357        let cwd = self.vm.state.cwd.clone();
1358        let status = {
1359            let util_fn = self.utils.get(cmd_name).unwrap();
1360            let mut ctx = UtilContext {
1361                fs: &mut self.fs,
1362                output: &mut output,
1363                cwd: &cwd,
1364                stdin: stdin_data.as_deref(),
1365                state: Some(&self.vm.state),
1366                network: self.network.as_deref(),
1367            };
1368            util_fn(&mut ctx, &argv_refs)
1369        };
1370        self.vm.stdout.extend_from_slice(&output.stdout);
1371        self.vm.stderr.extend_from_slice(&output.stderr);
1372        self.vm.output_bytes += (output.stdout.len() + output.stderr.len()) as u64;
1373        self.vm.state.last_status = status;
1374    }
1375
1376    /// Execute an `if` command.
1377    fn execute_if(&mut self, if_cmd: &wasmsh_hir::HirIf) {
1378        let saved_suppress = self.exec.errexit_suppressed;
1379        self.exec.errexit_suppressed = true;
1380        self.execute_body(&if_cmd.condition);
1381        self.exec.errexit_suppressed = saved_suppress;
1382        if self.vm.state.last_status == 0 {
1383            self.execute_body(&if_cmd.then_body);
1384            return;
1385        }
1386        for elif in &if_cmd.elifs {
1387            let saved = self.exec.errexit_suppressed;
1388            self.exec.errexit_suppressed = true;
1389            self.execute_body(&elif.condition);
1390            self.exec.errexit_suppressed = saved;
1391            if self.vm.state.last_status == 0 {
1392                self.execute_body(&elif.then_body);
1393                return;
1394            }
1395        }
1396        if let Some(else_body) = &if_cmd.else_body {
1397            self.execute_body(else_body);
1398        }
1399    }
1400
1401    /// Execute a `while` loop.
1402    fn execute_while_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
1403        loop {
1404            if self.check_resource_limits() {
1405                break;
1406            }
1407            let saved = self.exec.errexit_suppressed;
1408            self.exec.errexit_suppressed = true;
1409            self.execute_body(&loop_cmd.condition);
1410            self.exec.errexit_suppressed = saved;
1411            if self.vm.state.last_status != 0 {
1412                break;
1413            }
1414            self.execute_body(&loop_cmd.body);
1415            if self.handle_loop_control() {
1416                break;
1417            }
1418        }
1419    }
1420
1421    /// Execute an `until` loop.
1422    fn execute_until_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
1423        loop {
1424            if self.check_resource_limits() {
1425                break;
1426            }
1427            let saved = self.exec.errexit_suppressed;
1428            self.exec.errexit_suppressed = true;
1429            self.execute_body(&loop_cmd.condition);
1430            self.exec.errexit_suppressed = saved;
1431            if self.vm.state.last_status == 0 {
1432                break;
1433            }
1434            self.execute_body(&loop_cmd.body);
1435            if self.handle_loop_control() {
1436                break;
1437            }
1438        }
1439    }
1440
1441    /// Handle loop control flow (break/continue/exit). Returns true if the loop should break.
1442    fn handle_loop_control(&mut self) -> bool {
1443        if self.exec.break_depth > 0 {
1444            self.exec.break_depth -= 1;
1445            return true;
1446        }
1447        if self.exec.loop_continue {
1448            self.exec.loop_continue = false;
1449        }
1450        self.exec.exit_requested.is_some()
1451    }
1452
1453    /// Execute a `for` loop.
1454    fn execute_for_loop(&mut self, for_cmd: &wasmsh_hir::HirFor) {
1455        let words = self.expand_for_words(for_cmd.words.as_deref());
1456        for word in words {
1457            if self.check_resource_limits() {
1458                break;
1459            }
1460            self.vm.state.set_var(for_cmd.var_name.clone(), word.into());
1461            self.execute_body(&for_cmd.body);
1462            if self.exec.break_depth > 0 {
1463                self.exec.break_depth -= 1;
1464                break;
1465            }
1466            if self.exec.loop_continue {
1467                self.exec.loop_continue = false;
1468                continue;
1469            }
1470            if self.exec.exit_requested.is_some() {
1471                break;
1472            }
1473        }
1474    }
1475
1476    /// Expand word list for `for` and `select` commands.
1477    fn expand_for_words(&mut self, words: Option<&[wasmsh_ast::Word]>) -> Vec<String> {
1478        if let Some(ws) = words {
1479            let resolved = self.resolve_command_subst(ws);
1480            let mut result = Vec::new();
1481            for w in &resolved {
1482                let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
1483                result.extend(expanded.fields);
1484            }
1485            let result: Vec<String> = result
1486                .into_iter()
1487                .flat_map(|arg| wasmsh_expand::expand_braces(&arg))
1488                .collect();
1489            self.expand_globs(result)
1490        } else {
1491            self.vm
1492                .state
1493                .positional
1494                .iter()
1495                .map(ToString::to_string)
1496                .collect()
1497        }
1498    }
1499
1500    /// Execute a `case` command.
1501    fn execute_case(&mut self, case_cmd: &wasmsh_hir::HirCase) {
1502        let nocasematch = self.vm.state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
1503        let value = wasmsh_expand::expand_word(&case_cmd.word, &mut self.vm.state);
1504        let mut i = 0;
1505        let mut fallthrough = false;
1506        while i < case_cmd.items.len() {
1507            let item = &case_cmd.items[i];
1508            let pattern_matched = if fallthrough {
1509                true
1510            } else {
1511                item.patterns.iter().any(|pattern| {
1512                    let pat = wasmsh_expand::expand_word(pattern, &mut self.vm.state);
1513                    if nocasematch {
1514                        glob_match_inner(
1515                            pat.to_lowercase().as_bytes(),
1516                            value.to_lowercase().as_bytes(),
1517                        )
1518                    } else {
1519                        glob_match_inner(pat.as_bytes(), value.as_bytes())
1520                    }
1521                })
1522            };
1523            if pattern_matched {
1524                self.execute_body(&item.body);
1525                match item.terminator {
1526                    CaseTerminator::Break => break,
1527                    CaseTerminator::Fallthrough => {
1528                        fallthrough = true;
1529                        i += 1;
1530                    }
1531                    CaseTerminator::ContinueTesting => {
1532                        fallthrough = false;
1533                        i += 1;
1534                    }
1535                }
1536            } else {
1537                fallthrough = false;
1538                i += 1;
1539            }
1540        }
1541    }
1542
1543    /// Execute a C-style `for (( init; cond; step ))` loop.
1544    fn execute_arith_for(&mut self, af: &wasmsh_hir::HirArithFor) {
1545        if !af.init.is_empty() {
1546            wasmsh_expand::eval_arithmetic(&af.init, &mut self.vm.state);
1547        }
1548        loop {
1549            if self.check_resource_limits() {
1550                break;
1551            }
1552            if !af.cond.is_empty() {
1553                let cond_val = wasmsh_expand::eval_arithmetic(&af.cond, &mut self.vm.state);
1554                if cond_val == 0 {
1555                    break;
1556                }
1557            }
1558            self.execute_body(&af.body);
1559            if self.handle_loop_control() {
1560                break;
1561            }
1562            if !af.step.is_empty() {
1563                wasmsh_expand::eval_arithmetic(&af.step, &mut self.vm.state);
1564            }
1565        }
1566    }
1567
1568    /// Execute a `select` command.
1569    fn execute_select(&mut self, sel: &wasmsh_hir::HirSelect) {
1570        self.collect_stdin_from_redirections(&sel.redirections);
1571
1572        let words: Vec<String> = if let Some(ws) = &sel.words {
1573            let resolved = self.resolve_command_subst(ws);
1574            let mut result = Vec::new();
1575            for w in &resolved {
1576                let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
1577                result.extend(expanded.fields);
1578            }
1579            result
1580        } else {
1581            self.vm
1582                .state
1583                .positional
1584                .iter()
1585                .map(ToString::to_string)
1586                .collect()
1587        };
1588
1589        if words.is_empty() {
1590            return;
1591        }
1592        for (idx, w) in words.iter().enumerate() {
1593            let line = format!("{}) {}\n", idx + 1, w);
1594            self.vm.stderr.extend_from_slice(line.as_bytes());
1595        }
1596
1597        let stdin_data = self.pending_stdin.take().unwrap_or_default();
1598        let input = String::from_utf8_lossy(&stdin_data);
1599        let first_line = input.lines().next().unwrap_or("");
1600
1601        self.vm.state.set_var(
1602            smol_str::SmolStr::from("REPLY"),
1603            smol_str::SmolStr::from(first_line.trim()),
1604        );
1605
1606        let selected = first_line.trim().parse::<usize>().ok().and_then(|n| {
1607            if n >= 1 && n <= words.len() {
1608                Some(&words[n - 1])
1609            } else {
1610                None
1611            }
1612        });
1613
1614        if let Some(word) = selected {
1615            self.vm
1616                .state
1617                .set_var(sel.var_name.clone(), smol_str::SmolStr::from(word.as_str()));
1618        } else {
1619            self.vm
1620                .state
1621                .set_var(sel.var_name.clone(), smol_str::SmolStr::default());
1622        }
1623
1624        self.execute_body(&sel.body);
1625    }
1626
1627    // ---- [[ ]] extended test evaluation ----
1628
1629    /// Expand a word inside `[[ ]]` — no word splitting or glob expansion.
1630    fn dbl_bracket_expand(&mut self, word: &wasmsh_ast::Word) -> String {
1631        let resolved = self.resolve_command_subst(std::slice::from_ref(word));
1632        wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
1633    }
1634
1635    /// Evaluate a `[[ expression ]]` command. Returns true for exit-status 0.
1636    fn eval_double_bracket(&mut self, words: &[wasmsh_ast::Word]) -> bool {
1637        // Expand all words (no splitting/globbing) into string tokens for the evaluator
1638        let tokens: Vec<String> = words.iter().map(|w| self.dbl_bracket_expand(w)).collect();
1639        let mut pos = 0;
1640        dbl_bracket_eval_or(&tokens, &mut pos, &self.fs, &mut self.vm.state)
1641    }
1642
1643    fn resolve_cwd_path(&self, path: &str) -> String {
1644        if path.starts_with('/') {
1645            wasmsh_fs::normalize_path(path)
1646        } else {
1647            wasmsh_fs::normalize_path(&format!("{}/{}", self.vm.state.cwd, path))
1648        }
1649    }
1650
1651    /// Execute `alias [name[='value'] ...]`.
1652    fn execute_alias(&mut self, argv: &[String]) {
1653        let args = &argv[1..];
1654        if args.is_empty() {
1655            // List all aliases
1656            for (name, value) in &self.aliases {
1657                let line = format!("alias {name}='{value}'\n");
1658                self.vm.stdout.extend_from_slice(line.as_bytes());
1659            }
1660            self.vm.state.last_status = 0;
1661            return;
1662        }
1663        for arg in args {
1664            if let Some(eq_pos) = arg.find('=') {
1665                let name = &arg[..eq_pos];
1666                let value = &arg[eq_pos + 1..];
1667                self.aliases.insert(name.to_string(), value.to_string());
1668            } else {
1669                // Show specific alias
1670                if let Some(value) = self.aliases.get(arg.as_str()) {
1671                    let line = format!("alias {arg}='{value}'\n");
1672                    self.vm.stdout.extend_from_slice(line.as_bytes());
1673                } else {
1674                    let msg = format!("alias: {arg}: not found\n");
1675                    self.vm.stderr.extend_from_slice(msg.as_bytes());
1676                    self.vm.state.last_status = 1;
1677                    return;
1678                }
1679            }
1680        }
1681        self.vm.state.last_status = 0;
1682    }
1683
1684    /// Execute `unalias [-a] name ...`.
1685    fn execute_unalias(&mut self, argv: &[String]) {
1686        let args = &argv[1..];
1687        if args.is_empty() {
1688            self.vm
1689                .stderr
1690                .extend_from_slice(b"unalias: usage: unalias [-a] name ...\n");
1691            self.vm.state.last_status = 1;
1692            return;
1693        }
1694        for arg in args {
1695            if arg == "-a" {
1696                self.aliases.clear();
1697            } else if self.aliases.shift_remove(arg.as_str()).is_none() {
1698                let msg = format!("unalias: {arg}: not found\n");
1699                self.vm.stderr.extend_from_slice(msg.as_bytes());
1700                self.vm.state.last_status = 1;
1701                return;
1702            }
1703        }
1704        self.vm.state.last_status = 0;
1705    }
1706
1707    /// Execute `type name ...` — report how each name would be interpreted.
1708    /// Checks aliases, functions, builtins, and utilities in that order.
1709    fn execute_type(&mut self, argv: &[String]) {
1710        let mut status = 0;
1711        for name in &argv[1..] {
1712            if self.aliases.contains_key(name.as_str()) {
1713                let val = self.aliases.get(name.as_str()).unwrap();
1714                let msg = format!("{name} is aliased to `{val}'\n");
1715                self.vm.stdout.extend_from_slice(msg.as_bytes());
1716            } else if self.functions.contains_key(name.as_str()) {
1717                let msg = format!("{name} is a function\n");
1718                self.vm.stdout.extend_from_slice(msg.as_bytes());
1719            } else if self.builtins.is_builtin(name) {
1720                let msg = format!("{name} is a shell builtin\n");
1721                self.vm.stdout.extend_from_slice(msg.as_bytes());
1722            } else if self.utils.is_utility(name) {
1723                let msg = format!("{name} is a shell utility\n");
1724                self.vm.stdout.extend_from_slice(msg.as_bytes());
1725            } else {
1726                let msg = format!("wasmsh: type: {name}: not found\n");
1727                self.vm.stderr.extend_from_slice(msg.as_bytes());
1728                status = 1;
1729            }
1730        }
1731        self.vm.state.last_status = status;
1732    }
1733
1734    /// Execute `builtin name [args...]` — skip alias and function lookup,
1735    /// invoke the named builtin directly.
1736    fn execute_builtin_keyword(&mut self, argv: &[String]) {
1737        if argv.len() < 2 {
1738            self.vm.state.last_status = 0;
1739            return;
1740        }
1741        let cmd_name = &argv[1];
1742        let builtin_argv: Vec<String> = argv[1..].to_vec();
1743        if let Some(builtin_fn) = self.builtins.get(cmd_name) {
1744            let stdin_data = self.pending_stdin.take();
1745            let argv_refs: Vec<&str> = builtin_argv.iter().map(String::as_str).collect();
1746            let mut sink = wasmsh_builtins::VecSink::default();
1747            let status = {
1748                let mut ctx = wasmsh_builtins::BuiltinContext {
1749                    state: &mut self.vm.state,
1750                    output: &mut sink,
1751                    fs: Some(&self.fs),
1752                    stdin: stdin_data.as_deref(),
1753                };
1754                builtin_fn(&mut ctx, &argv_refs)
1755            };
1756            self.vm.stdout.extend_from_slice(&sink.stdout);
1757            self.vm.stderr.extend_from_slice(&sink.stderr);
1758            self.vm.output_bytes += (sink.stdout.len() + sink.stderr.len()) as u64;
1759            self.vm.state.last_status = status;
1760        } else {
1761            let msg = format!("builtin: {cmd_name}: not a shell builtin\n");
1762            self.vm.stderr.extend_from_slice(msg.as_bytes());
1763            self.vm.state.last_status = 1;
1764        }
1765    }
1766
1767    /// Execute `mapfile`/`readarray` — read stdin lines into an indexed array.
1768    /// Supports `-t` (strip trailing newline). Default array: MAPFILE.
1769    fn execute_mapfile(&mut self, argv: &[String]) {
1770        let (strip_newline, array_name) = Self::parse_mapfile_args(&argv[1..]);
1771        let data = self.pending_stdin.take().unwrap_or_default();
1772        let text = String::from_utf8_lossy(&data);
1773
1774        let name_key = smol_str::SmolStr::from(array_name.as_str());
1775        self.vm.state.init_indexed_array(name_key.clone());
1776        self.populate_mapfile_array(&name_key, &text, strip_newline);
1777        self.vm.state.last_status = 0;
1778    }
1779
1780    fn parse_mapfile_args(args: &[String]) -> (bool, String) {
1781        let mut strip_newline = false;
1782        let mut positional: Vec<&str> = Vec::new();
1783        for arg in args {
1784            match arg.as_str() {
1785                "-t" => strip_newline = true,
1786                _ => positional.push(arg),
1787            }
1788        }
1789        let array_name = positional
1790            .last()
1791            .map_or("MAPFILE".to_string(), ToString::to_string);
1792        (strip_newline, array_name)
1793    }
1794
1795    fn populate_mapfile_array(
1796        &mut self,
1797        name_key: &smol_str::SmolStr,
1798        text: &str,
1799        strip_newline: bool,
1800    ) {
1801        let mut idx = 0;
1802        for line in text.split('\n') {
1803            if line.is_empty() && idx > 0 {
1804                continue;
1805            }
1806            let value = if strip_newline {
1807                line.to_string()
1808            } else {
1809                format!("{line}\n")
1810            };
1811            self.vm.state.set_array_element(
1812                name_key.clone(),
1813                &idx.to_string(),
1814                smol_str::SmolStr::from(value.as_str()),
1815            );
1816            idx += 1;
1817        }
1818    }
1819
1820    /// Search `$PATH` directories in the VFS for a file. Returns the first match.
1821    fn search_path_for_file(&self, filename: &str) -> Option<String> {
1822        let path_var = self.vm.state.get_var("PATH")?;
1823        for dir in path_var.split(':') {
1824            if dir.is_empty() {
1825                continue;
1826            }
1827            let candidate = format!("{dir}/{filename}");
1828            let full = self.resolve_cwd_path(&candidate);
1829            if self.fs.stat(&full).is_ok() {
1830                return Some(full);
1831            }
1832        }
1833        None
1834    }
1835
1836    fn should_errexit(&self, and_or: &HirAndOr) -> bool {
1837        !self.exec.errexit_suppressed
1838            && and_or.rest.is_empty()
1839            && !and_or.first.negated
1840            && self.vm.state.get_var("SHOPT_e").as_deref() == Some("1")
1841            && self.vm.state.last_status != 0
1842            && self.exec.exit_requested.is_none()
1843    }
1844
1845    /// Execute `let expr1 expr2 ...` — evaluate each as arithmetic.
1846    /// Exit status: 0 if the last expression is non-zero, 1 if zero.
1847    fn execute_let(&mut self, argv: &[String]) {
1848        if argv.len() < 2 {
1849            self.vm
1850                .stderr
1851                .extend_from_slice(b"let: expression expected\n");
1852            self.vm.state.last_status = 1;
1853            return;
1854        }
1855        let mut last_val: i64 = 0;
1856        for expr in &argv[1..] {
1857            last_val = wasmsh_expand::eval_arithmetic(expr, &mut self.vm.state);
1858        }
1859        self.vm.state.last_status = i32::from(last_val == 0);
1860    }
1861
1862    /// Known `shopt` option names.
1863    const SHOPT_OPTIONS: &'static [&'static str] = &[
1864        "extglob",
1865        "nullglob",
1866        "dotglob",
1867        "globstar",
1868        "nocasematch",
1869        "nocaseglob",
1870        "failglob",
1871        "lastpipe",
1872        "expand_aliases",
1873    ];
1874
1875    /// Execute `shopt [-s|-u] [optname ...]`.
1876    fn execute_shopt(&mut self, argv: &[String]) {
1877        let (set_mode, names) = Self::parse_shopt_args(&argv[1..]);
1878        if let Some(enable) = set_mode {
1879            self.shopt_set_options(&names, enable);
1880        } else {
1881            self.shopt_print_options(&names);
1882        }
1883    }
1884
1885    fn parse_shopt_args(args: &[String]) -> (Option<bool>, Vec<&str>) {
1886        let mut set_mode = None;
1887        let mut names = Vec::new();
1888
1889        for arg in args {
1890            match arg.as_str() {
1891                "-s" => set_mode = Some(true),
1892                "-u" => set_mode = Some(false),
1893                _ => names.push(arg.as_str()),
1894            }
1895        }
1896
1897        (set_mode, names)
1898    }
1899
1900    /// Set shopt options (`-s` or `-u`).
1901    fn shopt_set_options(&mut self, names: &[&str], enable: bool) {
1902        if names.is_empty() {
1903            self.vm
1904                .stderr
1905                .extend_from_slice(b"shopt: option name required\n");
1906            self.vm.state.last_status = 1;
1907            return;
1908        }
1909        let val = if enable { "1" } else { "0" };
1910        for name in names {
1911            if self.reject_invalid_shopt_name(name) {
1912                return;
1913            }
1914            self.set_shopt_value(name, val);
1915        }
1916        self.vm.state.last_status = 0;
1917    }
1918
1919    /// Print shopt option statuses. If `names` is empty, print all.
1920    fn shopt_print_options(&mut self, names: &[&str]) {
1921        let options_to_print: Vec<&str> = if names.is_empty() {
1922            Self::SHOPT_OPTIONS.to_vec()
1923        } else {
1924            names.to_vec()
1925        };
1926        for name in &options_to_print {
1927            if self.reject_invalid_shopt_name(name) {
1928                return;
1929            }
1930            let enabled = self.get_shopt_value(name);
1931            let status_str = if enabled { "on" } else { "off" };
1932            let line = format!("{name}\t{status_str}\n");
1933            self.vm.stdout.extend_from_slice(line.as_bytes());
1934        }
1935        self.vm.state.last_status = 0;
1936    }
1937
1938    fn reject_invalid_shopt_name(&mut self, name: &str) -> bool {
1939        if Self::SHOPT_OPTIONS.contains(&name) {
1940            return false;
1941        }
1942
1943        let msg = format!("shopt: {name}: invalid shell option name\n");
1944        self.vm.stderr.extend_from_slice(msg.as_bytes());
1945        self.vm.state.last_status = 1;
1946        true
1947    }
1948
1949    fn shopt_var_name(name: &str) -> String {
1950        format!("SHOPT_{name}")
1951    }
1952
1953    fn set_shopt_value(&mut self, name: &str, value: &str) {
1954        let var = Self::shopt_var_name(name);
1955        self.vm.state.set_var(
1956            smol_str::SmolStr::from(var.as_str()),
1957            smol_str::SmolStr::from(value),
1958        );
1959    }
1960
1961    fn get_shopt_value(&self, name: &str) -> bool {
1962        let var = Self::shopt_var_name(name);
1963        self.vm.state.get_var(&var).as_deref() == Some("1")
1964    }
1965
1966    /// Execute `declare`/`typeset` with flag parsing.
1967    /// Supports: -i, -a, -A, -x, -r, -l, -u, -p, -n, name=value.
1968    fn execute_declare(&mut self, argv: &[String]) {
1969        let (flags, names) = parse_declare_flags(argv);
1970
1971        if flags.is_print {
1972            self.declare_print(argv, &names);
1973            return;
1974        }
1975
1976        for &idx in &names {
1977            self.declare_one_name(argv, idx, &flags);
1978        }
1979        self.vm.state.last_status = 0;
1980    }
1981
1982    /// Handle `declare -p` printing.
1983    fn declare_print(&mut self, argv: &[String], names: &[usize]) {
1984        if names.is_empty() {
1985            let vars: Vec<(String, String)> = self
1986                .vm
1987                .state
1988                .env
1989                .scopes
1990                .iter()
1991                .flat_map(|scope| {
1992                    scope
1993                        .iter()
1994                        .map(|(n, v)| (n.to_string(), v.value.as_scalar().to_string()))
1995                })
1996                .collect();
1997            for (name, val) in &vars {
1998                let line = format!("declare -- {name}=\"{val}\"\n");
1999                self.vm.stdout.extend_from_slice(line.as_bytes());
2000            }
2001        } else {
2002            for &idx in names {
2003                let name_arg = &argv[idx];
2004                let name = name_arg
2005                    .find('=')
2006                    .map_or(name_arg.as_str(), |eq| &name_arg[..eq]);
2007                if let Some(var) = self.vm.state.env.get(name) {
2008                    let val = var.value.as_scalar();
2009                    let line = format!("declare -- {name}=\"{val}\"\n");
2010                    self.vm.stdout.extend_from_slice(line.as_bytes());
2011                }
2012            }
2013        }
2014        self.vm.state.last_status = 0;
2015    }
2016
2017    /// Process a single name in a `declare`/`typeset` command.
2018    fn declare_one_name(&mut self, argv: &[String], idx: usize, flags: &DeclareFlags) {
2019        let name_arg = &argv[idx];
2020        let (name, value) = if let Some(eq) = name_arg.find('=') {
2021            (&name_arg[..eq], Some(&name_arg[eq + 1..]))
2022        } else {
2023            (name_arg.as_str(), None)
2024        };
2025
2026        if flags.is_assoc {
2027            self.vm
2028                .state
2029                .init_assoc_array(smol_str::SmolStr::from(name));
2030        } else if flags.is_indexed {
2031            self.vm
2032                .state
2033                .init_indexed_array(smol_str::SmolStr::from(name));
2034        }
2035
2036        if let Some(val) = value {
2037            self.declare_assign_value(name, val, flags);
2038        } else if !flags.is_assoc && !flags.is_indexed && self.vm.state.get_var(name).is_none() {
2039            self.vm
2040                .state
2041                .set_var(smol_str::SmolStr::from(name), smol_str::SmolStr::default());
2042        }
2043
2044        self.declare_apply_attributes(name, flags);
2045
2046        if flags.is_nameref {
2047            self.declare_apply_nameref(name);
2048        }
2049    }
2050
2051    /// Assign a value in `declare`, handling compound arrays and scalar transforms.
2052    fn declare_assign_value(&mut self, name: &str, val: &str, flags: &DeclareFlags) {
2053        if val.starts_with('(') && val.ends_with(')') {
2054            self.declare_assign_compound(name, &val[1..val.len() - 1], flags);
2055            return;
2056        }
2057        let final_val = Self::transform_declare_scalar(val, flags, &mut self.vm.state);
2058        self.vm.state.set_var(
2059            smol_str::SmolStr::from(name),
2060            smol_str::SmolStr::from(final_val.as_str()),
2061        );
2062    }
2063
2064    fn declare_assign_compound(&mut self, name: &str, inner: &str, flags: &DeclareFlags) {
2065        let name_key = smol_str::SmolStr::from(name);
2066        if flags.is_assoc || inner.contains("]=") {
2067            self.declare_assign_assoc_compound(&name_key, inner);
2068        } else {
2069            self.declare_assign_indexed_compound(&name_key, inner);
2070        }
2071    }
2072
2073    fn declare_assign_assoc_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2074        self.vm.state.init_assoc_array(name_key.clone());
2075        for pair in Self::parse_assoc_pairs(inner) {
2076            self.vm.state.set_array_element(
2077                name_key.clone(),
2078                &pair.0,
2079                smol_str::SmolStr::from(pair.1.as_str()),
2080            );
2081        }
2082    }
2083
2084    fn declare_assign_indexed_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2085        let elements = Self::parse_array_elements(inner);
2086        self.vm.state.init_indexed_array(name_key.clone());
2087        for (i, elem) in elements.iter().enumerate() {
2088            self.vm
2089                .state
2090                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
2091        }
2092    }
2093
2094    fn transform_declare_scalar(val: &str, flags: &DeclareFlags, state: &mut ShellState) -> String {
2095        if flags.is_integer {
2096            wasmsh_expand::eval_arithmetic(val, state).to_string()
2097        } else if flags.is_lower {
2098            val.to_lowercase()
2099        } else if flags.is_upper {
2100            val.to_uppercase()
2101        } else {
2102            val.to_string()
2103        }
2104    }
2105
2106    /// Apply export, readonly, integer attributes after declare assignment.
2107    fn declare_apply_attributes(&mut self, name: &str, flags: &DeclareFlags) {
2108        if let Some(var) = self.vm.state.env.get_mut(name) {
2109            if flags.is_export {
2110                var.exported = true;
2111            }
2112            if flags.is_readonly {
2113                var.readonly = true;
2114            }
2115            if flags.is_integer {
2116                var.integer = true;
2117            }
2118        }
2119    }
2120
2121    /// Apply nameref attribute for `declare -n`.
2122    fn declare_apply_nameref(&mut self, name: &str) {
2123        let target_value = if let Some(eq_pos) = name.find('=') {
2124            smol_str::SmolStr::from(&name[eq_pos + 1..])
2125        } else if let Some(var) = self.vm.state.env.get(name) {
2126            var.value.as_scalar()
2127        } else {
2128            smol_str::SmolStr::default()
2129        };
2130        let actual_name = name.find('=').map_or(name, |eq| &name[..eq]);
2131        self.vm.state.env.set(
2132            smol_str::SmolStr::from(actual_name),
2133            wasmsh_state::ShellVar {
2134                value: wasmsh_state::VarValue::Scalar(target_value),
2135                exported: false,
2136                readonly: false,
2137                integer: false,
2138                nameref: true,
2139            },
2140        );
2141    }
2142
2143    fn should_stop_execution(&self) -> bool {
2144        self.exec.break_depth > 0
2145            || self.exec.loop_continue
2146            || self.exec.exit_requested.is_some()
2147            || self.exec.resource_exhausted
2148    }
2149
2150    /// Check resource limits (step budget, output limit, cancellation).
2151    /// Returns true if execution should stop. Emits a diagnostic on first violation.
2152    fn check_resource_limits(&mut self) -> bool {
2153        if self.exec.resource_exhausted {
2154            return true;
2155        }
2156        // Step budget
2157        self.vm.steps += 1;
2158        if self.vm.limits.step_limit > 0 && self.vm.steps >= self.vm.limits.step_limit {
2159            self.exec.resource_exhausted = true;
2160            self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2161                level: wasmsh_vm::DiagLevel::Error,
2162                category: wasmsh_vm::DiagCategory::Budget,
2163                message: format!(
2164                    "step budget exhausted: {} steps (limit: {})",
2165                    self.vm.steps, self.vm.limits.step_limit
2166                ),
2167            });
2168            return true;
2169        }
2170        // Cancellation
2171        if self.vm.cancellation_token().is_cancelled() {
2172            self.exec.resource_exhausted = true;
2173            self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2174                level: wasmsh_vm::DiagLevel::Error,
2175                category: wasmsh_vm::DiagCategory::Budget,
2176                message: "execution cancelled".to_string(),
2177            });
2178            return true;
2179        }
2180        // Output limit
2181        if self.vm.limits.output_byte_limit > 0
2182            && self.vm.output_bytes > self.vm.limits.output_byte_limit
2183        {
2184            self.exec.resource_exhausted = true;
2185            self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2186                level: wasmsh_vm::DiagLevel::Error,
2187                category: wasmsh_vm::DiagCategory::Budget,
2188                message: format!(
2189                    "output limit exceeded: {} bytes (limit: {})",
2190                    self.vm.output_bytes, self.vm.limits.output_byte_limit
2191                ),
2192            });
2193            return true;
2194        }
2195        false
2196    }
2197
2198    fn execute_body(&mut self, body: &[HirCompleteCommand]) {
2199        for cc in body {
2200            if self.should_stop_execution() || self.check_resource_limits() {
2201                break;
2202            }
2203            self.execute_complete_command(cc);
2204        }
2205    }
2206
2207    fn execute_complete_command(&mut self, cc: &HirCompleteCommand) {
2208        for and_or in &cc.list {
2209            if self.should_stop_execution() {
2210                break;
2211            }
2212            self.execute_pipeline_chain(and_or);
2213            if self.should_errexit(and_or) {
2214                self.exec.exit_requested = Some(self.vm.state.last_status);
2215            }
2216        }
2217    }
2218
2219    /// Expand a word value via command substitution and word expansion.
2220    fn expand_assignment_value(&mut self, value: Option<&wasmsh_ast::Word>) -> String {
2221        if let Some(w) = value {
2222            let resolved = self.resolve_command_subst(std::slice::from_ref(w));
2223            wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
2224        } else {
2225            String::new()
2226        }
2227    }
2228
2229    /// Execute a variable assignment, handling array syntax:
2230    /// - `name=(val1 val2 ...)` -- indexed array compound assignment
2231    /// - `name[idx]=val` -- single element assignment
2232    /// - `name+=(val1 val2 ...)` -- array append
2233    /// - Plain `name=val` -- scalar assignment
2234    fn execute_assignment(
2235        &mut self,
2236        raw_name: &smol_str::SmolStr,
2237        value: Option<&wasmsh_ast::Word>,
2238    ) {
2239        let (name_str, is_append) = Self::split_assignment_name(raw_name.as_str());
2240        if self.try_assign_array_element(name_str, value) {
2241            return;
2242        }
2243
2244        let val_str = self.expand_assignment_value(value);
2245        if val_str.starts_with('(') && val_str.ends_with(')') {
2246            self.assign_compound_array(name_str, &val_str, is_append);
2247            return;
2248        }
2249
2250        let final_val = self.resolve_scalar_assignment_value(name_str, &val_str, is_append);
2251        self.vm
2252            .state
2253            .set_var(smol_str::SmolStr::from(name_str), final_val.into());
2254    }
2255
2256    fn split_assignment_name(name: &str) -> (&str, bool) {
2257        if let Some(stripped) = name.strip_suffix('+') {
2258            (stripped, true)
2259        } else {
2260            (name, false)
2261        }
2262    }
2263
2264    fn parse_array_element_assignment(name: &str) -> Option<(&str, &str)> {
2265        let bracket_pos = name.find('[')?;
2266        name.ends_with(']')
2267            .then_some((&name[..bracket_pos], &name[bracket_pos + 1..name.len() - 1]))
2268    }
2269
2270    fn try_assign_array_element(&mut self, name: &str, value: Option<&wasmsh_ast::Word>) -> bool {
2271        let Some((base, index)) = Self::parse_array_element_assignment(name) else {
2272            return false;
2273        };
2274        let val = self.expand_assignment_value(value);
2275        self.vm
2276            .state
2277            .set_array_element(smol_str::SmolStr::from(base), index, val.into());
2278        true
2279    }
2280
2281    fn resolve_scalar_assignment_value(
2282        &mut self,
2283        name: &str,
2284        value: &str,
2285        is_append: bool,
2286    ) -> String {
2287        if self.vm.state.env.get(name).is_some_and(|v| v.integer) {
2288            return self.eval_integer_assignment(name, value, is_append);
2289        }
2290        if is_append {
2291            return format!(
2292                "{}{}",
2293                self.vm.state.get_var(name).unwrap_or_default(),
2294                value
2295            );
2296        }
2297        value.to_string()
2298    }
2299
2300    fn eval_integer_assignment(&mut self, name: &str, value: &str, is_append: bool) -> String {
2301        let arith_input = if is_append {
2302            format!(
2303                "{}+{}",
2304                self.vm.state.get_var(name).unwrap_or_default(),
2305                value
2306            )
2307        } else {
2308            value.to_string()
2309        };
2310        wasmsh_expand::eval_arithmetic(&arith_input, &mut self.vm.state).to_string()
2311    }
2312
2313    /// Assign a compound array value `(...)` to a variable.
2314    fn assign_compound_array(&mut self, name_str: &str, val_str: &str, is_append: bool) {
2315        let inner = &val_str[1..val_str.len() - 1];
2316        let elements = Self::parse_array_elements(inner);
2317        let name_key = smol_str::SmolStr::from(name_str);
2318
2319        if is_append {
2320            self.vm.state.append_array(name_str, elements);
2321            return;
2322        }
2323
2324        if Self::is_assoc_array_assignment(inner, &elements) {
2325            self.assign_assoc_array(&name_key, inner);
2326            return;
2327        }
2328        self.assign_indexed_array(&name_key, &elements);
2329    }
2330
2331    fn is_assoc_array_assignment(inner: &str, elements: &[smol_str::SmolStr]) -> bool {
2332        !elements.is_empty() && inner.contains('[') && inner.contains("]=")
2333    }
2334
2335    fn assign_assoc_array(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2336        self.vm.state.init_assoc_array(name_key.clone());
2337        for (key, value) in Self::parse_assoc_pairs(inner) {
2338            self.vm.state.set_array_element(
2339                name_key.clone(),
2340                &key,
2341                smol_str::SmolStr::from(value.as_str()),
2342            );
2343        }
2344    }
2345
2346    fn assign_indexed_array(
2347        &mut self,
2348        name_key: &smol_str::SmolStr,
2349        elements: &[smol_str::SmolStr],
2350    ) {
2351        self.vm.state.init_indexed_array(name_key.clone());
2352        for (i, elem) in elements.iter().enumerate() {
2353            self.vm
2354                .state
2355                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
2356        }
2357    }
2358
2359    fn push_array_element(elements: &mut Vec<smol_str::SmolStr>, current: &mut String) {
2360        if current.is_empty() {
2361            return;
2362        }
2363        elements.push(smol_str::SmolStr::from(current.as_str()));
2364        current.clear();
2365    }
2366
2367    /// Parse space-separated array elements from the inner content of `(...)`.
2368    /// Respects quoting (single and double quotes).
2369    fn parse_array_elements(inner: &str) -> Vec<smol_str::SmolStr> {
2370        let mut elements = Vec::new();
2371        let mut current = String::new();
2372        let mut state = ArrayParseState::default();
2373
2374        for ch in inner.chars() {
2375            match state.process_char(ch) {
2376                ArrayCharAction::Append(c) => current.push(c),
2377                ArrayCharAction::Skip => {}
2378                ArrayCharAction::SplitField => {
2379                    Self::push_array_element(&mut elements, &mut current);
2380                }
2381            }
2382        }
2383        Self::push_array_element(&mut elements, &mut current);
2384        elements
2385    }
2386
2387    /// Parse `[key]=value` pairs from associative array compound assignment.
2388    fn parse_assoc_pairs(inner: &str) -> Vec<(String, String)> {
2389        let mut pairs = Vec::new();
2390        let mut pos = 0;
2391        let bytes = inner.as_bytes();
2392
2393        while pos < bytes.len() {
2394            Self::skip_ascii_whitespace(bytes, &mut pos);
2395            if pos >= bytes.len() {
2396                break;
2397            }
2398            if let Some(key) = Self::parse_assoc_key(inner, &mut pos) {
2399                pairs.push((key, Self::parse_assoc_value(inner, &mut pos)));
2400                continue;
2401            }
2402            Self::skip_non_whitespace(bytes, &mut pos);
2403        }
2404        pairs
2405    }
2406
2407    fn skip_ascii_whitespace(bytes: &[u8], pos: &mut usize) {
2408        while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() {
2409            *pos += 1;
2410        }
2411    }
2412
2413    fn skip_non_whitespace(bytes: &[u8], pos: &mut usize) {
2414        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
2415            *pos += 1;
2416        }
2417    }
2418
2419    fn parse_assoc_key(inner: &str, pos: &mut usize) -> Option<String> {
2420        let bytes = inner.as_bytes();
2421        if *pos >= bytes.len() || bytes[*pos] != b'[' {
2422            return None;
2423        }
2424
2425        *pos += 1;
2426        let key_start = *pos;
2427        while *pos < bytes.len() && bytes[*pos] != b']' {
2428            *pos += 1;
2429        }
2430        let key = inner[key_start..*pos].to_string();
2431        if *pos < bytes.len() {
2432            *pos += 1;
2433        }
2434        if *pos < bytes.len() && bytes[*pos] == b'=' {
2435            *pos += 1;
2436        }
2437        Some(key)
2438    }
2439
2440    /// Parse a single value in an associative array assignment (may be quoted).
2441    fn parse_assoc_value(inner: &str, pos: &mut usize) -> String {
2442        let bytes = inner.as_bytes();
2443        match bytes.get(*pos).copied() {
2444            Some(b'"') => Self::parse_double_quoted_assoc_value(bytes, pos),
2445            Some(b'\'') => Self::parse_single_quoted_assoc_value(bytes, pos),
2446            _ => Self::parse_unquoted_assoc_value(bytes, pos),
2447        }
2448    }
2449
2450    fn parse_double_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2451        let mut value = String::new();
2452        *pos += 1;
2453        while *pos < bytes.len() && bytes[*pos] != b'"' {
2454            if bytes[*pos] == b'\\' && *pos + 1 < bytes.len() {
2455                *pos += 1;
2456            }
2457            value.push(bytes[*pos] as char);
2458            *pos += 1;
2459        }
2460        if *pos < bytes.len() {
2461            *pos += 1;
2462        }
2463        value
2464    }
2465
2466    fn parse_single_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2467        let mut value = String::new();
2468        *pos += 1;
2469        while *pos < bytes.len() && bytes[*pos] != b'\'' {
2470            value.push(bytes[*pos] as char);
2471            *pos += 1;
2472        }
2473        if *pos < bytes.len() {
2474            *pos += 1;
2475        }
2476        value
2477    }
2478
2479    fn parse_unquoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2480        let mut value = String::new();
2481        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
2482            value.push(bytes[*pos] as char);
2483            *pos += 1;
2484        }
2485        value
2486    }
2487
2488    /// Maximum number of arguments after glob expansion.
2489    const MAX_GLOB_RESULTS: usize = 10_000;
2490
2491    /// Expand glob patterns in argv against the VFS.
2492    /// Supports: basic glob (`*`, `?`, `[...]`), globstar (`**`), nullglob,
2493    /// dotglob, and extglob patterns.
2494    /// When `set -f` (noglob) is active, glob expansion is skipped entirely.
2495    fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
2496        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
2497            return argv;
2498        }
2499        let nullglob = self.get_shopt_value("nullglob");
2500        let dotglob = self.get_shopt_value("dotglob");
2501        let globstar = self.get_shopt_value("globstar");
2502        let extglob = self.get_shopt_value("extglob");
2503
2504        let mut result = Vec::new();
2505        for arg in argv {
2506            result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
2507        }
2508        result.truncate(Self::MAX_GLOB_RESULTS);
2509        result
2510    }
2511
2512    #[allow(clippy::fn_params_excessive_bools)]
2513    fn expand_glob_arg(
2514        &self,
2515        arg: String,
2516        nullglob: bool,
2517        dotglob: bool,
2518        globstar: bool,
2519        extglob: bool,
2520    ) -> Vec<String> {
2521        if !Self::is_glob_pattern(&arg, extglob) {
2522            return vec![arg];
2523        }
2524        if globstar && arg.contains("**") {
2525            return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
2526        }
2527        self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
2528    }
2529
2530    fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
2531        let has_bracket_class = arg.contains('[') && arg.contains(']');
2532        arg.contains('*')
2533            || arg.contains('?')
2534            || has_bracket_class
2535            || (extglob && has_extglob_pattern(arg))
2536    }
2537
2538    fn expand_globstar_arg(
2539        &self,
2540        arg: String,
2541        nullglob: bool,
2542        dotglob: bool,
2543        extglob: bool,
2544    ) -> Vec<String> {
2545        let mut matches = self.expand_globstar(&arg, dotglob, extglob);
2546        matches.sort();
2547        self.finalize_glob_matches(arg, matches, nullglob)
2548    }
2549
2550    fn expand_standard_glob_arg(
2551        &self,
2552        arg: String,
2553        nullglob: bool,
2554        dotglob: bool,
2555        extglob: bool,
2556    ) -> Vec<String> {
2557        let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
2558            return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
2559        };
2560        let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
2561        self.finalize_glob_matches(arg, matches, nullglob)
2562    }
2563
2564    fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
2565        let Some(slash_pos) = arg.rfind('/') else {
2566            return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
2567        };
2568
2569        let dir_part = &arg[..=slash_pos];
2570        if Self::path_segment_has_glob(dir_part) {
2571            return None;
2572        }
2573
2574        Some((
2575            self.resolve_cwd_path(dir_part),
2576            arg[slash_pos + 1..].to_string(),
2577            Some(dir_part.to_string()),
2578        ))
2579    }
2580
2581    fn path_segment_has_glob(path: &str) -> bool {
2582        path.contains('*') || path.contains('?') || path.contains('[')
2583    }
2584
2585    fn read_glob_matches(
2586        &self,
2587        dir: &str,
2588        pattern: &str,
2589        prefix: Option<&str>,
2590        dotglob: bool,
2591        extglob: bool,
2592    ) -> Vec<String> {
2593        let Ok(entries) = self.fs.read_dir(dir) else {
2594            return Vec::new();
2595        };
2596
2597        let mut matches: Vec<String> = entries
2598            .iter()
2599            .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
2600            .map(|e| match prefix {
2601                Some(prefix) => format!("{prefix}{}", e.name),
2602                None => e.name.clone(),
2603            })
2604            .collect();
2605        matches.sort();
2606        matches
2607    }
2608
2609    #[allow(clippy::unused_self)]
2610    fn finalize_glob_matches(
2611        &self,
2612        arg: String,
2613        matches: Vec<String>,
2614        nullglob: bool,
2615    ) -> Vec<String> {
2616        if !matches.is_empty() {
2617            return matches;
2618        }
2619        if nullglob {
2620            Vec::new()
2621        } else {
2622            vec![arg]
2623        }
2624    }
2625
2626    /// Expand a globstar (**) pattern against the VFS with recursive directory traversal.
2627    fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
2628        // Split pattern into segments by /
2629        let segments: Vec<&str> = pattern.split('/').collect();
2630        let base_dir = self.vm.state.cwd.clone();
2631        let mut matches = Vec::new();
2632        self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
2633        matches
2634    }
2635
2636    /// Recursive walk for globstar expansion.
2637    fn globstar_walk(
2638        &self,
2639        dir: &str,
2640        segments: &[&str],
2641        seg_idx: usize,
2642        prefix: &str,
2643        dotglob: bool,
2644        extglob: bool,
2645        matches: &mut Vec<String>,
2646    ) {
2647        if seg_idx >= segments.len() {
2648            return;
2649        }
2650
2651        let seg = segments[seg_idx];
2652        if seg == "**" {
2653            self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
2654            return;
2655        }
2656        self.globstar_walk_segment(
2657            dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
2658        );
2659    }
2660
2661    fn globstar_walk_wildcard(
2662        &self,
2663        dir: &str,
2664        segments: &[&str],
2665        seg_idx: usize,
2666        prefix: &str,
2667        dotglob: bool,
2668        extglob: bool,
2669        matches: &mut Vec<String>,
2670    ) {
2671        if seg_idx + 1 < segments.len() {
2672            self.globstar_walk(
2673                dir,
2674                segments,
2675                seg_idx + 1,
2676                prefix,
2677                dotglob,
2678                extglob,
2679                matches,
2680            );
2681        }
2682
2683        let Ok(entries) = self.fs.read_dir(dir) else {
2684            return;
2685        };
2686        for entry in &entries {
2687            if !dotglob && entry.name.starts_with('.') {
2688                continue;
2689            }
2690            let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
2691            if self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false) {
2692                self.globstar_walk(
2693                    &child_path,
2694                    segments,
2695                    seg_idx,
2696                    &child_prefix,
2697                    dotglob,
2698                    extglob,
2699                    matches,
2700                );
2701            }
2702        }
2703    }
2704
2705    #[allow(clippy::too_many_arguments)]
2706    fn globstar_walk_segment(
2707        &self,
2708        dir: &str,
2709        seg: &str,
2710        segments: &[&str],
2711        seg_idx: usize,
2712        prefix: &str,
2713        dotglob: bool,
2714        extglob: bool,
2715        matches: &mut Vec<String>,
2716    ) {
2717        let Ok(entries) = self.fs.read_dir(dir) else {
2718            return;
2719        };
2720        let is_last = seg_idx == segments.len() - 1;
2721
2722        for entry in &entries {
2723            if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
2724                continue;
2725            }
2726            self.globstar_handle_matched_entry(
2727                dir,
2728                segments,
2729                seg_idx,
2730                prefix,
2731                dotglob,
2732                extglob,
2733                matches,
2734                &entry.name,
2735                is_last,
2736            );
2737        }
2738    }
2739
2740    #[allow(clippy::too_many_arguments)]
2741    fn globstar_handle_matched_entry(
2742        &self,
2743        dir: &str,
2744        segments: &[&str],
2745        seg_idx: usize,
2746        prefix: &str,
2747        dotglob: bool,
2748        extglob: bool,
2749        matches: &mut Vec<String>,
2750        name: &str,
2751        is_last: bool,
2752    ) {
2753        let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
2754        if is_last {
2755            matches.push(child_prefix);
2756            return;
2757        }
2758        let is_dir = self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false);
2759        if is_dir {
2760            self.globstar_walk(
2761                &child_path,
2762                segments,
2763                seg_idx + 1,
2764                &child_prefix,
2765                dotglob,
2766                extglob,
2767                matches,
2768            );
2769        }
2770    }
2771
2772    fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
2773        let child_path = if dir == "/" {
2774            format!("/{name}")
2775        } else {
2776            format!("{dir}/{name}")
2777        };
2778        let child_prefix = if prefix.is_empty() {
2779            name.to_string()
2780        } else {
2781            format!("{prefix}/{name}")
2782        };
2783        (child_path, child_prefix)
2784    }
2785
2786    /// Write data to a file path, reporting errors to stderr.
2787    fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
2788        match self.fs.open(path, opts) {
2789            Ok(h) => {
2790                if let Err(e) = self.fs.write_file(h, data) {
2791                    self.vm
2792                        .stderr
2793                        .extend_from_slice(format!("wasmsh: write error: {e}\n").as_bytes());
2794                }
2795                self.fs.close(h);
2796            }
2797            Err(e) => {
2798                self.vm
2799                    .stderr
2800                    .extend_from_slice(format!("wasmsh: {target}: {e}\n").as_bytes());
2801            }
2802        }
2803    }
2804
2805    /// Capture stdout data from the given position, truncating the stdout buffer.
2806    fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
2807        let data = self.vm.stdout[from..].to_vec();
2808        self.vm.stdout.truncate(from);
2809        data
2810    }
2811
2812    /// Apply redirections: for `>` and `>>`, write captured stdout/stderr to file.
2813    /// For `<`, read file content (handled pre-execution).
2814    /// Supports fd-specific redirections (2>, 2>>) and &> (both stdout and stderr).
2815    fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
2816        for redir in redirections {
2817            // Resolve command substitutions in redirect targets before expansion
2818            let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
2819            let resolved_target = resolved.first().unwrap_or(&redir.target);
2820            let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
2821            let path = self.resolve_cwd_path(&target);
2822            let fd = redir.fd.unwrap_or(1);
2823
2824            match redir.op {
2825                RedirectionOp::Output => {
2826                    self.apply_output_redir(&path, &target, fd, stdout_before);
2827                }
2828                RedirectionOp::Append => {
2829                    self.apply_append_redir(&path, &target, fd, stdout_before);
2830                }
2831                RedirectionOp::DupOutput => {
2832                    let target_fd: u32 = target.parse().unwrap_or(1);
2833                    let source_fd = redir.fd.unwrap_or(1);
2834                    if source_fd == 2 && target_fd == 1 {
2835                        let stderr_data = std::mem::take(&mut self.vm.stderr);
2836                        self.vm.stdout.extend_from_slice(&stderr_data);
2837                    } else if source_fd == 1 && target_fd == 2 {
2838                        let stdout_data = self.capture_stdout(stdout_before);
2839                        self.vm.stderr.extend_from_slice(&stdout_data);
2840                    }
2841                }
2842                #[allow(unreachable_patterns)]
2843                _ => {}
2844            }
2845        }
2846    }
2847
2848    /// Apply `>` output redirection for a specific fd.
2849    fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2850        let data = if fd == FD_BOTH {
2851            let mut combined = self.capture_stdout(stdout_before);
2852            combined.extend_from_slice(&std::mem::take(&mut self.vm.stderr));
2853            combined
2854        } else if fd == 2 {
2855            std::mem::take(&mut self.vm.stderr)
2856        } else {
2857            self.capture_stdout(stdout_before)
2858        };
2859        self.write_to_file(path, target, &data, OpenOptions::write());
2860    }
2861
2862    /// Apply `>>` append redirection for a specific fd.
2863    fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2864        let data = if fd == 2 {
2865            std::mem::take(&mut self.vm.stderr)
2866        } else {
2867            self.capture_stdout(stdout_before)
2868        };
2869        self.write_to_file(path, target, &data, OpenOptions::append());
2870    }
2871}
2872
2873/// Convert a protocol diagnostic level to a VM diagnostic level.
2874fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
2875    match level {
2876        DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
2877        DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
2878        DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
2879        _ => wasmsh_vm::DiagLevel::Info,
2880    }
2881}
2882
2883// ---- [[ ]] expression evaluator (free functions) ----
2884
2885/// Evaluate an `||` expression (lowest precedence).
2886fn dbl_bracket_eval_or(
2887    tokens: &[String],
2888    pos: &mut usize,
2889    fs: &BackendFs,
2890    state: &mut ShellState,
2891) -> bool {
2892    let mut result = dbl_bracket_eval_and(tokens, pos, fs, state);
2893    while *pos < tokens.len() && tokens[*pos] == "||" {
2894        *pos += 1;
2895        let rhs = dbl_bracket_eval_and(tokens, pos, fs, state);
2896        result = result || rhs;
2897    }
2898    result
2899}
2900
2901/// Evaluate an `&&` expression.
2902fn dbl_bracket_eval_and(
2903    tokens: &[String],
2904    pos: &mut usize,
2905    fs: &BackendFs,
2906    state: &mut ShellState,
2907) -> bool {
2908    let mut result = dbl_bracket_eval_not(tokens, pos, fs, state);
2909    while *pos < tokens.len() && tokens[*pos] == "&&" {
2910        *pos += 1;
2911        let rhs = dbl_bracket_eval_not(tokens, pos, fs, state);
2912        result = result && rhs;
2913    }
2914    result
2915}
2916
2917/// Evaluate a `!` (negation) expression.
2918fn dbl_bracket_eval_not(
2919    tokens: &[String],
2920    pos: &mut usize,
2921    fs: &BackendFs,
2922    state: &mut ShellState,
2923) -> bool {
2924    if *pos < tokens.len() && tokens[*pos] == "!" {
2925        *pos += 1;
2926        return !dbl_bracket_eval_not(tokens, pos, fs, state);
2927    }
2928    dbl_bracket_eval_primary(tokens, pos, fs, state)
2929}
2930
2931/// Evaluate a primary expression: grouped `(expr)`, unary test, binary test, or string truth.
2932fn dbl_bracket_eval_primary(
2933    tokens: &[String],
2934    pos: &mut usize,
2935    fs: &BackendFs,
2936    state: &mut ShellState,
2937) -> bool {
2938    if *pos >= tokens.len() {
2939        return false;
2940    }
2941    if let Some(result) = dbl_bracket_try_group(tokens, pos, fs, state) {
2942        return result;
2943    }
2944    if let Some(result) = dbl_bracket_try_unary(tokens, pos, fs) {
2945        return result;
2946    }
2947    if *pos + 1 == tokens.len() {
2948        return dbl_bracket_take_truthy_token(tokens, pos);
2949    }
2950    if let Some(result) = dbl_bracket_try_binary(tokens, pos, state) {
2951        return result;
2952    }
2953    dbl_bracket_take_truthy_token(tokens, pos)
2954}
2955
2956fn dbl_bracket_try_group(
2957    tokens: &[String],
2958    pos: &mut usize,
2959    fs: &BackendFs,
2960    state: &mut ShellState,
2961) -> Option<bool> {
2962    if tokens.get(*pos).map(String::as_str) != Some("(") {
2963        return None;
2964    }
2965
2966    *pos += 1;
2967    let result = dbl_bracket_eval_or(tokens, pos, fs, state);
2968    if tokens.get(*pos).map(String::as_str) == Some(")") {
2969        *pos += 1;
2970    }
2971    Some(result)
2972}
2973
2974fn dbl_bracket_take_truthy_token(tokens: &[String], pos: &mut usize) -> bool {
2975    let Some(token) = tokens.get(*pos) else {
2976        return false;
2977    };
2978    *pos += 1;
2979    !token.is_empty()
2980}
2981
2982/// Try to evaluate a unary test (`-z`, `-n`, `-f`, etc.). Returns `None` if not a unary op.
2983fn dbl_bracket_try_unary(tokens: &[String], pos: &mut usize, fs: &BackendFs) -> Option<bool> {
2984    if *pos + 1 >= tokens.len() {
2985        return None;
2986    }
2987    let flag = dbl_bracket_parse_unary_flag(&tokens[*pos])?;
2988    match flag {
2989        b'z' | b'n' => Some(dbl_bracket_eval_string_test(tokens, pos, flag)),
2990        b'f' | b'd' | b'e' | b's' | b'r' | b'w' | b'x' => {
2991            dbl_bracket_eval_file_test(tokens, pos, flag, fs)
2992        }
2993        _ => None,
2994    }
2995}
2996
2997fn dbl_bracket_parse_unary_flag(op: &str) -> Option<u8> {
2998    if !op.starts_with('-') || op.len() != 2 {
2999        return None;
3000    }
3001    Some(op.as_bytes()[1])
3002}
3003
3004fn dbl_bracket_eval_string_test(tokens: &[String], pos: &mut usize, flag: u8) -> bool {
3005    *pos += 1;
3006    let arg = &tokens[*pos];
3007    *pos += 1;
3008    if flag == b'z' {
3009        arg.is_empty()
3010    } else {
3011        !arg.is_empty()
3012    }
3013}
3014
3015fn dbl_bracket_eval_file_test(
3016    tokens: &[String],
3017    pos: &mut usize,
3018    flag: u8,
3019    fs: &BackendFs,
3020) -> Option<bool> {
3021    if *pos + 2 < tokens.len() && is_binary_op(&tokens[*pos + 2]) {
3022        return None;
3023    }
3024    *pos += 1;
3025    let path_str = &tokens[*pos];
3026    *pos += 1;
3027    Some(eval_file_test(flag, path_str, fs))
3028}
3029
3030/// Try to evaluate a binary test. Returns `None` if no binary op at pos+1.
3031fn dbl_bracket_try_binary(
3032    tokens: &[String],
3033    pos: &mut usize,
3034    state: &mut ShellState,
3035) -> Option<bool> {
3036    if *pos + 2 > tokens.len() {
3037        return None;
3038    }
3039    let op_idx = *pos + 1;
3040    if op_idx >= tokens.len() || !is_binary_op(&tokens[op_idx]) {
3041        return None;
3042    }
3043    let lhs = tokens[*pos].clone();
3044    *pos += 1;
3045    let op = tokens[*pos].clone();
3046    *pos += 1;
3047
3048    let rhs = dbl_bracket_collect_rhs(tokens, pos, &op);
3049    Some(eval_binary_op(&lhs, &op, &rhs, state))
3050}
3051
3052/// Collect the right-hand side for a binary operator. For `=~`, the RHS extends
3053/// until `&&`, `||`, or end of tokens.
3054fn dbl_bracket_collect_rhs(tokens: &[String], pos: &mut usize, op: &str) -> String {
3055    if *pos >= tokens.len() {
3056        return String::new();
3057    }
3058    if op == "=~" {
3059        return dbl_bracket_collect_regex_rhs(tokens, pos);
3060    }
3061    let rhs = tokens[*pos].clone();
3062    *pos += 1;
3063    rhs
3064}
3065
3066fn dbl_bracket_collect_regex_rhs(tokens: &[String], pos: &mut usize) -> String {
3067    let mut rhs = String::new();
3068    while *pos < tokens.len() && tokens[*pos] != "&&" && tokens[*pos] != "||" {
3069        rhs.push_str(&tokens[*pos]);
3070        *pos += 1;
3071    }
3072    rhs
3073}
3074
3075/// Check whether a token is a binary operator in `[[ ]]` context.
3076fn is_binary_op(s: &str) -> bool {
3077    matches!(
3078        s,
3079        "==" | "!=" | "=~" | "=" | "<" | ">" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge"
3080    )
3081}
3082
3083/// Evaluate a binary operation.
3084fn eval_binary_op(lhs: &str, op: &str, rhs: &str, state: &mut ShellState) -> bool {
3085    match op {
3086        "==" | "=" => glob_cmp(lhs, rhs, state, false),
3087        "!=" => !glob_cmp(lhs, rhs, state, false),
3088        "=~" => eval_regex_match(lhs, rhs, state),
3089        "<" => lhs < rhs,
3090        ">" => lhs > rhs,
3091        _ => eval_int_cmp(lhs, op, rhs),
3092    }
3093}
3094
3095/// Glob-compare lhs against rhs pattern, respecting nocasematch.
3096fn glob_cmp(lhs: &str, rhs: &str, state: &ShellState, _negate: bool) -> bool {
3097    let nocasematch = state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
3098    if nocasematch {
3099        glob_match_inner(rhs.to_lowercase().as_bytes(), lhs.to_lowercase().as_bytes())
3100    } else {
3101        glob_match_inner(rhs.as_bytes(), lhs.as_bytes())
3102    }
3103}
3104
3105/// Evaluate a regex match (`=~`) with capture groups for `BASH_REMATCH`.
3106fn eval_regex_match(lhs: &str, rhs: &str, state: &mut ShellState) -> bool {
3107    let captures = regex_match_with_captures(lhs, rhs);
3108    let br_name = smol_str::SmolStr::from("BASH_REMATCH");
3109    let Some(caps) = captures else {
3110        state.init_indexed_array(br_name);
3111        return false;
3112    };
3113    state.init_indexed_array(br_name.clone());
3114    for (i, cap) in caps.iter().enumerate() {
3115        state.set_array_element(
3116            br_name.clone(),
3117            &i.to_string(),
3118            smol_str::SmolStr::from(cap.as_str()),
3119        );
3120    }
3121    true
3122}
3123
3124/// Evaluate an integer comparison operator (`-eq`, `-ne`, `-lt`, `-le`, `-gt`, `-ge`).
3125fn eval_int_cmp(lhs: &str, op: &str, rhs: &str) -> bool {
3126    let a: i64 = lhs.trim().parse().unwrap_or(0);
3127    let b: i64 = rhs.trim().parse().unwrap_or(0);
3128    match op {
3129        "-eq" => a == b,
3130        "-ne" => a != b,
3131        "-lt" => a < b,
3132        "-le" => a <= b,
3133        "-gt" => a > b,
3134        "-ge" => a >= b,
3135        _ => false,
3136    }
3137}
3138
3139/// Evaluate a unary file test.
3140fn eval_file_test(flag: u8, path: &str, fs: &BackendFs) -> bool {
3141    use wasmsh_fs::Vfs;
3142    match fs.stat(path) {
3143        Ok(meta) => match flag {
3144            b'f' => !meta.is_dir,
3145            b'd' => meta.is_dir,
3146            b's' => meta.size > 0,
3147            // -e, -r, -w, -x: in the VFS all existing files are accessible
3148            b'e' | b'r' | b'w' | b'x' => true,
3149            _ => false,
3150        },
3151        Err(_) => false,
3152    }
3153}
3154
3155/// Strip anchoring from a regex pattern, returning (core, `anchored_start`, `anchored_end`).
3156fn regex_strip_anchors(pattern: &str) -> (&str, bool, bool) {
3157    let anchored_start = pattern.starts_with('^');
3158    let anchored_end = pattern.ends_with('$') && !pattern.ends_with("\\$");
3159    let core = match (anchored_start, anchored_end) {
3160        (true, true) if pattern.len() >= 2 => &pattern[1..pattern.len() - 1],
3161        (true, _) => &pattern[1..],
3162        (_, true) => &pattern[..pattern.len() - 1],
3163        _ => pattern,
3164    };
3165    (core, anchored_start, anchored_end)
3166}
3167
3168/// Check if a regex core has any special regex metacharacters.
3169fn has_regex_metachar(core: &str) -> bool {
3170    core.contains('.')
3171        || core.contains('+')
3172        || core.contains('*')
3173        || core.contains('?')
3174        || core.contains('[')
3175        || core.contains('(')
3176        || core.contains('|')
3177}
3178
3179/// Find match range for a literal pattern with anchoring.
3180fn literal_match_range(text: &str, core: &str, start: bool, end: bool) -> Option<(usize, usize)> {
3181    match (start, end) {
3182        (true, true) if text == core => Some((0, text.len())),
3183        (true, false) if text.starts_with(core) => Some((0, core.len())),
3184        (false, true) if text.ends_with(core) => Some((text.len() - core.len(), text.len())),
3185        (false, false) => text.find(core).map(|pos| (pos, pos + core.len())),
3186        _ => None,
3187    }
3188}
3189
3190/// Regex match with capture group support.
3191///
3192/// Returns `Some(captures)` if the pattern matches, where `captures[0]` is the
3193/// full match and `captures[1..]` are the parenthesized subgroup matches.
3194/// Returns `None` if no match.
3195fn regex_match_with_captures(text: &str, pattern: &str) -> Option<Vec<String>> {
3196    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3197
3198    if !has_regex_metachar(core) {
3199        return regex_match_literal_with_captures(text, core, anchored_start, anchored_end);
3200    }
3201
3202    regex_find_first_match(text, core, anchored_start, anchored_end)
3203}
3204
3205fn regex_find_first_match(
3206    text: &str,
3207    core: &str,
3208    anchored_start: bool,
3209    anchored_end: bool,
3210) -> Option<Vec<String>> {
3211    let end = if anchored_start { 0 } else { text.len() };
3212    for start in 0..=end {
3213        if let Some(result) = regex_match_from_start(text, core, anchored_end, start) {
3214            return Some(result);
3215        }
3216    }
3217    None
3218}
3219
3220fn regex_match_literal_with_captures(
3221    text: &str,
3222    core: &str,
3223    anchored_start: bool,
3224    anchored_end: bool,
3225) -> Option<Vec<String>> {
3226    literal_match_range(text, core, anchored_start, anchored_end)
3227        .map(|(s, e)| vec![text[s..e].to_string()])
3228}
3229
3230fn regex_match_from_start(
3231    text: &str,
3232    core: &str,
3233    anchored_end: bool,
3234    start: usize,
3235) -> Option<Vec<String>> {
3236    let mut group_caps: Vec<(usize, usize)> = Vec::new();
3237    let end = regex_match_capturing(
3238        text.as_bytes(),
3239        start,
3240        core.as_bytes(),
3241        0,
3242        anchored_end,
3243        &mut group_caps,
3244    )?;
3245    Some(regex_build_capture_list(text, start, end, &group_caps))
3246}
3247
3248fn regex_build_capture_list(
3249    text: &str,
3250    start: usize,
3251    end: usize,
3252    group_caps: &[(usize, usize)],
3253) -> Vec<String> {
3254    let mut result = vec![text[start..end].to_string()];
3255    for &(gs, ge) in group_caps {
3256        result.push(text[gs..ge].to_string());
3257    }
3258    result
3259}
3260
3261/// Backtracking regex matcher with capture group support.
3262/// Returns `Some(end_position)` on match, `None` on no match.
3263/// `captures` accumulates (start, end) pairs for each parenthesized group.
3264fn regex_match_capturing(
3265    text: &[u8],
3266    ti: usize,
3267    pat: &[u8],
3268    pi: usize,
3269    must_end: bool,
3270    captures: &mut Vec<(usize, usize)>,
3271) -> Option<usize> {
3272    if pi >= pat.len() {
3273        return regex_check_end(ti, text.len(), must_end);
3274    }
3275
3276    if pat[pi] == b'(' {
3277        return regex_match_group(text, ti, pat, pi, must_end, captures);
3278    }
3279
3280    regex_match_elem(text, ti, pat, pi, must_end, captures)
3281}
3282
3283/// Check if end-of-pattern is valid given anchoring.
3284fn regex_check_end(ti: usize, text_len: usize, must_end: bool) -> Option<usize> {
3285    if must_end && ti < text_len {
3286        None
3287    } else {
3288        Some(ti)
3289    }
3290}
3291
3292/// Handle a parenthesized group in the regex, dispatching by quantifier.
3293fn regex_match_group(
3294    text: &[u8],
3295    ti: usize,
3296    pat: &[u8],
3297    pi: usize,
3298    must_end: bool,
3299    captures: &mut Vec<(usize, usize)>,
3300) -> Option<usize> {
3301    let close = find_matching_paren_bytes(pat, pi + 1)?;
3302    let inner = &pat[pi + 1..close];
3303    let rest = &pat[close + 1..];
3304    let (quant, after_quant_offset) = parse_group_quantifier(pat, close);
3305    let after_quant = &pat[after_quant_offset..];
3306    let alternatives = split_alternatives_bytes(inner);
3307
3308    regex_dispatch_group_quant(
3309        text,
3310        ti,
3311        rest,
3312        after_quant,
3313        must_end,
3314        captures,
3315        &alternatives,
3316        quant,
3317    )
3318}
3319
3320fn parse_group_quantifier(pat: &[u8], close: usize) -> (u8, usize) {
3321    if close + 1 < pat.len() {
3322        match pat[close + 1] {
3323            q @ (b'*' | b'+' | b'?') => (q, close + 2),
3324            _ => (0, close + 1),
3325        }
3326    } else {
3327        (0, close + 1)
3328    }
3329}
3330
3331#[allow(clippy::too_many_arguments)]
3332fn regex_dispatch_group_quant(
3333    text: &[u8],
3334    ti: usize,
3335    rest: &[u8],
3336    after_quant: &[u8],
3337    must_end: bool,
3338    captures: &mut Vec<(usize, usize)>,
3339    alternatives: &[Vec<u8>],
3340    quant: u8,
3341) -> Option<usize> {
3342    match quant {
3343        b'+' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 1),
3344        b'*' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 0),
3345        b'?' => regex_match_group_opt(text, ti, after_quant, must_end, captures, alternatives),
3346        _ => regex_match_group_exact(text, ti, rest, must_end, captures, alternatives),
3347    }
3348}
3349
3350/// Match a group with repetition quantifier (+ or *).
3351fn regex_match_group_rep(
3352    text: &[u8],
3353    ti: usize,
3354    after: &[u8],
3355    must_end: bool,
3356    captures: &mut Vec<(usize, usize)>,
3357    alternatives: &[Vec<u8>],
3358    min_reps: usize,
3359) -> Option<usize> {
3360    let save = captures.len();
3361    for end_pos in (ti..=text.len()).rev() {
3362        captures.truncate(save);
3363        if let Some(result) = regex_try_group_rep_at(
3364            text,
3365            ti,
3366            end_pos,
3367            after,
3368            must_end,
3369            captures,
3370            alternatives,
3371            min_reps,
3372            save,
3373        ) {
3374            return Some(result);
3375        }
3376    }
3377    captures.truncate(save);
3378    None
3379}
3380
3381#[allow(clippy::too_many_arguments)]
3382fn regex_try_group_rep_at(
3383    text: &[u8],
3384    ti: usize,
3385    end_pos: usize,
3386    after: &[u8],
3387    must_end: bool,
3388    captures: &mut Vec<(usize, usize)>,
3389    alternatives: &[Vec<u8>],
3390    min_reps: usize,
3391    save: usize,
3392) -> Option<usize> {
3393    if !regex_match_group_repeated(text, ti, end_pos, alternatives, min_reps) {
3394        return None;
3395    }
3396    let final_end = regex_match_capturing(text, end_pos, after, 0, must_end, captures)?;
3397    captures.insert(save, (ti, end_pos));
3398    Some(final_end)
3399}
3400
3401/// Match a group with `?` quantifier (zero or one).
3402fn regex_match_group_opt(
3403    text: &[u8],
3404    ti: usize,
3405    after: &[u8],
3406    must_end: bool,
3407    captures: &mut Vec<(usize, usize)>,
3408    alternatives: &[Vec<u8>],
3409) -> Option<usize> {
3410    let save = captures.len();
3411    // Try one
3412    if let Some(result) =
3413        regex_try_group_one_alt(text, ti, after, must_end, captures, alternatives, save)
3414    {
3415        return Some(result);
3416    }
3417    // Try zero
3418    captures.truncate(save);
3419    if let Some(final_end) = regex_match_capturing(text, ti, after, 0, must_end, captures) {
3420        captures.insert(save, (ti, ti));
3421        return Some(final_end);
3422    }
3423    captures.truncate(save);
3424    None
3425}
3426
3427fn regex_try_group_one_alt(
3428    text: &[u8],
3429    ti: usize,
3430    after: &[u8],
3431    must_end: bool,
3432    captures: &mut Vec<(usize, usize)>,
3433    alternatives: &[Vec<u8>],
3434    save: usize,
3435) -> Option<usize> {
3436    for alt in alternatives {
3437        captures.truncate(save);
3438        if let Some(result) =
3439            regex_try_alt_then_continue(text, ti, alt, after, must_end, captures, save)
3440        {
3441            return Some(result);
3442        }
3443        captures.truncate(save);
3444    }
3445    None
3446}
3447
3448fn regex_try_alt_then_continue(
3449    text: &[u8],
3450    ti: usize,
3451    alt: &[u8],
3452    after: &[u8],
3453    must_end: bool,
3454    captures: &mut Vec<(usize, usize)>,
3455    save: usize,
3456) -> Option<usize> {
3457    let end = regex_try_match_at(text, ti, alt)?;
3458    let final_end = regex_match_capturing(text, end, after, 0, must_end, captures)?;
3459    captures.insert(save, (ti, end));
3460    Some(final_end)
3461}
3462
3463/// Match a group exactly once (no quantifier).
3464fn regex_match_group_exact(
3465    text: &[u8],
3466    ti: usize,
3467    rest: &[u8],
3468    must_end: bool,
3469    captures: &mut Vec<(usize, usize)>,
3470    alternatives: &[Vec<u8>],
3471) -> Option<usize> {
3472    regex_try_group_one_alt(
3473        text,
3474        ti,
3475        rest,
3476        must_end,
3477        captures,
3478        alternatives,
3479        captures.len(),
3480    )
3481}
3482
3483/// Parse a quantifier after a regex element.
3484fn parse_quantifier(pat: &[u8], pos: usize) -> (u8, usize) {
3485    if pos < pat.len() {
3486        match pat[pos] {
3487            b'*' => (b'*', pos + 1),
3488            b'+' => (b'+', pos + 1),
3489            b'?' => (b'?', pos + 1),
3490            _ => (0, pos),
3491        }
3492    } else {
3493        (0, pos)
3494    }
3495}
3496
3497/// Match a single regex element (not a group) with optional quantifier.
3498fn regex_match_elem(
3499    text: &[u8],
3500    ti: usize,
3501    pat: &[u8],
3502    pi: usize,
3503    must_end: bool,
3504    captures: &mut Vec<(usize, usize)>,
3505) -> Option<usize> {
3506    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3507    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3508
3509    match quant {
3510        b'*' | b'+' => regex_match_repeated_elem(
3511            text,
3512            ti,
3513            pat,
3514            after_quant,
3515            quant,
3516            must_end,
3517            captures,
3518            &matches_fn,
3519        ),
3520        b'?' => {
3521            regex_match_optional_elem(text, ti, pat, after_quant, must_end, captures, &matches_fn)
3522        }
3523        _ => regex_match_single_elem(text, ti, pat, elem_end, must_end, captures, &matches_fn),
3524    }
3525}
3526
3527fn count_regex_matches(text: &[u8], ti: usize, matches_fn: &dyn Fn(u8) -> bool) -> usize {
3528    let mut count = 0;
3529    while ti + count < text.len() && matches_fn(text[ti + count]) {
3530        count += 1;
3531    }
3532    count
3533}
3534
3535fn regex_match_repeated_elem(
3536    text: &[u8],
3537    ti: usize,
3538    pat: &[u8],
3539    after_quant: usize,
3540    quant: u8,
3541    must_end: bool,
3542    captures: &mut Vec<(usize, usize)>,
3543    matches_fn: &dyn Fn(u8) -> bool,
3544) -> Option<usize> {
3545    let min = usize::from(quant == b'+');
3546    let count = count_regex_matches(text, ti, matches_fn);
3547    for c in (min..=count).rev() {
3548        if let Some(end) = regex_match_capturing(text, ti + c, pat, after_quant, must_end, captures)
3549        {
3550            return Some(end);
3551        }
3552    }
3553    None
3554}
3555
3556fn regex_match_optional_elem(
3557    text: &[u8],
3558    ti: usize,
3559    pat: &[u8],
3560    after_quant: usize,
3561    must_end: bool,
3562    captures: &mut Vec<(usize, usize)>,
3563    matches_fn: &dyn Fn(u8) -> bool,
3564) -> Option<usize> {
3565    if ti < text.len() && matches_fn(text[ti]) {
3566        if let Some(end) = regex_match_capturing(text, ti + 1, pat, after_quant, must_end, captures)
3567        {
3568            return Some(end);
3569        }
3570    }
3571    regex_match_capturing(text, ti, pat, after_quant, must_end, captures)
3572}
3573
3574fn regex_match_single_elem(
3575    text: &[u8],
3576    ti: usize,
3577    pat: &[u8],
3578    elem_end: usize,
3579    must_end: bool,
3580    captures: &mut Vec<(usize, usize)>,
3581    matches_fn: &dyn Fn(u8) -> bool,
3582) -> Option<usize> {
3583    if ti < text.len() && matches_fn(text[ti]) {
3584        regex_match_capturing(text, ti + 1, pat, elem_end, must_end, captures)
3585    } else {
3586        None
3587    }
3588}
3589
3590/// Try to match a simple pattern at a position, returning the end position if matched.
3591fn regex_try_match_at(text: &[u8], start: usize, pattern: &[u8]) -> Option<usize> {
3592    regex_try_match_inner(text, start, pattern, 0)
3593}
3594
3595/// Inner helper to find end position of a pattern match.
3596fn regex_try_match_inner(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3597    if pi >= pat.len() {
3598        return Some(ti);
3599    }
3600    if pat[pi] == b'(' {
3601        return regex_try_match_group(text, ti, pat, pi);
3602    }
3603    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3604    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3605    regex_try_apply_quant(text, ti, pat, elem_end, after_quant, quant, &matches_fn)
3606}
3607
3608/// Handle a group in `regex_try_match_inner`.
3609fn regex_try_match_group(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3610    let close = find_matching_paren_bytes(pat, pi + 1)?;
3611    let inner = &pat[pi + 1..close];
3612    let rest = &pat[close + 1..];
3613    let alternatives = split_alternatives_bytes(inner);
3614    for alt in &alternatives {
3615        if let Some(end) = regex_try_alt_and_rest(text, ti, alt, rest) {
3616            return Some(end);
3617        }
3618    }
3619    None
3620}
3621
3622fn regex_try_alt_and_rest(text: &[u8], ti: usize, alt: &[u8], rest: &[u8]) -> Option<usize> {
3623    let after_alt = regex_try_match_inner(text, ti, alt, 0)?;
3624    regex_try_match_inner(text, after_alt, rest, 0)
3625}
3626
3627/// Apply quantifier logic for `regex_try_match_inner`.
3628fn regex_try_apply_quant(
3629    text: &[u8],
3630    ti: usize,
3631    pat: &[u8],
3632    elem_end: usize,
3633    after_quant: usize,
3634    quant: u8,
3635    matches_fn: &dyn Fn(u8) -> bool,
3636) -> Option<usize> {
3637    match quant {
3638        b'*' | b'+' => regex_try_match_repeated_elem(text, ti, pat, after_quant, quant, matches_fn),
3639        b'?' => regex_try_match_optional_elem(text, ti, pat, after_quant, matches_fn),
3640        _ => regex_try_match_single_elem(text, ti, pat, elem_end, matches_fn),
3641    }
3642}
3643
3644fn regex_try_match_repeated_elem(
3645    text: &[u8],
3646    ti: usize,
3647    pat: &[u8],
3648    after_quant: usize,
3649    quant: u8,
3650    matches_fn: &dyn Fn(u8) -> bool,
3651) -> Option<usize> {
3652    let min = usize::from(quant == b'+');
3653    let count = count_regex_matches(text, ti, matches_fn);
3654    for c in (min..=count).rev() {
3655        if let Some(end) = regex_try_match_inner(text, ti + c, pat, after_quant) {
3656            return Some(end);
3657        }
3658    }
3659    None
3660}
3661
3662fn regex_try_match_optional_elem(
3663    text: &[u8],
3664    ti: usize,
3665    pat: &[u8],
3666    after_quant: usize,
3667    matches_fn: &dyn Fn(u8) -> bool,
3668) -> Option<usize> {
3669    if ti < text.len() && matches_fn(text[ti]) {
3670        if let Some(end) = regex_try_match_inner(text, ti + 1, pat, after_quant) {
3671            return Some(end);
3672        }
3673    }
3674    regex_try_match_inner(text, ti, pat, after_quant)
3675}
3676
3677fn regex_try_match_single_elem(
3678    text: &[u8],
3679    ti: usize,
3680    pat: &[u8],
3681    elem_end: usize,
3682    matches_fn: &dyn Fn(u8) -> bool,
3683) -> Option<usize> {
3684    if ti < text.len() && matches_fn(text[ti]) {
3685        regex_try_match_inner(text, ti + 1, pat, elem_end)
3686    } else {
3687        None
3688    }
3689}
3690
3691/// Check if alternatives can be matched repeatedly to fill text[start..end].
3692fn regex_match_group_repeated(
3693    text: &[u8],
3694    start: usize,
3695    end: usize,
3696    alternatives: &[Vec<u8>],
3697    min_reps: usize,
3698) -> bool {
3699    if start == end {
3700        return min_reps == 0;
3701    }
3702    if start > end {
3703        return false;
3704    }
3705    for alt in alternatives {
3706        if regex_group_repetition_matches(text, start, end, alternatives, min_reps, alt) {
3707            return true;
3708        }
3709    }
3710    false
3711}
3712
3713fn regex_group_repetition_matches(
3714    text: &[u8],
3715    start: usize,
3716    end: usize,
3717    alternatives: &[Vec<u8>],
3718    min_reps: usize,
3719    alt: &[u8],
3720) -> bool {
3721    let Some(after) = regex_try_match_inner(text, start, alt, 0) else {
3722        return false;
3723    };
3724    if after <= start || after > end {
3725        return false;
3726    }
3727    if after == end && min_reps <= 1 {
3728        return true;
3729    }
3730    regex_match_group_repeated(text, after, end, alternatives, min_reps.saturating_sub(1))
3731}
3732
3733/// Find matching `)` for a `(` in a byte pattern, handling nesting.
3734fn find_matching_paren_bytes(pat: &[u8], start: usize) -> Option<usize> {
3735    let mut depth = 1;
3736    let mut i = start;
3737    while i < pat.len() {
3738        if pat[i] == b'\\' {
3739            i += 2;
3740            continue;
3741        }
3742        if pat[i] == b'(' {
3743            depth += 1;
3744        } else if pat[i] == b')' {
3745            depth -= 1;
3746            if depth == 0 {
3747                return Some(i);
3748            }
3749        }
3750        i += 1;
3751    }
3752    None
3753}
3754
3755/// Split a byte pattern by `|` at the top level (not inside nested parens).
3756fn split_alternatives_bytes(pat: &[u8]) -> Vec<Vec<u8>> {
3757    let mut alternatives = Vec::new();
3758    let mut current = Vec::new();
3759    let mut depth = 0i32;
3760    let mut i = 0;
3761    while i < pat.len() {
3762        if pat[i] == b'\\' && i + 1 < pat.len() {
3763            current.push(pat[i]);
3764            current.push(pat[i + 1]);
3765            i += 2;
3766            continue;
3767        }
3768        split_alt_classify_byte(pat[i], &mut depth, &mut current, &mut alternatives);
3769        i += 1;
3770    }
3771    alternatives.push(current);
3772    alternatives
3773}
3774
3775fn split_alt_classify_byte(
3776    byte: u8,
3777    depth: &mut i32,
3778    current: &mut Vec<u8>,
3779    alternatives: &mut Vec<Vec<u8>>,
3780) {
3781    match byte {
3782        b'(' => {
3783            *depth += 1;
3784            current.push(byte);
3785        }
3786        b')' => {
3787            *depth -= 1;
3788            current.push(byte);
3789        }
3790        b'|' if *depth == 0 => {
3791            alternatives.push(std::mem::take(current));
3792        }
3793        _ => {
3794            current.push(byte);
3795        }
3796    }
3797}
3798
3799/// Simple regex-like matching for `=~`.
3800///
3801/// Supports: `^prefix`, `suffix$`, `^exact$`, and literal substring match.
3802/// This avoids pulling in a regex crate for wasm32.
3803#[allow(dead_code)]
3804fn simple_regex_match(text: &str, pattern: &str) -> bool {
3805    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3806
3807    if has_regex_metachar(core) {
3808        return regex_like_match(text, pattern);
3809    }
3810
3811    // Pure literal matching with anchoring
3812    literal_match_range(text, core, anchored_start, anchored_end).is_some()
3813}
3814
3815/// A simple regex-like matcher supporting: `.` (any char), `*` (zero or more of previous),
3816/// `+` (one or more of previous), `?` (zero or one of previous), `^`, `$`,
3817/// `[abc]` character classes, `(a|b)` alternation, and literal chars.
3818/// This is intentionally limited but handles common bash `=~` patterns.
3819#[allow(dead_code)]
3820fn regex_like_match(text: &str, pattern: &str) -> bool {
3821    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3822
3823    if anchored_start {
3824        regex_match_at(text, 0, core, anchored_end)
3825    } else {
3826        (0..=text.len()).any(|start| regex_match_at(text, start, core, anchored_end))
3827    }
3828}
3829
3830/// Try to match `core` pattern starting at byte position `start` in `text`.
3831/// If `must_end` is true, the match must consume through end of `text`.
3832#[allow(dead_code)]
3833fn regex_match_at(text: &str, start: usize, core: &str, must_end: bool) -> bool {
3834    let text_bytes = text.as_bytes();
3835    let core_bytes = core.as_bytes();
3836    regex_backtrack(text_bytes, start, core_bytes, 0, must_end)
3837}
3838
3839/// Recursive backtracking regex matcher.
3840#[allow(dead_code)]
3841fn regex_backtrack(text: &[u8], ti: usize, pat: &[u8], pi: usize, must_end: bool) -> bool {
3842    if pi >= pat.len() {
3843        return if must_end { ti >= text.len() } else { true };
3844    }
3845
3846    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3847    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3848
3849    match quant {
3850        b'*' => regex_backtrack_star(text, ti, pat, after_quant, must_end, &matches_fn),
3851        b'+' => regex_backtrack_plus(text, ti, pat, after_quant, must_end, &matches_fn),
3852        b'?' => regex_backtrack_optional(text, ti, pat, after_quant, must_end, &matches_fn),
3853        _ => regex_backtrack_single(text, ti, pat, elem_end, must_end, &matches_fn),
3854    }
3855}
3856
3857fn regex_backtrack_star(
3858    text: &[u8],
3859    ti: usize,
3860    pat: &[u8],
3861    after_quant: usize,
3862    must_end: bool,
3863    matches_fn: &dyn Fn(u8) -> bool,
3864) -> bool {
3865    let mut count = 0;
3866    loop {
3867        if regex_backtrack(text, ti + count, pat, after_quant, must_end) {
3868            return true;
3869        }
3870        if ti + count < text.len() && matches_fn(text[ti + count]) {
3871            count += 1;
3872        } else {
3873            return false;
3874        }
3875    }
3876}
3877
3878fn regex_backtrack_plus(
3879    text: &[u8],
3880    ti: usize,
3881    pat: &[u8],
3882    after_quant: usize,
3883    must_end: bool,
3884    matches_fn: &dyn Fn(u8) -> bool,
3885) -> bool {
3886    let count = count_regex_matches(text, ti, matches_fn);
3887    (1..=count).any(|matched| regex_backtrack(text, ti + matched, pat, after_quant, must_end))
3888}
3889
3890fn regex_backtrack_optional(
3891    text: &[u8],
3892    ti: usize,
3893    pat: &[u8],
3894    after_quant: usize,
3895    must_end: bool,
3896    matches_fn: &dyn Fn(u8) -> bool,
3897) -> bool {
3898    regex_backtrack(text, ti, pat, after_quant, must_end)
3899        || (ti < text.len()
3900            && matches_fn(text[ti])
3901            && regex_backtrack(text, ti + 1, pat, after_quant, must_end))
3902}
3903
3904fn regex_backtrack_single(
3905    text: &[u8],
3906    ti: usize,
3907    pat: &[u8],
3908    elem_end: usize,
3909    must_end: bool,
3910    matches_fn: &dyn Fn(u8) -> bool,
3911) -> bool {
3912    ti < text.len()
3913        && matches_fn(text[ti])
3914        && regex_backtrack(text, ti + 1, pat, elem_end, must_end)
3915}
3916
3917/// Parse one regex element at position `pi`, return (`end_pos`, `match_fn`).
3918/// An element is: `.`, `[class]`, `(alt)`, or a literal byte.
3919fn parse_regex_elem(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3920    match pat[pi] {
3921        b'.' => (pi + 1, Box::new(|_: u8| true)),
3922        b'[' => parse_regex_char_class(pat, pi),
3923        b'\\' if pi + 1 < pat.len() => {
3924            let escaped = pat[pi + 1];
3925            (pi + 2, Box::new(move |c: u8| c == escaped))
3926        }
3927        ch => (pi + 1, Box::new(move |c: u8| c == ch)),
3928    }
3929}
3930
3931fn parse_regex_char_class(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3932    let mut i = pi + 1;
3933    let negate = i < pat.len() && (pat[i] == b'^' || pat[i] == b'!');
3934    if negate {
3935        i += 1;
3936    }
3937    let mut chars = Vec::new();
3938    while i < pat.len() && pat[i] != b']' {
3939        if i + 2 < pat.len() && pat[i + 1] == b'-' {
3940            chars.extend(pat[i]..=pat[i + 2]);
3941            i += 3;
3942        } else {
3943            chars.push(pat[i]);
3944            i += 1;
3945        }
3946    }
3947    let end = if i < pat.len() { i + 1 } else { i };
3948    (
3949        end,
3950        Box::new(move |c: u8| regex_char_class_matches(&chars, negate, c)),
3951    )
3952}
3953
3954fn regex_char_class_matches(chars: &[u8], negate: bool, c: u8) -> bool {
3955    let found = chars.contains(&c);
3956    if negate {
3957        !found
3958    } else {
3959        found
3960    }
3961}
3962
3963/// Match a glob character class `[...]` at position `pi` (just past the `[`).
3964/// Returns `(new_pi, matched)` where `new_pi` is past the `]`.
3965fn glob_match_char_class(pattern: &[u8], mut pi: usize, ch: u8) -> (usize, bool) {
3966    let negate = pi < pattern.len() && (pattern[pi] == b'!' || pattern[pi] == b'^');
3967    if negate {
3968        pi += 1;
3969    }
3970    let mut matched = false;
3971    let mut first = true;
3972    while pi < pattern.len() && (first || pattern[pi] != b']') {
3973        first = false;
3974        let (next_pi, item_matched) = glob_match_char_class_item(pattern, pi, ch);
3975        matched |= item_matched;
3976        pi = next_pi;
3977    }
3978    if pi < pattern.len() && pattern[pi] == b']' {
3979        pi += 1;
3980    }
3981    (pi, matched != negate)
3982}
3983
3984fn glob_match_char_class_item(pattern: &[u8], pi: usize, ch: u8) -> (usize, bool) {
3985    if pi + 2 < pattern.len() && pattern[pi + 1] == b'-' {
3986        let lo = pattern[pi];
3987        let hi = pattern[pi + 2];
3988        return (pi + 3, ch >= lo && ch <= hi);
3989    }
3990    (pi + 1, pattern[pi] == ch)
3991}
3992
3993enum GlobPatternStep {
3994    Consume(usize),
3995    Star,
3996    Class(usize, bool),
3997    Mismatch,
3998}
3999
4000fn glob_step(pattern: &[u8], pi: usize, ch: u8) -> GlobPatternStep {
4001    if pi >= pattern.len() {
4002        return GlobPatternStep::Mismatch;
4003    }
4004
4005    match pattern[pi] {
4006        b'?' => GlobPatternStep::Consume(pi + 1),
4007        b'*' => GlobPatternStep::Star,
4008        b'[' => {
4009            let (new_pi, matched) = glob_match_char_class(pattern, pi + 1, ch);
4010            GlobPatternStep::Class(new_pi, matched)
4011        }
4012        literal if literal == ch => GlobPatternStep::Consume(pi + 1),
4013        _ => GlobPatternStep::Mismatch,
4014    }
4015}
4016
4017fn glob_backtrack(pi: &mut usize, ni: &mut usize, star_pi: usize, star_ni: &mut usize) -> bool {
4018    if star_pi == usize::MAX {
4019        return false;
4020    }
4021
4022    *pi = star_pi + 1;
4023    *star_ni += 1;
4024    *ni = *star_ni;
4025    true
4026}
4027
4028/// Core glob pattern matching (byte-level).
4029///
4030/// Supports `*` (any sequence), `?` (one char), and `[abc]` (character class).
4031fn glob_match_inner(pattern: &[u8], name: &[u8]) -> bool {
4032    let mut pi = 0;
4033    let mut ni = 0;
4034    let mut star_pi = usize::MAX;
4035    let mut star_ni = usize::MAX;
4036
4037    while ni < name.len() {
4038        match glob_step(pattern, pi, name[ni]) {
4039            GlobPatternStep::Star => {
4040                star_pi = pi;
4041                star_ni = ni;
4042                pi += 1;
4043            }
4044            GlobPatternStep::Consume(new_pi) | GlobPatternStep::Class(new_pi, true) => {
4045                pi = new_pi;
4046                ni += 1;
4047            }
4048            GlobPatternStep::Class(_, false) | GlobPatternStep::Mismatch => {
4049                if !glob_backtrack(&mut pi, &mut ni, star_pi, &mut star_ni) {
4050                    return false;
4051                }
4052            }
4053        }
4054    }
4055
4056    // Consume trailing stars
4057    while pi < pattern.len() && pattern[pi] == b'*' {
4058        pi += 1;
4059    }
4060
4061    pi == pattern.len()
4062}
4063
4064/// Extended glob matching with dotglob and extglob support.
4065fn glob_match_ext(pattern: &str, name: &str, dotglob: bool, extglob: bool) -> bool {
4066    // Don't match hidden files unless dotglob is enabled or pattern starts with '.'
4067    if name.starts_with('.') && !pattern.starts_with('.') && !dotglob {
4068        return false;
4069    }
4070    if extglob && has_extglob_pattern(pattern) {
4071        return extglob_match(pattern, name);
4072    }
4073    glob_match_inner(pattern.as_bytes(), name.as_bytes())
4074}
4075
4076/// Check if a pattern contains extglob operators: `?(`, `*(`, `+(`, `@(`, `!(`.
4077fn has_extglob_pattern(pattern: &str) -> bool {
4078    let bytes = pattern.as_bytes();
4079    for i in 0..bytes.len().saturating_sub(1) {
4080        if bytes[i + 1] == b'(' && matches!(bytes[i], b'?' | b'*' | b'+' | b'@' | b'!') {
4081            return true;
4082        }
4083    }
4084    false
4085}
4086
4087/// Match a name against an extglob pattern.
4088///
4089/// Supports: `?(pat|pat)`, `*(pat|pat)`, `+(pat|pat)`, `@(pat|pat)`, `!(pat|pat)`.
4090/// Non-extglob portions are handled by regular glob matching.
4091pub fn extglob_match(pattern: &str, name: &str) -> bool {
4092    extglob_match_recursive(pattern.as_bytes(), name.as_bytes())
4093}
4094
4095fn extglob_match_recursive(pattern: &[u8], name: &[u8]) -> bool {
4096    // Find the first extglob operator
4097    let Some((pi, op, close)) = find_extglob_operator(pattern) else {
4098        return glob_match_inner(pattern, name);
4099    };
4100
4101    let open = pi + 2;
4102    let alternatives = split_alternatives(&pattern[open..close]);
4103    let prefix = &pattern[..pi];
4104    let suffix = &pattern[close + 1..];
4105
4106    match op {
4107        b'@' | b'?' => extglob_match_at_or_opt(op, prefix, &alternatives, suffix, name),
4108        b'*' => extglob_star(prefix, &alternatives, suffix, name, 0),
4109        b'+' => extglob_plus(prefix, &alternatives, suffix, name, 0),
4110        b'!' => extglob_match_negate(prefix, &alternatives, suffix, name),
4111        _ => unreachable!(),
4112    }
4113}
4114
4115/// Find the first extglob operator in a pattern, returning (position, operator, `close_paren`).
4116fn find_extglob_operator(pattern: &[u8]) -> Option<(usize, u8, usize)> {
4117    let mut pi = 0;
4118    while pi < pattern.len() {
4119        if pi + 1 < pattern.len()
4120            && pattern[pi + 1] == b'('
4121            && matches!(pattern[pi], b'?' | b'*' | b'+' | b'@' | b'!')
4122        {
4123            if let Some(close) = find_matching_paren(pattern, pi + 2) {
4124                return Some((pi, pattern[pi], close));
4125            }
4126        }
4127        pi += 1;
4128    }
4129    None
4130}
4131
4132/// Build a combined pattern from prefix + alt + suffix.
4133fn build_combined(prefix: &[u8], mid: &[u8], suffix: &[u8]) -> Vec<u8> {
4134    let mut combined = Vec::with_capacity(prefix.len() + mid.len() + suffix.len());
4135    combined.extend_from_slice(prefix);
4136    combined.extend_from_slice(mid);
4137    combined.extend_from_slice(suffix);
4138    combined
4139}
4140
4141/// Handle `@(...)` (exactly one) and `?(...)` (zero or one) extglob patterns.
4142fn extglob_match_at_or_opt(
4143    op: u8,
4144    prefix: &[u8],
4145    alternatives: &[Vec<u8>],
4146    suffix: &[u8],
4147    name: &[u8],
4148) -> bool {
4149    // For `?`, try zero first
4150    if op == b'?' && extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4151        return true;
4152    }
4153    // Try each alternative exactly once
4154    for alt in alternatives {
4155        if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4156            return true;
4157        }
4158    }
4159    false
4160}
4161
4162/// Handle `!(...)` extglob pattern: matches if no alternative matches.
4163fn extglob_match_negate(
4164    prefix: &[u8],
4165    alternatives: &[Vec<u8>],
4166    suffix: &[u8],
4167    name: &[u8],
4168) -> bool {
4169    for alt in alternatives {
4170        if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4171            return false;
4172        }
4173    }
4174    let wildcard = build_combined(prefix, b"*", suffix);
4175    glob_match_inner(&wildcard, name)
4176}
4177
4178/// Try zero or more repetitions of alternatives for `*(...)`.
4179fn extglob_star(
4180    prefix: &[u8],
4181    alternatives: &[Vec<u8>],
4182    suffix: &[u8],
4183    name: &[u8],
4184    depth: u32,
4185) -> bool {
4186    if depth > 20 {
4187        return false;
4188    }
4189    // Try zero repetitions
4190    if extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4191        return true;
4192    }
4193    // Try one repetition followed by zero or more
4194    extglob_try_extend(prefix, alternatives, suffix, name, depth)
4195}
4196
4197fn extglob_try_extend(
4198    prefix: &[u8],
4199    alternatives: &[Vec<u8>],
4200    suffix: &[u8],
4201    name: &[u8],
4202    depth: u32,
4203) -> bool {
4204    let prefix_len = prefix.len();
4205    for alt in alternatives {
4206        let new_prefix = build_combined(prefix, alt, &[]);
4207        if new_prefix.len() > prefix_len
4208            && extglob_star(&new_prefix, alternatives, suffix, name, depth + 1)
4209        {
4210            return true;
4211        }
4212    }
4213    false
4214}
4215
4216/// Try one or more repetitions of alternatives for `+(...)`.
4217fn extglob_plus(
4218    prefix: &[u8],
4219    alternatives: &[Vec<u8>],
4220    suffix: &[u8],
4221    name: &[u8],
4222    depth: u32,
4223) -> bool {
4224    if depth > 20 {
4225        return false;
4226    }
4227    for alt in alternatives {
4228        let new_prefix = build_combined(prefix, alt, &[]);
4229        if extglob_star(&new_prefix, alternatives, suffix, name, depth + 1) {
4230            return true;
4231        }
4232    }
4233    false
4234}
4235
4236/// Find the matching `)` for a `(` at position `open` (character after `(`).
4237fn find_matching_paren(pattern: &[u8], open: usize) -> Option<usize> {
4238    let mut depth: u32 = 1;
4239    let mut i = open;
4240    while i < pattern.len() {
4241        if pattern[i] == b'(' {
4242            depth += 1;
4243        } else if pattern[i] == b')' {
4244            depth -= 1;
4245            if depth == 0 {
4246                return Some(i);
4247            }
4248        }
4249        i += 1;
4250    }
4251    None
4252}
4253
4254/// Split alternatives by `|` at the top level (not inside nested parens).
4255fn split_alternatives(pat: &[u8]) -> Vec<Vec<u8>> {
4256    let mut result = Vec::new();
4257    let mut current = Vec::new();
4258    let mut depth: u32 = 0;
4259    for &b in pat {
4260        if b == b'(' {
4261            depth += 1;
4262            current.push(b);
4263        } else if b == b')' {
4264            depth -= 1;
4265            current.push(b);
4266        } else if b == b'|' && depth == 0 {
4267            result.push(std::mem::take(&mut current));
4268        } else {
4269            current.push(b);
4270        }
4271    }
4272    result.push(current);
4273    result
4274}
4275
4276impl Default for WorkerRuntime {
4277    fn default() -> Self {
4278        Self::new()
4279    }
4280}