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(
2515                    self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob),
2516                );
2517            }
2518        }
2519        result.truncate(Self::MAX_GLOB_RESULTS);
2520        result
2521    }
2522
2523    fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
2524        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
2525            return argv;
2526        }
2527        let nullglob = self.get_shopt_value("nullglob");
2528        let dotglob = self.get_shopt_value("dotglob");
2529        let globstar = self.get_shopt_value("globstar");
2530        let extglob = self.get_shopt_value("extglob");
2531
2532        let mut result = Vec::new();
2533        for arg in argv {
2534            result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
2535        }
2536        result.truncate(Self::MAX_GLOB_RESULTS);
2537        result
2538    }
2539
2540    #[allow(clippy::fn_params_excessive_bools)]
2541    fn expand_glob_arg(
2542        &self,
2543        arg: String,
2544        nullglob: bool,
2545        dotglob: bool,
2546        globstar: bool,
2547        extglob: bool,
2548    ) -> Vec<String> {
2549        if !Self::is_glob_pattern(&arg, extglob) {
2550            return vec![arg];
2551        }
2552        if globstar && arg.contains("**") {
2553            return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
2554        }
2555        self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
2556    }
2557
2558    fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
2559        let has_bracket_class = arg.contains('[') && arg.contains(']');
2560        arg.contains('*')
2561            || arg.contains('?')
2562            || has_bracket_class
2563            || (extglob && has_extglob_pattern(arg))
2564    }
2565
2566    fn expand_globstar_arg(
2567        &self,
2568        arg: String,
2569        nullglob: bool,
2570        dotglob: bool,
2571        extglob: bool,
2572    ) -> Vec<String> {
2573        let mut matches = self.expand_globstar(&arg, dotglob, extglob);
2574        matches.sort();
2575        self.finalize_glob_matches(arg, matches, nullglob)
2576    }
2577
2578    fn expand_standard_glob_arg(
2579        &self,
2580        arg: String,
2581        nullglob: bool,
2582        dotglob: bool,
2583        extglob: bool,
2584    ) -> Vec<String> {
2585        let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
2586            return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
2587        };
2588        let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
2589        self.finalize_glob_matches(arg, matches, nullglob)
2590    }
2591
2592    fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
2593        let Some(slash_pos) = arg.rfind('/') else {
2594            return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
2595        };
2596
2597        let dir_part = &arg[..=slash_pos];
2598        if Self::path_segment_has_glob(dir_part) {
2599            return None;
2600        }
2601
2602        Some((
2603            self.resolve_cwd_path(dir_part),
2604            arg[slash_pos + 1..].to_string(),
2605            Some(dir_part.to_string()),
2606        ))
2607    }
2608
2609    fn path_segment_has_glob(path: &str) -> bool {
2610        path.contains('*') || path.contains('?') || path.contains('[')
2611    }
2612
2613    fn read_glob_matches(
2614        &self,
2615        dir: &str,
2616        pattern: &str,
2617        prefix: Option<&str>,
2618        dotglob: bool,
2619        extglob: bool,
2620    ) -> Vec<String> {
2621        let Ok(entries) = self.fs.read_dir(dir) else {
2622            return Vec::new();
2623        };
2624
2625        let mut matches: Vec<String> = entries
2626            .iter()
2627            .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
2628            .map(|e| match prefix {
2629                Some(prefix) => format!("{prefix}{}", e.name),
2630                None => e.name.clone(),
2631            })
2632            .collect();
2633        matches.sort();
2634        matches
2635    }
2636
2637    #[allow(clippy::unused_self)]
2638    fn finalize_glob_matches(
2639        &self,
2640        arg: String,
2641        matches: Vec<String>,
2642        nullglob: bool,
2643    ) -> Vec<String> {
2644        if !matches.is_empty() {
2645            return matches;
2646        }
2647        if nullglob {
2648            Vec::new()
2649        } else {
2650            vec![arg]
2651        }
2652    }
2653
2654    /// Expand a globstar (**) pattern against the VFS with recursive directory traversal.
2655    fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
2656        // Split pattern into segments by /
2657        let segments: Vec<&str> = pattern.split('/').collect();
2658        let base_dir = self.vm.state.cwd.clone();
2659        let mut matches = Vec::new();
2660        self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
2661        matches
2662    }
2663
2664    /// Recursive walk for globstar expansion.
2665    fn globstar_walk(
2666        &self,
2667        dir: &str,
2668        segments: &[&str],
2669        seg_idx: usize,
2670        prefix: &str,
2671        dotglob: bool,
2672        extglob: bool,
2673        matches: &mut Vec<String>,
2674    ) {
2675        if seg_idx >= segments.len() {
2676            return;
2677        }
2678
2679        let seg = segments[seg_idx];
2680        if seg == "**" {
2681            self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
2682            return;
2683        }
2684        self.globstar_walk_segment(
2685            dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
2686        );
2687    }
2688
2689    fn globstar_walk_wildcard(
2690        &self,
2691        dir: &str,
2692        segments: &[&str],
2693        seg_idx: usize,
2694        prefix: &str,
2695        dotglob: bool,
2696        extglob: bool,
2697        matches: &mut Vec<String>,
2698    ) {
2699        if seg_idx + 1 < segments.len() {
2700            self.globstar_walk(
2701                dir,
2702                segments,
2703                seg_idx + 1,
2704                prefix,
2705                dotglob,
2706                extglob,
2707                matches,
2708            );
2709        }
2710
2711        let Ok(entries) = self.fs.read_dir(dir) else {
2712            return;
2713        };
2714        for entry in &entries {
2715            if !dotglob && entry.name.starts_with('.') {
2716                continue;
2717            }
2718            let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
2719            if self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false) {
2720                self.globstar_walk(
2721                    &child_path,
2722                    segments,
2723                    seg_idx,
2724                    &child_prefix,
2725                    dotglob,
2726                    extglob,
2727                    matches,
2728                );
2729            }
2730        }
2731    }
2732
2733    #[allow(clippy::too_many_arguments)]
2734    fn globstar_walk_segment(
2735        &self,
2736        dir: &str,
2737        seg: &str,
2738        segments: &[&str],
2739        seg_idx: usize,
2740        prefix: &str,
2741        dotglob: bool,
2742        extglob: bool,
2743        matches: &mut Vec<String>,
2744    ) {
2745        let Ok(entries) = self.fs.read_dir(dir) else {
2746            return;
2747        };
2748        let is_last = seg_idx == segments.len() - 1;
2749
2750        for entry in &entries {
2751            if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
2752                continue;
2753            }
2754            self.globstar_handle_matched_entry(
2755                dir,
2756                segments,
2757                seg_idx,
2758                prefix,
2759                dotglob,
2760                extglob,
2761                matches,
2762                &entry.name,
2763                is_last,
2764            );
2765        }
2766    }
2767
2768    #[allow(clippy::too_many_arguments)]
2769    fn globstar_handle_matched_entry(
2770        &self,
2771        dir: &str,
2772        segments: &[&str],
2773        seg_idx: usize,
2774        prefix: &str,
2775        dotglob: bool,
2776        extglob: bool,
2777        matches: &mut Vec<String>,
2778        name: &str,
2779        is_last: bool,
2780    ) {
2781        let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
2782        if is_last {
2783            matches.push(child_prefix);
2784            return;
2785        }
2786        let is_dir = self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false);
2787        if is_dir {
2788            self.globstar_walk(
2789                &child_path,
2790                segments,
2791                seg_idx + 1,
2792                &child_prefix,
2793                dotglob,
2794                extglob,
2795                matches,
2796            );
2797        }
2798    }
2799
2800    fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
2801        let child_path = if dir == "/" {
2802            format!("/{name}")
2803        } else {
2804            format!("{dir}/{name}")
2805        };
2806        let child_prefix = if prefix.is_empty() {
2807            name.to_string()
2808        } else {
2809            format!("{prefix}/{name}")
2810        };
2811        (child_path, child_prefix)
2812    }
2813
2814    /// Write data to a file path, reporting errors to stderr.
2815    fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
2816        match self.fs.open(path, opts) {
2817            Ok(h) => {
2818                if let Err(e) = self.fs.write_file(h, data) {
2819                    self.vm
2820                        .stderr
2821                        .extend_from_slice(format!("wasmsh: write error: {e}\n").as_bytes());
2822                }
2823                self.fs.close(h);
2824            }
2825            Err(e) => {
2826                self.vm
2827                    .stderr
2828                    .extend_from_slice(format!("wasmsh: {target}: {e}\n").as_bytes());
2829            }
2830        }
2831    }
2832
2833    /// Capture stdout data from the given position, truncating the stdout buffer.
2834    fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
2835        let data = self.vm.stdout[from..].to_vec();
2836        self.vm.stdout.truncate(from);
2837        data
2838    }
2839
2840    /// Apply redirections: for `>` and `>>`, write captured stdout/stderr to file.
2841    /// For `<`, read file content (handled pre-execution).
2842    /// Supports fd-specific redirections (2>, 2>>) and &> (both stdout and stderr).
2843    fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
2844        for redir in redirections {
2845            // Resolve command substitutions in redirect targets before expansion
2846            let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
2847            let resolved_target = resolved.first().unwrap_or(&redir.target);
2848            let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
2849            let path = self.resolve_cwd_path(&target);
2850            let fd = redir.fd.unwrap_or(1);
2851
2852            match redir.op {
2853                RedirectionOp::Output => {
2854                    self.apply_output_redir(&path, &target, fd, stdout_before);
2855                }
2856                RedirectionOp::Append => {
2857                    self.apply_append_redir(&path, &target, fd, stdout_before);
2858                }
2859                RedirectionOp::DupOutput => {
2860                    let target_fd: u32 = target.parse().unwrap_or(1);
2861                    let source_fd = redir.fd.unwrap_or(1);
2862                    if source_fd == 2 && target_fd == 1 {
2863                        let stderr_data = std::mem::take(&mut self.vm.stderr);
2864                        self.vm.stdout.extend_from_slice(&stderr_data);
2865                    } else if source_fd == 1 && target_fd == 2 {
2866                        let stdout_data = self.capture_stdout(stdout_before);
2867                        self.vm.stderr.extend_from_slice(&stdout_data);
2868                    }
2869                }
2870                #[allow(unreachable_patterns)]
2871                _ => {}
2872            }
2873        }
2874    }
2875
2876    /// Apply `>` output redirection for a specific fd.
2877    fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2878        let data = if fd == FD_BOTH {
2879            let mut combined = self.capture_stdout(stdout_before);
2880            combined.extend_from_slice(&std::mem::take(&mut self.vm.stderr));
2881            combined
2882        } else if fd == 2 {
2883            std::mem::take(&mut self.vm.stderr)
2884        } else {
2885            self.capture_stdout(stdout_before)
2886        };
2887        self.write_to_file(path, target, &data, OpenOptions::write());
2888    }
2889
2890    /// Apply `>>` append redirection for a specific fd.
2891    fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2892        let data = if fd == 2 {
2893            std::mem::take(&mut self.vm.stderr)
2894        } else {
2895            self.capture_stdout(stdout_before)
2896        };
2897        self.write_to_file(path, target, &data, OpenOptions::append());
2898    }
2899}
2900
2901/// Convert a protocol diagnostic level to a VM diagnostic level.
2902fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
2903    match level {
2904        DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
2905        DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
2906        DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
2907        _ => wasmsh_vm::DiagLevel::Info,
2908    }
2909}
2910
2911// ---- [[ ]] expression evaluator (free functions) ----
2912
2913/// Evaluate an `||` expression (lowest precedence).
2914fn dbl_bracket_eval_or(
2915    tokens: &[String],
2916    pos: &mut usize,
2917    fs: &BackendFs,
2918    state: &mut ShellState,
2919) -> bool {
2920    let mut result = dbl_bracket_eval_and(tokens, pos, fs, state);
2921    while *pos < tokens.len() && tokens[*pos] == "||" {
2922        *pos += 1;
2923        let rhs = dbl_bracket_eval_and(tokens, pos, fs, state);
2924        result = result || rhs;
2925    }
2926    result
2927}
2928
2929/// Evaluate an `&&` expression.
2930fn dbl_bracket_eval_and(
2931    tokens: &[String],
2932    pos: &mut usize,
2933    fs: &BackendFs,
2934    state: &mut ShellState,
2935) -> bool {
2936    let mut result = dbl_bracket_eval_not(tokens, pos, fs, state);
2937    while *pos < tokens.len() && tokens[*pos] == "&&" {
2938        *pos += 1;
2939        let rhs = dbl_bracket_eval_not(tokens, pos, fs, state);
2940        result = result && rhs;
2941    }
2942    result
2943}
2944
2945/// Evaluate a `!` (negation) expression.
2946fn dbl_bracket_eval_not(
2947    tokens: &[String],
2948    pos: &mut usize,
2949    fs: &BackendFs,
2950    state: &mut ShellState,
2951) -> bool {
2952    if *pos < tokens.len() && tokens[*pos] == "!" {
2953        *pos += 1;
2954        return !dbl_bracket_eval_not(tokens, pos, fs, state);
2955    }
2956    dbl_bracket_eval_primary(tokens, pos, fs, state)
2957}
2958
2959/// Evaluate a primary expression: grouped `(expr)`, unary test, binary test, or string truth.
2960fn dbl_bracket_eval_primary(
2961    tokens: &[String],
2962    pos: &mut usize,
2963    fs: &BackendFs,
2964    state: &mut ShellState,
2965) -> bool {
2966    if *pos >= tokens.len() {
2967        return false;
2968    }
2969    if let Some(result) = dbl_bracket_try_group(tokens, pos, fs, state) {
2970        return result;
2971    }
2972    if let Some(result) = dbl_bracket_try_unary(tokens, pos, fs) {
2973        return result;
2974    }
2975    if *pos + 1 == tokens.len() {
2976        return dbl_bracket_take_truthy_token(tokens, pos);
2977    }
2978    if let Some(result) = dbl_bracket_try_binary(tokens, pos, state) {
2979        return result;
2980    }
2981    dbl_bracket_take_truthy_token(tokens, pos)
2982}
2983
2984fn dbl_bracket_try_group(
2985    tokens: &[String],
2986    pos: &mut usize,
2987    fs: &BackendFs,
2988    state: &mut ShellState,
2989) -> Option<bool> {
2990    if tokens.get(*pos).map(String::as_str) != Some("(") {
2991        return None;
2992    }
2993
2994    *pos += 1;
2995    let result = dbl_bracket_eval_or(tokens, pos, fs, state);
2996    if tokens.get(*pos).map(String::as_str) == Some(")") {
2997        *pos += 1;
2998    }
2999    Some(result)
3000}
3001
3002fn dbl_bracket_take_truthy_token(tokens: &[String], pos: &mut usize) -> bool {
3003    let Some(token) = tokens.get(*pos) else {
3004        return false;
3005    };
3006    *pos += 1;
3007    !token.is_empty()
3008}
3009
3010/// Try to evaluate a unary test (`-z`, `-n`, `-f`, etc.). Returns `None` if not a unary op.
3011fn dbl_bracket_try_unary(tokens: &[String], pos: &mut usize, fs: &BackendFs) -> Option<bool> {
3012    if *pos + 1 >= tokens.len() {
3013        return None;
3014    }
3015    let flag = dbl_bracket_parse_unary_flag(&tokens[*pos])?;
3016    match flag {
3017        b'z' | b'n' => Some(dbl_bracket_eval_string_test(tokens, pos, flag)),
3018        b'f' | b'd' | b'e' | b's' | b'r' | b'w' | b'x' => {
3019            dbl_bracket_eval_file_test(tokens, pos, flag, fs)
3020        }
3021        _ => None,
3022    }
3023}
3024
3025fn dbl_bracket_parse_unary_flag(op: &str) -> Option<u8> {
3026    if !op.starts_with('-') || op.len() != 2 {
3027        return None;
3028    }
3029    Some(op.as_bytes()[1])
3030}
3031
3032fn dbl_bracket_eval_string_test(tokens: &[String], pos: &mut usize, flag: u8) -> bool {
3033    *pos += 1;
3034    let arg = &tokens[*pos];
3035    *pos += 1;
3036    if flag == b'z' {
3037        arg.is_empty()
3038    } else {
3039        !arg.is_empty()
3040    }
3041}
3042
3043fn dbl_bracket_eval_file_test(
3044    tokens: &[String],
3045    pos: &mut usize,
3046    flag: u8,
3047    fs: &BackendFs,
3048) -> Option<bool> {
3049    if *pos + 2 < tokens.len() && is_binary_op(&tokens[*pos + 2]) {
3050        return None;
3051    }
3052    *pos += 1;
3053    let path_str = &tokens[*pos];
3054    *pos += 1;
3055    Some(eval_file_test(flag, path_str, fs))
3056}
3057
3058/// Try to evaluate a binary test. Returns `None` if no binary op at pos+1.
3059fn dbl_bracket_try_binary(
3060    tokens: &[String],
3061    pos: &mut usize,
3062    state: &mut ShellState,
3063) -> Option<bool> {
3064    if *pos + 2 > tokens.len() {
3065        return None;
3066    }
3067    let op_idx = *pos + 1;
3068    if op_idx >= tokens.len() || !is_binary_op(&tokens[op_idx]) {
3069        return None;
3070    }
3071    let lhs = tokens[*pos].clone();
3072    *pos += 1;
3073    let op = tokens[*pos].clone();
3074    *pos += 1;
3075
3076    let rhs = dbl_bracket_collect_rhs(tokens, pos, &op);
3077    Some(eval_binary_op(&lhs, &op, &rhs, state))
3078}
3079
3080/// Collect the right-hand side for a binary operator. For `=~`, the RHS extends
3081/// until `&&`, `||`, or end of tokens.
3082fn dbl_bracket_collect_rhs(tokens: &[String], pos: &mut usize, op: &str) -> String {
3083    if *pos >= tokens.len() {
3084        return String::new();
3085    }
3086    if op == "=~" {
3087        return dbl_bracket_collect_regex_rhs(tokens, pos);
3088    }
3089    let rhs = tokens[*pos].clone();
3090    *pos += 1;
3091    rhs
3092}
3093
3094fn dbl_bracket_collect_regex_rhs(tokens: &[String], pos: &mut usize) -> String {
3095    let mut rhs = String::new();
3096    while *pos < tokens.len() && tokens[*pos] != "&&" && tokens[*pos] != "||" {
3097        rhs.push_str(&tokens[*pos]);
3098        *pos += 1;
3099    }
3100    rhs
3101}
3102
3103/// Check whether a token is a binary operator in `[[ ]]` context.
3104fn is_binary_op(s: &str) -> bool {
3105    matches!(
3106        s,
3107        "==" | "!=" | "=~" | "=" | "<" | ">" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge"
3108    )
3109}
3110
3111/// Evaluate a binary operation.
3112fn eval_binary_op(lhs: &str, op: &str, rhs: &str, state: &mut ShellState) -> bool {
3113    match op {
3114        "==" | "=" => glob_cmp(lhs, rhs, state, false),
3115        "!=" => !glob_cmp(lhs, rhs, state, false),
3116        "=~" => eval_regex_match(lhs, rhs, state),
3117        "<" => lhs < rhs,
3118        ">" => lhs > rhs,
3119        _ => eval_int_cmp(lhs, op, rhs),
3120    }
3121}
3122
3123/// Glob-compare lhs against rhs pattern, respecting nocasematch.
3124fn glob_cmp(lhs: &str, rhs: &str, state: &ShellState, _negate: bool) -> bool {
3125    let nocasematch = state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
3126    if nocasematch {
3127        glob_match_inner(rhs.to_lowercase().as_bytes(), lhs.to_lowercase().as_bytes())
3128    } else {
3129        glob_match_inner(rhs.as_bytes(), lhs.as_bytes())
3130    }
3131}
3132
3133/// Evaluate a regex match (`=~`) with capture groups for `BASH_REMATCH`.
3134fn eval_regex_match(lhs: &str, rhs: &str, state: &mut ShellState) -> bool {
3135    let captures = regex_match_with_captures(lhs, rhs);
3136    let br_name = smol_str::SmolStr::from("BASH_REMATCH");
3137    let Some(caps) = captures else {
3138        state.init_indexed_array(br_name);
3139        return false;
3140    };
3141    state.init_indexed_array(br_name.clone());
3142    for (i, cap) in caps.iter().enumerate() {
3143        state.set_array_element(
3144            br_name.clone(),
3145            &i.to_string(),
3146            smol_str::SmolStr::from(cap.as_str()),
3147        );
3148    }
3149    true
3150}
3151
3152/// Evaluate an integer comparison operator (`-eq`, `-ne`, `-lt`, `-le`, `-gt`, `-ge`).
3153fn eval_int_cmp(lhs: &str, op: &str, rhs: &str) -> bool {
3154    let a: i64 = lhs.trim().parse().unwrap_or(0);
3155    let b: i64 = rhs.trim().parse().unwrap_or(0);
3156    match op {
3157        "-eq" => a == b,
3158        "-ne" => a != b,
3159        "-lt" => a < b,
3160        "-le" => a <= b,
3161        "-gt" => a > b,
3162        "-ge" => a >= b,
3163        _ => false,
3164    }
3165}
3166
3167/// Evaluate a unary file test.
3168fn eval_file_test(flag: u8, path: &str, fs: &BackendFs) -> bool {
3169    use wasmsh_fs::Vfs;
3170    match fs.stat(path) {
3171        Ok(meta) => match flag {
3172            b'f' => !meta.is_dir,
3173            b'd' => meta.is_dir,
3174            b's' => meta.size > 0,
3175            // -e, -r, -w, -x: in the VFS all existing files are accessible
3176            b'e' | b'r' | b'w' | b'x' => true,
3177            _ => false,
3178        },
3179        Err(_) => false,
3180    }
3181}
3182
3183/// Strip anchoring from a regex pattern, returning (core, `anchored_start`, `anchored_end`).
3184fn regex_strip_anchors(pattern: &str) -> (&str, bool, bool) {
3185    let anchored_start = pattern.starts_with('^');
3186    let anchored_end = pattern.ends_with('$') && !pattern.ends_with("\\$");
3187    let core = match (anchored_start, anchored_end) {
3188        (true, true) if pattern.len() >= 2 => &pattern[1..pattern.len() - 1],
3189        (true, _) => &pattern[1..],
3190        (_, true) => &pattern[..pattern.len() - 1],
3191        _ => pattern,
3192    };
3193    (core, anchored_start, anchored_end)
3194}
3195
3196/// Check if a regex core has any special regex metacharacters.
3197fn has_regex_metachar(core: &str) -> bool {
3198    core.contains('.')
3199        || core.contains('+')
3200        || core.contains('*')
3201        || core.contains('?')
3202        || core.contains('[')
3203        || core.contains('(')
3204        || core.contains('|')
3205}
3206
3207/// Find match range for a literal pattern with anchoring.
3208fn literal_match_range(text: &str, core: &str, start: bool, end: bool) -> Option<(usize, usize)> {
3209    match (start, end) {
3210        (true, true) if text == core => Some((0, text.len())),
3211        (true, false) if text.starts_with(core) => Some((0, core.len())),
3212        (false, true) if text.ends_with(core) => Some((text.len() - core.len(), text.len())),
3213        (false, false) => text.find(core).map(|pos| (pos, pos + core.len())),
3214        _ => None,
3215    }
3216}
3217
3218/// Regex match with capture group support.
3219///
3220/// Returns `Some(captures)` if the pattern matches, where `captures[0]` is the
3221/// full match and `captures[1..]` are the parenthesized subgroup matches.
3222/// Returns `None` if no match.
3223fn regex_match_with_captures(text: &str, pattern: &str) -> Option<Vec<String>> {
3224    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3225
3226    if !has_regex_metachar(core) {
3227        return regex_match_literal_with_captures(text, core, anchored_start, anchored_end);
3228    }
3229
3230    regex_find_first_match(text, core, anchored_start, anchored_end)
3231}
3232
3233fn regex_find_first_match(
3234    text: &str,
3235    core: &str,
3236    anchored_start: bool,
3237    anchored_end: bool,
3238) -> Option<Vec<String>> {
3239    let end = if anchored_start { 0 } else { text.len() };
3240    for start in 0..=end {
3241        if let Some(result) = regex_match_from_start(text, core, anchored_end, start) {
3242            return Some(result);
3243        }
3244    }
3245    None
3246}
3247
3248fn regex_match_literal_with_captures(
3249    text: &str,
3250    core: &str,
3251    anchored_start: bool,
3252    anchored_end: bool,
3253) -> Option<Vec<String>> {
3254    literal_match_range(text, core, anchored_start, anchored_end)
3255        .map(|(s, e)| vec![text[s..e].to_string()])
3256}
3257
3258fn regex_match_from_start(
3259    text: &str,
3260    core: &str,
3261    anchored_end: bool,
3262    start: usize,
3263) -> Option<Vec<String>> {
3264    let mut group_caps: Vec<(usize, usize)> = Vec::new();
3265    let end = regex_match_capturing(
3266        text.as_bytes(),
3267        start,
3268        core.as_bytes(),
3269        0,
3270        anchored_end,
3271        &mut group_caps,
3272    )?;
3273    Some(regex_build_capture_list(text, start, end, &group_caps))
3274}
3275
3276fn regex_build_capture_list(
3277    text: &str,
3278    start: usize,
3279    end: usize,
3280    group_caps: &[(usize, usize)],
3281) -> Vec<String> {
3282    let mut result = vec![text[start..end].to_string()];
3283    for &(gs, ge) in group_caps {
3284        result.push(text[gs..ge].to_string());
3285    }
3286    result
3287}
3288
3289/// Backtracking regex matcher with capture group support.
3290/// Returns `Some(end_position)` on match, `None` on no match.
3291/// `captures` accumulates (start, end) pairs for each parenthesized group.
3292fn regex_match_capturing(
3293    text: &[u8],
3294    ti: usize,
3295    pat: &[u8],
3296    pi: usize,
3297    must_end: bool,
3298    captures: &mut Vec<(usize, usize)>,
3299) -> Option<usize> {
3300    if pi >= pat.len() {
3301        return regex_check_end(ti, text.len(), must_end);
3302    }
3303
3304    if pat[pi] == b'(' {
3305        return regex_match_group(text, ti, pat, pi, must_end, captures);
3306    }
3307
3308    regex_match_elem(text, ti, pat, pi, must_end, captures)
3309}
3310
3311/// Check if end-of-pattern is valid given anchoring.
3312fn regex_check_end(ti: usize, text_len: usize, must_end: bool) -> Option<usize> {
3313    if must_end && ti < text_len {
3314        None
3315    } else {
3316        Some(ti)
3317    }
3318}
3319
3320/// Handle a parenthesized group in the regex, dispatching by quantifier.
3321fn regex_match_group(
3322    text: &[u8],
3323    ti: usize,
3324    pat: &[u8],
3325    pi: usize,
3326    must_end: bool,
3327    captures: &mut Vec<(usize, usize)>,
3328) -> Option<usize> {
3329    let close = find_matching_paren_bytes(pat, pi + 1)?;
3330    let inner = &pat[pi + 1..close];
3331    let rest = &pat[close + 1..];
3332    let (quant, after_quant_offset) = parse_group_quantifier(pat, close);
3333    let after_quant = &pat[after_quant_offset..];
3334    let alternatives = split_alternatives_bytes(inner);
3335
3336    regex_dispatch_group_quant(
3337        text,
3338        ti,
3339        rest,
3340        after_quant,
3341        must_end,
3342        captures,
3343        &alternatives,
3344        quant,
3345    )
3346}
3347
3348fn parse_group_quantifier(pat: &[u8], close: usize) -> (u8, usize) {
3349    if close + 1 < pat.len() {
3350        match pat[close + 1] {
3351            q @ (b'*' | b'+' | b'?') => (q, close + 2),
3352            _ => (0, close + 1),
3353        }
3354    } else {
3355        (0, close + 1)
3356    }
3357}
3358
3359#[allow(clippy::too_many_arguments)]
3360fn regex_dispatch_group_quant(
3361    text: &[u8],
3362    ti: usize,
3363    rest: &[u8],
3364    after_quant: &[u8],
3365    must_end: bool,
3366    captures: &mut Vec<(usize, usize)>,
3367    alternatives: &[Vec<u8>],
3368    quant: u8,
3369) -> Option<usize> {
3370    match quant {
3371        b'+' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 1),
3372        b'*' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 0),
3373        b'?' => regex_match_group_opt(text, ti, after_quant, must_end, captures, alternatives),
3374        _ => regex_match_group_exact(text, ti, rest, must_end, captures, alternatives),
3375    }
3376}
3377
3378/// Match a group with repetition quantifier (+ or *).
3379fn regex_match_group_rep(
3380    text: &[u8],
3381    ti: usize,
3382    after: &[u8],
3383    must_end: bool,
3384    captures: &mut Vec<(usize, usize)>,
3385    alternatives: &[Vec<u8>],
3386    min_reps: usize,
3387) -> Option<usize> {
3388    let save = captures.len();
3389    for end_pos in (ti..=text.len()).rev() {
3390        captures.truncate(save);
3391        if let Some(result) = regex_try_group_rep_at(
3392            text,
3393            ti,
3394            end_pos,
3395            after,
3396            must_end,
3397            captures,
3398            alternatives,
3399            min_reps,
3400            save,
3401        ) {
3402            return Some(result);
3403        }
3404    }
3405    captures.truncate(save);
3406    None
3407}
3408
3409#[allow(clippy::too_many_arguments)]
3410fn regex_try_group_rep_at(
3411    text: &[u8],
3412    ti: usize,
3413    end_pos: usize,
3414    after: &[u8],
3415    must_end: bool,
3416    captures: &mut Vec<(usize, usize)>,
3417    alternatives: &[Vec<u8>],
3418    min_reps: usize,
3419    save: usize,
3420) -> Option<usize> {
3421    if !regex_match_group_repeated(text, ti, end_pos, alternatives, min_reps) {
3422        return None;
3423    }
3424    let final_end = regex_match_capturing(text, end_pos, after, 0, must_end, captures)?;
3425    captures.insert(save, (ti, end_pos));
3426    Some(final_end)
3427}
3428
3429/// Match a group with `?` quantifier (zero or one).
3430fn regex_match_group_opt(
3431    text: &[u8],
3432    ti: usize,
3433    after: &[u8],
3434    must_end: bool,
3435    captures: &mut Vec<(usize, usize)>,
3436    alternatives: &[Vec<u8>],
3437) -> Option<usize> {
3438    let save = captures.len();
3439    // Try one
3440    if let Some(result) =
3441        regex_try_group_one_alt(text, ti, after, must_end, captures, alternatives, save)
3442    {
3443        return Some(result);
3444    }
3445    // Try zero
3446    captures.truncate(save);
3447    if let Some(final_end) = regex_match_capturing(text, ti, after, 0, must_end, captures) {
3448        captures.insert(save, (ti, ti));
3449        return Some(final_end);
3450    }
3451    captures.truncate(save);
3452    None
3453}
3454
3455fn regex_try_group_one_alt(
3456    text: &[u8],
3457    ti: usize,
3458    after: &[u8],
3459    must_end: bool,
3460    captures: &mut Vec<(usize, usize)>,
3461    alternatives: &[Vec<u8>],
3462    save: usize,
3463) -> Option<usize> {
3464    for alt in alternatives {
3465        captures.truncate(save);
3466        if let Some(result) =
3467            regex_try_alt_then_continue(text, ti, alt, after, must_end, captures, save)
3468        {
3469            return Some(result);
3470        }
3471        captures.truncate(save);
3472    }
3473    None
3474}
3475
3476fn regex_try_alt_then_continue(
3477    text: &[u8],
3478    ti: usize,
3479    alt: &[u8],
3480    after: &[u8],
3481    must_end: bool,
3482    captures: &mut Vec<(usize, usize)>,
3483    save: usize,
3484) -> Option<usize> {
3485    let end = regex_try_match_at(text, ti, alt)?;
3486    let final_end = regex_match_capturing(text, end, after, 0, must_end, captures)?;
3487    captures.insert(save, (ti, end));
3488    Some(final_end)
3489}
3490
3491/// Match a group exactly once (no quantifier).
3492fn regex_match_group_exact(
3493    text: &[u8],
3494    ti: usize,
3495    rest: &[u8],
3496    must_end: bool,
3497    captures: &mut Vec<(usize, usize)>,
3498    alternatives: &[Vec<u8>],
3499) -> Option<usize> {
3500    regex_try_group_one_alt(
3501        text,
3502        ti,
3503        rest,
3504        must_end,
3505        captures,
3506        alternatives,
3507        captures.len(),
3508    )
3509}
3510
3511/// Parse a quantifier after a regex element.
3512fn parse_quantifier(pat: &[u8], pos: usize) -> (u8, usize) {
3513    if pos < pat.len() {
3514        match pat[pos] {
3515            b'*' => (b'*', pos + 1),
3516            b'+' => (b'+', pos + 1),
3517            b'?' => (b'?', pos + 1),
3518            _ => (0, pos),
3519        }
3520    } else {
3521        (0, pos)
3522    }
3523}
3524
3525/// Match a single regex element (not a group) with optional quantifier.
3526fn regex_match_elem(
3527    text: &[u8],
3528    ti: usize,
3529    pat: &[u8],
3530    pi: usize,
3531    must_end: bool,
3532    captures: &mut Vec<(usize, usize)>,
3533) -> Option<usize> {
3534    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3535    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3536
3537    match quant {
3538        b'*' | b'+' => regex_match_repeated_elem(
3539            text,
3540            ti,
3541            pat,
3542            after_quant,
3543            quant,
3544            must_end,
3545            captures,
3546            &matches_fn,
3547        ),
3548        b'?' => {
3549            regex_match_optional_elem(text, ti, pat, after_quant, must_end, captures, &matches_fn)
3550        }
3551        _ => regex_match_single_elem(text, ti, pat, elem_end, must_end, captures, &matches_fn),
3552    }
3553}
3554
3555fn count_regex_matches(text: &[u8], ti: usize, matches_fn: &dyn Fn(u8) -> bool) -> usize {
3556    let mut count = 0;
3557    while ti + count < text.len() && matches_fn(text[ti + count]) {
3558        count += 1;
3559    }
3560    count
3561}
3562
3563fn regex_match_repeated_elem(
3564    text: &[u8],
3565    ti: usize,
3566    pat: &[u8],
3567    after_quant: usize,
3568    quant: u8,
3569    must_end: bool,
3570    captures: &mut Vec<(usize, usize)>,
3571    matches_fn: &dyn Fn(u8) -> bool,
3572) -> Option<usize> {
3573    let min = usize::from(quant == b'+');
3574    let count = count_regex_matches(text, ti, matches_fn);
3575    for c in (min..=count).rev() {
3576        if let Some(end) = regex_match_capturing(text, ti + c, pat, after_quant, must_end, captures)
3577        {
3578            return Some(end);
3579        }
3580    }
3581    None
3582}
3583
3584fn regex_match_optional_elem(
3585    text: &[u8],
3586    ti: usize,
3587    pat: &[u8],
3588    after_quant: usize,
3589    must_end: bool,
3590    captures: &mut Vec<(usize, usize)>,
3591    matches_fn: &dyn Fn(u8) -> bool,
3592) -> Option<usize> {
3593    if ti < text.len() && matches_fn(text[ti]) {
3594        if let Some(end) = regex_match_capturing(text, ti + 1, pat, after_quant, must_end, captures)
3595        {
3596            return Some(end);
3597        }
3598    }
3599    regex_match_capturing(text, ti, pat, after_quant, must_end, captures)
3600}
3601
3602fn regex_match_single_elem(
3603    text: &[u8],
3604    ti: usize,
3605    pat: &[u8],
3606    elem_end: usize,
3607    must_end: bool,
3608    captures: &mut Vec<(usize, usize)>,
3609    matches_fn: &dyn Fn(u8) -> bool,
3610) -> Option<usize> {
3611    if ti < text.len() && matches_fn(text[ti]) {
3612        regex_match_capturing(text, ti + 1, pat, elem_end, must_end, captures)
3613    } else {
3614        None
3615    }
3616}
3617
3618/// Try to match a simple pattern at a position, returning the end position if matched.
3619fn regex_try_match_at(text: &[u8], start: usize, pattern: &[u8]) -> Option<usize> {
3620    regex_try_match_inner(text, start, pattern, 0)
3621}
3622
3623/// Inner helper to find end position of a pattern match.
3624fn regex_try_match_inner(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3625    if pi >= pat.len() {
3626        return Some(ti);
3627    }
3628    if pat[pi] == b'(' {
3629        return regex_try_match_group(text, ti, pat, pi);
3630    }
3631    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3632    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3633    regex_try_apply_quant(text, ti, pat, elem_end, after_quant, quant, &matches_fn)
3634}
3635
3636/// Handle a group in `regex_try_match_inner`.
3637fn regex_try_match_group(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3638    let close = find_matching_paren_bytes(pat, pi + 1)?;
3639    let inner = &pat[pi + 1..close];
3640    let rest = &pat[close + 1..];
3641    let alternatives = split_alternatives_bytes(inner);
3642    for alt in &alternatives {
3643        if let Some(end) = regex_try_alt_and_rest(text, ti, alt, rest) {
3644            return Some(end);
3645        }
3646    }
3647    None
3648}
3649
3650fn regex_try_alt_and_rest(text: &[u8], ti: usize, alt: &[u8], rest: &[u8]) -> Option<usize> {
3651    let after_alt = regex_try_match_inner(text, ti, alt, 0)?;
3652    regex_try_match_inner(text, after_alt, rest, 0)
3653}
3654
3655/// Apply quantifier logic for `regex_try_match_inner`.
3656fn regex_try_apply_quant(
3657    text: &[u8],
3658    ti: usize,
3659    pat: &[u8],
3660    elem_end: usize,
3661    after_quant: usize,
3662    quant: u8,
3663    matches_fn: &dyn Fn(u8) -> bool,
3664) -> Option<usize> {
3665    match quant {
3666        b'*' | b'+' => regex_try_match_repeated_elem(text, ti, pat, after_quant, quant, matches_fn),
3667        b'?' => regex_try_match_optional_elem(text, ti, pat, after_quant, matches_fn),
3668        _ => regex_try_match_single_elem(text, ti, pat, elem_end, matches_fn),
3669    }
3670}
3671
3672fn regex_try_match_repeated_elem(
3673    text: &[u8],
3674    ti: usize,
3675    pat: &[u8],
3676    after_quant: usize,
3677    quant: u8,
3678    matches_fn: &dyn Fn(u8) -> bool,
3679) -> Option<usize> {
3680    let min = usize::from(quant == b'+');
3681    let count = count_regex_matches(text, ti, matches_fn);
3682    for c in (min..=count).rev() {
3683        if let Some(end) = regex_try_match_inner(text, ti + c, pat, after_quant) {
3684            return Some(end);
3685        }
3686    }
3687    None
3688}
3689
3690fn regex_try_match_optional_elem(
3691    text: &[u8],
3692    ti: usize,
3693    pat: &[u8],
3694    after_quant: usize,
3695    matches_fn: &dyn Fn(u8) -> bool,
3696) -> Option<usize> {
3697    if ti < text.len() && matches_fn(text[ti]) {
3698        if let Some(end) = regex_try_match_inner(text, ti + 1, pat, after_quant) {
3699            return Some(end);
3700        }
3701    }
3702    regex_try_match_inner(text, ti, pat, after_quant)
3703}
3704
3705fn regex_try_match_single_elem(
3706    text: &[u8],
3707    ti: usize,
3708    pat: &[u8],
3709    elem_end: usize,
3710    matches_fn: &dyn Fn(u8) -> bool,
3711) -> Option<usize> {
3712    if ti < text.len() && matches_fn(text[ti]) {
3713        regex_try_match_inner(text, ti + 1, pat, elem_end)
3714    } else {
3715        None
3716    }
3717}
3718
3719/// Check if alternatives can be matched repeatedly to fill text[start..end].
3720fn regex_match_group_repeated(
3721    text: &[u8],
3722    start: usize,
3723    end: usize,
3724    alternatives: &[Vec<u8>],
3725    min_reps: usize,
3726) -> bool {
3727    if start == end {
3728        return min_reps == 0;
3729    }
3730    if start > end {
3731        return false;
3732    }
3733    for alt in alternatives {
3734        if regex_group_repetition_matches(text, start, end, alternatives, min_reps, alt) {
3735            return true;
3736        }
3737    }
3738    false
3739}
3740
3741fn regex_group_repetition_matches(
3742    text: &[u8],
3743    start: usize,
3744    end: usize,
3745    alternatives: &[Vec<u8>],
3746    min_reps: usize,
3747    alt: &[u8],
3748) -> bool {
3749    let Some(after) = regex_try_match_inner(text, start, alt, 0) else {
3750        return false;
3751    };
3752    if after <= start || after > end {
3753        return false;
3754    }
3755    if after == end && min_reps <= 1 {
3756        return true;
3757    }
3758    regex_match_group_repeated(text, after, end, alternatives, min_reps.saturating_sub(1))
3759}
3760
3761/// Find matching `)` for a `(` in a byte pattern, handling nesting.
3762fn find_matching_paren_bytes(pat: &[u8], start: usize) -> Option<usize> {
3763    let mut depth = 1;
3764    let mut i = start;
3765    while i < pat.len() {
3766        if pat[i] == b'\\' {
3767            i += 2;
3768            continue;
3769        }
3770        if pat[i] == b'(' {
3771            depth += 1;
3772        } else if pat[i] == b')' {
3773            depth -= 1;
3774            if depth == 0 {
3775                return Some(i);
3776            }
3777        }
3778        i += 1;
3779    }
3780    None
3781}
3782
3783/// Split a byte pattern by `|` at the top level (not inside nested parens).
3784fn split_alternatives_bytes(pat: &[u8]) -> Vec<Vec<u8>> {
3785    let mut alternatives = Vec::new();
3786    let mut current = Vec::new();
3787    let mut depth = 0i32;
3788    let mut i = 0;
3789    while i < pat.len() {
3790        if pat[i] == b'\\' && i + 1 < pat.len() {
3791            current.push(pat[i]);
3792            current.push(pat[i + 1]);
3793            i += 2;
3794            continue;
3795        }
3796        split_alt_classify_byte(pat[i], &mut depth, &mut current, &mut alternatives);
3797        i += 1;
3798    }
3799    alternatives.push(current);
3800    alternatives
3801}
3802
3803fn split_alt_classify_byte(
3804    byte: u8,
3805    depth: &mut i32,
3806    current: &mut Vec<u8>,
3807    alternatives: &mut Vec<Vec<u8>>,
3808) {
3809    match byte {
3810        b'(' => {
3811            *depth += 1;
3812            current.push(byte);
3813        }
3814        b')' => {
3815            *depth -= 1;
3816            current.push(byte);
3817        }
3818        b'|' if *depth == 0 => {
3819            alternatives.push(std::mem::take(current));
3820        }
3821        _ => {
3822            current.push(byte);
3823        }
3824    }
3825}
3826
3827/// Simple regex-like matching for `=~`.
3828///
3829/// Supports: `^prefix`, `suffix$`, `^exact$`, and literal substring match.
3830/// This avoids pulling in a regex crate for wasm32.
3831#[allow(dead_code)]
3832fn simple_regex_match(text: &str, pattern: &str) -> bool {
3833    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3834
3835    if has_regex_metachar(core) {
3836        return regex_like_match(text, pattern);
3837    }
3838
3839    // Pure literal matching with anchoring
3840    literal_match_range(text, core, anchored_start, anchored_end).is_some()
3841}
3842
3843/// A simple regex-like matcher supporting: `.` (any char), `*` (zero or more of previous),
3844/// `+` (one or more of previous), `?` (zero or one of previous), `^`, `$`,
3845/// `[abc]` character classes, `(a|b)` alternation, and literal chars.
3846/// This is intentionally limited but handles common bash `=~` patterns.
3847#[allow(dead_code)]
3848fn regex_like_match(text: &str, pattern: &str) -> bool {
3849    let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3850
3851    if anchored_start {
3852        regex_match_at(text, 0, core, anchored_end)
3853    } else {
3854        (0..=text.len()).any(|start| regex_match_at(text, start, core, anchored_end))
3855    }
3856}
3857
3858/// Try to match `core` pattern starting at byte position `start` in `text`.
3859/// If `must_end` is true, the match must consume through end of `text`.
3860#[allow(dead_code)]
3861fn regex_match_at(text: &str, start: usize, core: &str, must_end: bool) -> bool {
3862    let text_bytes = text.as_bytes();
3863    let core_bytes = core.as_bytes();
3864    regex_backtrack(text_bytes, start, core_bytes, 0, must_end)
3865}
3866
3867/// Recursive backtracking regex matcher.
3868#[allow(dead_code)]
3869fn regex_backtrack(text: &[u8], ti: usize, pat: &[u8], pi: usize, must_end: bool) -> bool {
3870    if pi >= pat.len() {
3871        return if must_end { ti >= text.len() } else { true };
3872    }
3873
3874    let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3875    let (quant, after_quant) = parse_quantifier(pat, elem_end);
3876
3877    match quant {
3878        b'*' => regex_backtrack_star(text, ti, pat, after_quant, must_end, &matches_fn),
3879        b'+' => regex_backtrack_plus(text, ti, pat, after_quant, must_end, &matches_fn),
3880        b'?' => regex_backtrack_optional(text, ti, pat, after_quant, must_end, &matches_fn),
3881        _ => regex_backtrack_single(text, ti, pat, elem_end, must_end, &matches_fn),
3882    }
3883}
3884
3885fn regex_backtrack_star(
3886    text: &[u8],
3887    ti: usize,
3888    pat: &[u8],
3889    after_quant: usize,
3890    must_end: bool,
3891    matches_fn: &dyn Fn(u8) -> bool,
3892) -> bool {
3893    let mut count = 0;
3894    loop {
3895        if regex_backtrack(text, ti + count, pat, after_quant, must_end) {
3896            return true;
3897        }
3898        if ti + count < text.len() && matches_fn(text[ti + count]) {
3899            count += 1;
3900        } else {
3901            return false;
3902        }
3903    }
3904}
3905
3906fn regex_backtrack_plus(
3907    text: &[u8],
3908    ti: usize,
3909    pat: &[u8],
3910    after_quant: usize,
3911    must_end: bool,
3912    matches_fn: &dyn Fn(u8) -> bool,
3913) -> bool {
3914    let count = count_regex_matches(text, ti, matches_fn);
3915    (1..=count).any(|matched| regex_backtrack(text, ti + matched, pat, after_quant, must_end))
3916}
3917
3918fn regex_backtrack_optional(
3919    text: &[u8],
3920    ti: usize,
3921    pat: &[u8],
3922    after_quant: usize,
3923    must_end: bool,
3924    matches_fn: &dyn Fn(u8) -> bool,
3925) -> bool {
3926    regex_backtrack(text, ti, pat, after_quant, must_end)
3927        || (ti < text.len()
3928            && matches_fn(text[ti])
3929            && regex_backtrack(text, ti + 1, pat, after_quant, must_end))
3930}
3931
3932fn regex_backtrack_single(
3933    text: &[u8],
3934    ti: usize,
3935    pat: &[u8],
3936    elem_end: usize,
3937    must_end: bool,
3938    matches_fn: &dyn Fn(u8) -> bool,
3939) -> bool {
3940    ti < text.len()
3941        && matches_fn(text[ti])
3942        && regex_backtrack(text, ti + 1, pat, elem_end, must_end)
3943}
3944
3945/// Parse one regex element at position `pi`, return (`end_pos`, `match_fn`).
3946/// An element is: `.`, `[class]`, `(alt)`, or a literal byte.
3947fn parse_regex_elem(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3948    match pat[pi] {
3949        b'.' => (pi + 1, Box::new(|_: u8| true)),
3950        b'[' => parse_regex_char_class(pat, pi),
3951        b'\\' if pi + 1 < pat.len() => {
3952            let escaped = pat[pi + 1];
3953            (pi + 2, Box::new(move |c: u8| c == escaped))
3954        }
3955        ch => (pi + 1, Box::new(move |c: u8| c == ch)),
3956    }
3957}
3958
3959fn parse_regex_char_class(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3960    let mut i = pi + 1;
3961    let negate = i < pat.len() && (pat[i] == b'^' || pat[i] == b'!');
3962    if negate {
3963        i += 1;
3964    }
3965    let mut chars = Vec::new();
3966    while i < pat.len() && pat[i] != b']' {
3967        if i + 2 < pat.len() && pat[i + 1] == b'-' {
3968            chars.extend(pat[i]..=pat[i + 2]);
3969            i += 3;
3970        } else {
3971            chars.push(pat[i]);
3972            i += 1;
3973        }
3974    }
3975    let end = if i < pat.len() { i + 1 } else { i };
3976    (
3977        end,
3978        Box::new(move |c: u8| regex_char_class_matches(&chars, negate, c)),
3979    )
3980}
3981
3982fn regex_char_class_matches(chars: &[u8], negate: bool, c: u8) -> bool {
3983    let found = chars.contains(&c);
3984    if negate {
3985        !found
3986    } else {
3987        found
3988    }
3989}
3990
3991/// Match a glob character class `[...]` at position `pi` (just past the `[`).
3992/// Returns `(new_pi, matched)` where `new_pi` is past the `]`.
3993fn glob_match_char_class(pattern: &[u8], mut pi: usize, ch: u8) -> (usize, bool) {
3994    let negate = pi < pattern.len() && (pattern[pi] == b'!' || pattern[pi] == b'^');
3995    if negate {
3996        pi += 1;
3997    }
3998    let mut matched = false;
3999    let mut first = true;
4000    while pi < pattern.len() && (first || pattern[pi] != b']') {
4001        first = false;
4002        let (next_pi, item_matched) = glob_match_char_class_item(pattern, pi, ch);
4003        matched |= item_matched;
4004        pi = next_pi;
4005    }
4006    if pi < pattern.len() && pattern[pi] == b']' {
4007        pi += 1;
4008    }
4009    (pi, matched != negate)
4010}
4011
4012fn glob_match_char_class_item(pattern: &[u8], pi: usize, ch: u8) -> (usize, bool) {
4013    if pi + 2 < pattern.len() && pattern[pi + 1] == b'-' {
4014        let lo = pattern[pi];
4015        let hi = pattern[pi + 2];
4016        return (pi + 3, ch >= lo && ch <= hi);
4017    }
4018    (pi + 1, pattern[pi] == ch)
4019}
4020
4021enum GlobPatternStep {
4022    Consume(usize),
4023    Star,
4024    Class(usize, bool),
4025    Mismatch,
4026}
4027
4028fn glob_step(pattern: &[u8], pi: usize, ch: u8) -> GlobPatternStep {
4029    if pi >= pattern.len() {
4030        return GlobPatternStep::Mismatch;
4031    }
4032
4033    match pattern[pi] {
4034        b'?' => GlobPatternStep::Consume(pi + 1),
4035        b'*' => GlobPatternStep::Star,
4036        b'[' => {
4037            let (new_pi, matched) = glob_match_char_class(pattern, pi + 1, ch);
4038            GlobPatternStep::Class(new_pi, matched)
4039        }
4040        literal if literal == ch => GlobPatternStep::Consume(pi + 1),
4041        _ => GlobPatternStep::Mismatch,
4042    }
4043}
4044
4045fn glob_backtrack(pi: &mut usize, ni: &mut usize, star_pi: usize, star_ni: &mut usize) -> bool {
4046    if star_pi == usize::MAX {
4047        return false;
4048    }
4049
4050    *pi = star_pi + 1;
4051    *star_ni += 1;
4052    *ni = *star_ni;
4053    true
4054}
4055
4056/// Core glob pattern matching (byte-level).
4057///
4058/// Supports `*` (any sequence), `?` (one char), and `[abc]` (character class).
4059fn glob_match_inner(pattern: &[u8], name: &[u8]) -> bool {
4060    let mut pi = 0;
4061    let mut ni = 0;
4062    let mut star_pi = usize::MAX;
4063    let mut star_ni = usize::MAX;
4064
4065    while ni < name.len() {
4066        match glob_step(pattern, pi, name[ni]) {
4067            GlobPatternStep::Star => {
4068                star_pi = pi;
4069                star_ni = ni;
4070                pi += 1;
4071            }
4072            GlobPatternStep::Consume(new_pi) | GlobPatternStep::Class(new_pi, true) => {
4073                pi = new_pi;
4074                ni += 1;
4075            }
4076            GlobPatternStep::Class(_, false) | GlobPatternStep::Mismatch => {
4077                if !glob_backtrack(&mut pi, &mut ni, star_pi, &mut star_ni) {
4078                    return false;
4079                }
4080            }
4081        }
4082    }
4083
4084    // Consume trailing stars
4085    while pi < pattern.len() && pattern[pi] == b'*' {
4086        pi += 1;
4087    }
4088
4089    pi == pattern.len()
4090}
4091
4092/// Extended glob matching with dotglob and extglob support.
4093fn glob_match_ext(pattern: &str, name: &str, dotglob: bool, extglob: bool) -> bool {
4094    // Don't match hidden files unless dotglob is enabled or pattern starts with '.'
4095    if name.starts_with('.') && !pattern.starts_with('.') && !dotglob {
4096        return false;
4097    }
4098    if extglob && has_extglob_pattern(pattern) {
4099        return extglob_match(pattern, name);
4100    }
4101    glob_match_inner(pattern.as_bytes(), name.as_bytes())
4102}
4103
4104/// Check if a pattern contains extglob operators: `?(`, `*(`, `+(`, `@(`, `!(`.
4105fn has_extglob_pattern(pattern: &str) -> bool {
4106    let bytes = pattern.as_bytes();
4107    for i in 0..bytes.len().saturating_sub(1) {
4108        if bytes[i + 1] == b'(' && matches!(bytes[i], b'?' | b'*' | b'+' | b'@' | b'!') {
4109            return true;
4110        }
4111    }
4112    false
4113}
4114
4115/// Match a name against an extglob pattern.
4116///
4117/// Supports: `?(pat|pat)`, `*(pat|pat)`, `+(pat|pat)`, `@(pat|pat)`, `!(pat|pat)`.
4118/// Non-extglob portions are handled by regular glob matching.
4119pub fn extglob_match(pattern: &str, name: &str) -> bool {
4120    extglob_match_recursive(pattern.as_bytes(), name.as_bytes())
4121}
4122
4123fn extglob_match_recursive(pattern: &[u8], name: &[u8]) -> bool {
4124    // Find the first extglob operator
4125    let Some((pi, op, close)) = find_extglob_operator(pattern) else {
4126        return glob_match_inner(pattern, name);
4127    };
4128
4129    let open = pi + 2;
4130    let alternatives = split_alternatives(&pattern[open..close]);
4131    let prefix = &pattern[..pi];
4132    let suffix = &pattern[close + 1..];
4133
4134    match op {
4135        b'@' | b'?' => extglob_match_at_or_opt(op, prefix, &alternatives, suffix, name),
4136        b'*' => extglob_star(prefix, &alternatives, suffix, name, 0),
4137        b'+' => extglob_plus(prefix, &alternatives, suffix, name, 0),
4138        b'!' => extglob_match_negate(prefix, &alternatives, suffix, name),
4139        _ => unreachable!(),
4140    }
4141}
4142
4143/// Find the first extglob operator in a pattern, returning (position, operator, `close_paren`).
4144fn find_extglob_operator(pattern: &[u8]) -> Option<(usize, u8, usize)> {
4145    let mut pi = 0;
4146    while pi < pattern.len() {
4147        if pi + 1 < pattern.len()
4148            && pattern[pi + 1] == b'('
4149            && matches!(pattern[pi], b'?' | b'*' | b'+' | b'@' | b'!')
4150        {
4151            if let Some(close) = find_matching_paren(pattern, pi + 2) {
4152                return Some((pi, pattern[pi], close));
4153            }
4154        }
4155        pi += 1;
4156    }
4157    None
4158}
4159
4160/// Build a combined pattern from prefix + alt + suffix.
4161fn build_combined(prefix: &[u8], mid: &[u8], suffix: &[u8]) -> Vec<u8> {
4162    let mut combined = Vec::with_capacity(prefix.len() + mid.len() + suffix.len());
4163    combined.extend_from_slice(prefix);
4164    combined.extend_from_slice(mid);
4165    combined.extend_from_slice(suffix);
4166    combined
4167}
4168
4169/// Handle `@(...)` (exactly one) and `?(...)` (zero or one) extglob patterns.
4170fn extglob_match_at_or_opt(
4171    op: u8,
4172    prefix: &[u8],
4173    alternatives: &[Vec<u8>],
4174    suffix: &[u8],
4175    name: &[u8],
4176) -> bool {
4177    // For `?`, try zero first
4178    if op == b'?' && extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4179        return true;
4180    }
4181    // Try each alternative exactly once
4182    for alt in alternatives {
4183        if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4184            return true;
4185        }
4186    }
4187    false
4188}
4189
4190/// Handle `!(...)` extglob pattern: matches if no alternative matches.
4191fn extglob_match_negate(
4192    prefix: &[u8],
4193    alternatives: &[Vec<u8>],
4194    suffix: &[u8],
4195    name: &[u8],
4196) -> bool {
4197    for alt in alternatives {
4198        if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4199            return false;
4200        }
4201    }
4202    let wildcard = build_combined(prefix, b"*", suffix);
4203    glob_match_inner(&wildcard, name)
4204}
4205
4206/// Try zero or more repetitions of alternatives for `*(...)`.
4207fn extglob_star(
4208    prefix: &[u8],
4209    alternatives: &[Vec<u8>],
4210    suffix: &[u8],
4211    name: &[u8],
4212    depth: u32,
4213) -> bool {
4214    if depth > 20 {
4215        return false;
4216    }
4217    // Try zero repetitions
4218    if extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4219        return true;
4220    }
4221    // Try one repetition followed by zero or more
4222    extglob_try_extend(prefix, alternatives, suffix, name, depth)
4223}
4224
4225fn extglob_try_extend(
4226    prefix: &[u8],
4227    alternatives: &[Vec<u8>],
4228    suffix: &[u8],
4229    name: &[u8],
4230    depth: u32,
4231) -> bool {
4232    let prefix_len = prefix.len();
4233    for alt in alternatives {
4234        let new_prefix = build_combined(prefix, alt, &[]);
4235        if new_prefix.len() > prefix_len
4236            && extglob_star(&new_prefix, alternatives, suffix, name, depth + 1)
4237        {
4238            return true;
4239        }
4240    }
4241    false
4242}
4243
4244/// Try one or more repetitions of alternatives for `+(...)`.
4245fn extglob_plus(
4246    prefix: &[u8],
4247    alternatives: &[Vec<u8>],
4248    suffix: &[u8],
4249    name: &[u8],
4250    depth: u32,
4251) -> bool {
4252    if depth > 20 {
4253        return false;
4254    }
4255    for alt in alternatives {
4256        let new_prefix = build_combined(prefix, alt, &[]);
4257        if extglob_star(&new_prefix, alternatives, suffix, name, depth + 1) {
4258            return true;
4259        }
4260    }
4261    false
4262}
4263
4264/// Find the matching `)` for a `(` at position `open` (character after `(`).
4265fn find_matching_paren(pattern: &[u8], open: usize) -> Option<usize> {
4266    let mut depth: u32 = 1;
4267    let mut i = open;
4268    while i < pattern.len() {
4269        if pattern[i] == b'(' {
4270            depth += 1;
4271        } else if pattern[i] == b')' {
4272            depth -= 1;
4273            if depth == 0 {
4274                return Some(i);
4275            }
4276        }
4277        i += 1;
4278    }
4279    None
4280}
4281
4282/// Split alternatives by `|` at the top level (not inside nested parens).
4283fn split_alternatives(pat: &[u8]) -> Vec<Vec<u8>> {
4284    let mut result = Vec::new();
4285    let mut current = Vec::new();
4286    let mut depth: u32 = 0;
4287    for &b in pat {
4288        if b == b'(' {
4289            depth += 1;
4290            current.push(b);
4291        } else if b == b')' {
4292            depth -= 1;
4293            current.push(b);
4294        } else if b == b'|' && depth == 0 {
4295            result.push(std::mem::take(&mut current));
4296        } else {
4297            current.push(b);
4298        }
4299    }
4300    result.push(current);
4301    result
4302}
4303
4304impl Default for WorkerRuntime {
4305    fn default() -> Self {
4306        Self::new()
4307    }
4308}