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