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