Skip to main content

wasmsh_runtime/
lib.rs

1//! Shared shell runtime core for wasmsh.
2//!
3//! Platform-agnostic execution engine:
4//! `parse -> AST -> HIR -> runtime executor`.
5//!
6//! Most shell semantics are executed by interpreting HIR directly inside
7//! this crate. A bounded subset of top-level `and/or` lists is lowered
8//! through `wasmsh-ir` into `wasmsh-vm`, but that is an optimization and
9//! parity path rather than the primary executor for the whole grammar.
10
11mod dbl_bracket;
12mod fd_table;
13mod pattern;
14mod signals;
15mod streaming_cut;
16mod streaming_grep;
17mod streaming_sed;
18mod streaming_tr;
19mod streaming_uniq;
20
21use streaming_cut::{
22    CutStreamReader, StreamingCutMode, StreamingCutParseState, StreamingCutRange, StreamingCutStage,
23};
24use streaming_grep::{GrepStreamReader, StreamingGrepFlags, StreamingGrepStage, StreamingGrepStep};
25use streaming_sed::{parse_streaming_sed_script, SedStreamReader, StreamingSedStage};
26use streaming_tr::{streaming_tr_expand_set, StreamingTrStage, TrStreamReader};
27use streaming_uniq::{StreamingUniqFlags, UniqStreamReader};
28
29use std::cell::RefCell;
30use std::collections::VecDeque;
31use std::io::{Cursor, ErrorKind, Read};
32use std::rc::Rc;
33
34use indexmap::IndexMap;
35
36use crate::dbl_bracket::dbl_bracket_eval_or;
37use crate::fd_table::{ExecIo, InputTarget, OutputTarget};
38use crate::pattern::{glob_match_ext, glob_match_inner, has_extglob_pattern};
39use crate::signals::{find_runtime_signal_spec, RuntimeSignalSpec, SignalDefaultAction};
40
41pub use crate::pattern::extglob_match;
42use wasmsh_ast::{CaseTerminator, RedirectionOp, Word, WordPart};
43use wasmsh_expand::expand_words_argv;
44use wasmsh_fs::{BackendFs, FileHandle, OpenOptions, Vfs, VfsWriteSink};
45use wasmsh_hir::{
46    HirAndOr, HirAndOrOp, HirCommand, HirCompleteCommand, HirPipeline, HirProgram, HirRedirection,
47};
48use wasmsh_ir::{lower_supported_and_or, IrProgram, IrRedirection, LoweringError};
49use wasmsh_protocol::{DiagnosticLevel, HostCommand, WorkerEvent, PROTOCOL_VERSION};
50use wasmsh_state::ShellState;
51use wasmsh_utils::{UtilContext, UtilRegistry};
52use wasmsh_vm::pipe::{PipeBuffer, ReadResult, WriteResult};
53use wasmsh_vm::{BudgetCategory, ExecutionLimits, ExhaustionReason, StopReason, Vm, VmExecutor};
54
55/// Sentinel FD value for `&>` (redirect both stdout and stderr).
56const FD_BOTH: u32 = u32::MAX;
57
58// Runtime-level command names dispatched before builtins.
59const CMD_LOCAL: &str = "local";
60const CMD_BREAK: &str = "break";
61const CMD_CONTINUE: &str = "continue";
62const CMD_EXIT: &str = "exit";
63const CMD_EVAL: &str = "eval";
64const CMD_SOURCE: &str = "source";
65const CMD_DOT: &str = ".";
66const CMD_DECLARE: &str = "declare";
67const CMD_TYPESET: &str = "typeset";
68const CMD_LET: &str = "let";
69const CMD_SHOPT: &str = "shopt";
70const CMD_ALIAS: &str = "alias";
71const CMD_UNALIAS: &str = "unalias";
72const CMD_BUILTIN: &str = "builtin";
73const CMD_MAPFILE: &str = "mapfile";
74const CMD_READARRAY: &str = "readarray";
75const CMD_TYPE: &str = "type";
76const CMD_COMMAND: &str = "command";
77const CMD_EXEC: &str = "exec";
78const CMD_HASH: &str = "hash";
79const CMD_TIMES: &str = "times";
80const CMD_DIRS: &str = "dirs";
81const CMD_PUSHD: &str = "pushd";
82const CMD_POPD: &str = "popd";
83const CMD_UMASK: &str = "umask";
84const CMD_WAIT: &str = "wait";
85const CMD_ULIMIT: &str = "ulimit";
86
87/// Configuration for the browser runtime.
88#[derive(Debug, Clone)]
89pub struct BrowserConfig {
90    pub step_budget: u64,
91    /// Hostnames/IPs allowed for network access (empty = no network).
92    pub allowed_hosts: Vec<String>,
93    pub output_byte_limit: u64,
94    pub pipe_byte_limit: u64,
95    pub recursion_limit: u32,
96    pub vm_subset_enabled: bool,
97}
98
99impl Default for BrowserConfig {
100    fn default() -> Self {
101        Self {
102            step_budget: 100_000,
103            allowed_hosts: Vec::new(),
104            // Secure defaults (B4 from external audit). Set to nonzero so a
105            // caller who forgets to configure limits still gets a 64 MiB
106            // visible cap rather than unlimited memory growth. The previous
107            // 0 == unlimited sentinel is still honored by the runtime path
108            // when callers opt in explicitly via set_output_byte_limit(0).
109            output_byte_limit: 64 * 1024 * 1024,
110            pipe_byte_limit: 64 * 1024 * 1024,
111            recursion_limit: MAX_RECURSION_DEPTH,
112            vm_subset_enabled: true,
113        }
114    }
115}
116
117/// Maximum recursion depth for eval, source, and command substitution.
118const MAX_RECURSION_DEPTH: u32 = 100;
119
120/// Transient execution state, reset between top-level commands.
121#[derive(Clone)]
122#[allow(clippy::struct_excessive_bools)]
123struct ExecState {
124    break_depth: u32,
125    loop_continue: bool,
126    exit_requested: Option<i32>,
127    errexit_suppressed: bool,
128    local_save_stack: Vec<(smol_str::SmolStr, Option<smol_str::SmolStr>)>,
129    recursion_depth: u32,
130    /// Set when a resource limit (step budget, output limit, cancel) is hit.
131    resource_exhausted: bool,
132    stop_reason: Option<StopReason>,
133    /// Set when word expansion reports a hard semantic error.
134    expansion_failed: bool,
135    /// Trap handlers suppress nested trap reentry while they run.
136    trap_depth: u32,
137    /// Nested shell scopes (functions, sourced files, command substitutions).
138    nested_shell_depth: u32,
139    /// Nested output capture scopes for pipelines and substitutions.
140    output_captures: Vec<OutputCapture>,
141}
142
143impl ExecState {
144    fn new() -> Self {
145        Self {
146            break_depth: 0,
147            loop_continue: false,
148            exit_requested: None,
149            errexit_suppressed: false,
150            local_save_stack: Vec::new(),
151            recursion_depth: 0,
152            resource_exhausted: false,
153            stop_reason: None,
154            expansion_failed: false,
155            trap_depth: 0,
156            nested_shell_depth: 0,
157            output_captures: Vec::new(),
158        }
159    }
160
161    fn reset(&mut self) {
162        self.break_depth = 0;
163        self.loop_continue = false;
164        self.exit_requested = None;
165        self.errexit_suppressed = false;
166        self.resource_exhausted = false;
167        self.stop_reason = None;
168        self.expansion_failed = false;
169        self.trap_depth = 0;
170        self.nested_shell_depth = 0;
171        self.output_captures.clear();
172    }
173}
174
175const STREAMING_YES_MAX_LINES: usize = 65_536;
176const PIPEBUFFER_STREAMING_CAPACITY: usize = 1;
177
178#[derive(Clone, Debug, Default)]
179struct OutputCapture {
180    capture_stdout: bool,
181    capture_stderr: bool,
182    stdout: Vec<u8>,
183    stderr: Vec<u8>,
184}
185
186#[derive(Clone, Debug, Default)]
187struct CapturedOutput {
188    stdout: Vec<u8>,
189    stderr: Vec<u8>,
190}
191
192struct RuntimeOutputRouter<'a> {
193    exec: &'a mut ExecState,
194    exec_io: Option<&'a mut ExecIo>,
195    proc_subst_out_scopes: &'a mut Vec<Vec<PendingProcessSubstOut>>,
196    vm_stdout: &'a mut Vec<u8>,
197    vm_stderr: &'a mut Vec<u8>,
198    vm_output_bytes: &'a mut u64,
199    vm_output_limit: u64,
200    vm_diagnostics: &'a mut Vec<wasmsh_vm::DiagnosticEvent>,
201}
202
203impl RuntimeOutputRouter<'_> {
204    fn process_subst_out_sink_mut(&mut self, path: &str) -> Option<&mut PendingProcessSubstOut> {
205        for scope in self.proc_subst_out_scopes.iter_mut().rev() {
206            if let Some(index) = scope.iter().position(|sink| sink.path == path) {
207                return scope.get_mut(index);
208            }
209        }
210        None
211    }
212
213    fn append_visible_output_direct(&mut self, data: &[u8], stdout: bool) {
214        if stdout {
215            self.vm_stdout.extend_from_slice(data);
216        } else {
217            self.vm_stderr.extend_from_slice(data);
218        }
219    }
220
221    fn write_output_destination_direct(&mut self, destination: &OutputTarget, data: &[u8]) -> bool {
222        match destination {
223            OutputTarget::InheritStdout => {
224                self.append_visible_output_direct(data, true);
225                true
226            }
227            OutputTarget::InheritStderr => {
228                self.append_visible_output_direct(data, false);
229                true
230            }
231            OutputTarget::ProcessSubst { path } => {
232                if let Some(sink) = self.process_subst_out_sink_mut(path) {
233                    sink.write(data);
234                }
235                false
236            }
237            OutputTarget::File { path, sink, .. } => {
238                if let Err(err) = sink.borrow_mut().write(data) {
239                    let msg = format!("wasmsh: write error: {err}\n");
240                    self.append_visible_output_direct(msg.as_bytes(), false);
241                    self.vm_diagnostics.push(wasmsh_vm::DiagnosticEvent {
242                        level: wasmsh_vm::DiagLevel::Error,
243                        category: wasmsh_vm::DiagCategory::Filesystem,
244                        message: format!("write failed for {path}: {err}"),
245                    });
246                }
247                false
248            }
249            OutputTarget::Pipe(pipe) => {
250                pipe.borrow_mut().write_all(data);
251                false
252            }
253            OutputTarget::Closed => false,
254        }
255    }
256
257    fn route_output(&mut self, data: &[u8], stdout: bool) -> bool {
258        let mut routed_stdout = stdout;
259        if let Some(exec_io) = self.exec_io.as_deref_mut() {
260            let destination = exec_io.output_target(stdout);
261            match destination {
262                OutputTarget::InheritStdout => {
263                    routed_stdout = true;
264                }
265                OutputTarget::InheritStderr => {
266                    routed_stdout = false;
267                }
268                OutputTarget::File { .. }
269                | OutputTarget::ProcessSubst { .. }
270                | OutputTarget::Pipe(_)
271                | OutputTarget::Closed => {
272                    return self.write_output_destination_direct(&destination, data);
273                }
274            }
275        }
276
277        for capture in self.exec.output_captures.iter_mut().rev() {
278            let should_capture = if routed_stdout {
279                capture.capture_stdout
280            } else {
281                capture.capture_stderr
282            };
283            if !should_capture {
284                continue;
285            }
286            if routed_stdout {
287                capture.stdout.extend_from_slice(data);
288            } else {
289                capture.stderr.extend_from_slice(data);
290            }
291            return false;
292        }
293
294        self.append_visible_output_direct(data, routed_stdout);
295        true
296    }
297
298    fn account_output(&mut self, bytes: usize) {
299        *self.vm_output_bytes += bytes as u64;
300        self.exec.stop_reason = None;
301        if self.exec.resource_exhausted {
302            return;
303        }
304        let used = *self.vm_output_bytes;
305        if self.vm_output_limit > 0 && used > self.vm_output_limit {
306            let reason = ExhaustionReason {
307                category: BudgetCategory::VisibleOutputBytes,
308                used,
309                limit: self.vm_output_limit,
310            };
311            self.exec.resource_exhausted = true;
312            self.exec.stop_reason = Some(StopReason::Exhausted(reason.clone()));
313            self.vm_diagnostics.push(wasmsh_vm::DiagnosticEvent {
314                level: wasmsh_vm::DiagLevel::Error,
315                category: wasmsh_vm::DiagCategory::Budget,
316                message: reason.diagnostic_message(),
317            });
318        }
319    }
320
321    fn write_stdout(&mut self, data: &[u8]) {
322        if self.route_output(data, true) {
323            self.account_output(data.len());
324        }
325    }
326
327    fn write_stderr(&mut self, data: &[u8]) {
328        if self.route_output(data, false) {
329            self.account_output(data.len());
330        }
331    }
332}
333
334struct RuntimeBuiltinSink<'a> {
335    router: &'a mut RuntimeOutputRouter<'a>,
336}
337
338impl wasmsh_builtins::OutputSink for RuntimeBuiltinSink<'_> {
339    fn stdout(&mut self, data: &[u8]) {
340        self.router.write_stdout(data);
341    }
342
343    fn stderr(&mut self, data: &[u8]) {
344        self.router.write_stderr(data);
345    }
346}
347
348struct RuntimeUtilSink<'a> {
349    router: &'a mut RuntimeOutputRouter<'a>,
350}
351
352impl wasmsh_utils::UtilOutput for RuntimeUtilSink<'_> {
353    fn stdout(&mut self, data: &[u8]) {
354        self.router.write_stdout(data);
355    }
356
357    fn stderr(&mut self, data: &[u8]) {
358        self.router.write_stderr(data);
359    }
360}
361
362fn resolve_path_from_cwd(cwd: &str, path: &str) -> String {
363    if path.starts_with('/') {
364        wasmsh_fs::normalize_path(path)
365    } else {
366        wasmsh_fs::normalize_path(&format!("{cwd}/{path}"))
367    }
368}
369
370struct PipeReader {
371    pipe: Rc<RefCell<PipeBuffer>>,
372}
373
374impl PipeReader {
375    fn new(pipe: Rc<RefCell<PipeBuffer>>) -> Self {
376        Self { pipe }
377    }
378}
379
380impl Read for PipeReader {
381    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
382        match self.pipe.borrow_mut().read(buf) {
383            ReadResult::Read(read) => Ok(read),
384            ReadResult::WouldBlock => Err(std::io::Error::new(ErrorKind::WouldBlock, "pipe empty")),
385            ReadResult::Eof => Ok(0),
386        }
387    }
388}
389
390impl Drop for PipeReader {
391    fn drop(&mut self) {
392        self.pipe.borrow_mut().close_read();
393    }
394}
395
396#[derive(Clone, Copy)]
397enum PipeProcessPoll {
398    Ready,
399    PendingRead,
400    PendingWrite,
401    Exited,
402}
403
404struct LiveProcessSubstRunner {
405    isolated_runtime: Option<Box<WorkerRuntime>>,
406    source_pipe: Rc<RefCell<PipeBuffer>>,
407    processes: Vec<StreamingPipeProcess<'static>>,
408    finished: Vec<bool>,
409    final_pipe: Rc<RefCell<PipeBuffer>>,
410    stage_stderr: Vec<Rc<RefCell<Vec<u8>>>>,
411    stage_pipe_stderr: Vec<bool>,
412    captured_stdout: Vec<u8>,
413    captured_stderr: Vec<u8>,
414    captured_diagnostics: Vec<wasmsh_vm::DiagnosticEvent>,
415    done: bool,
416    synced_steps: u64,
417}
418
419struct LiveProcessSubstInReader {
420    isolated_runtime: Option<Box<WorkerRuntime>>,
421    processes: Vec<StreamingPipeProcess<'static>>,
422    finished: Vec<bool>,
423    final_pipe: Rc<RefCell<PipeBuffer>>,
424    stage_stderr: Vec<Rc<RefCell<Vec<u8>>>>,
425    stage_pipe_stderr: Vec<bool>,
426    flushed_stderr: Rc<RefCell<Vec<u8>>>,
427    flushed_diagnostics: Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>,
428    done: bool,
429}
430
431impl LiveProcessSubstInReader {
432    fn finalize_stderr(&mut self) {
433        let mut flushed = self.flushed_stderr.borrow_mut();
434        for (idx, stderr) in self.stage_stderr.iter().enumerate() {
435            if self.stage_pipe_stderr[idx] {
436                continue;
437            }
438            let data = stderr.borrow();
439            if !data.is_empty() {
440                flushed.extend_from_slice(&data);
441            }
442        }
443        if let Some(runtime) = self.isolated_runtime.as_mut() {
444            self.flushed_diagnostics
445                .borrow_mut()
446                .extend(runtime.vm.diagnostics.drain(..));
447        }
448    }
449
450    fn pump(&mut self) -> bool {
451        if self.done {
452            return false;
453        }
454        let progressed = if self.isolated_runtime.is_some() {
455            self.pump_with_isolated_runtime()
456        } else {
457            self.pump_without_runtime_loop()
458        };
459        if self.finished.iter().all(|done| *done) {
460            self.finalize_stderr();
461            self.done = true;
462        }
463        progressed
464    }
465
466    fn pump_with_isolated_runtime(&mut self) -> bool {
467        let runtime = self
468            .isolated_runtime
469            .as_mut()
470            .expect("isolated runtime present");
471        let mut progressed = false;
472        for idx in (0..self.processes.len()).rev() {
473            if self.finished[idx] {
474                continue;
475            }
476            let outcome = self.processes[idx].poll(runtime.as_mut());
477            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
478                progressed = true;
479            }
480        }
481        progressed
482    }
483
484    fn pump_without_runtime_loop(&mut self) -> bool {
485        let mut progressed = false;
486        for idx in (0..self.processes.len()).rev() {
487            if self.finished[idx] {
488                continue;
489            }
490            let outcome = self.processes[idx].poll_without_runtime();
491            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
492                progressed = true;
493            }
494        }
495        progressed
496    }
497}
498
499fn apply_process_poll_outcome(finished: &mut bool, outcome: PipeProcessPoll) -> bool {
500    match outcome {
501        PipeProcessPoll::Ready => true,
502        PipeProcessPoll::PendingRead | PipeProcessPoll::PendingWrite => false,
503        PipeProcessPoll::Exited => {
504            *finished = true;
505            true
506        }
507    }
508}
509
510impl Read for LiveProcessSubstInReader {
511    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
512        loop {
513            let read_result = {
514                let mut pipe = self.final_pipe.borrow_mut();
515                pipe.read(buf)
516            };
517            match read_result {
518                ReadResult::Read(read) => return Ok(read),
519                ReadResult::Eof if self.done => return Ok(0),
520                ReadResult::WouldBlock | ReadResult::Eof => {}
521            }
522
523            if !self.pump() {
524                if self.done {
525                    continue;
526                }
527                return Err(std::io::Error::new(
528                    ErrorKind::WouldBlock,
529                    "process substitution pipeline stalled",
530                ));
531            }
532        }
533    }
534}
535
536impl Drop for LiveProcessSubstInReader {
537    fn drop(&mut self) {
538        self.final_pipe.borrow_mut().close_read();
539        if let Some(runtime) = self.isolated_runtime.as_mut() {
540            for process in &mut self.processes {
541                process.close(runtime.as_mut());
542            }
543        } else {
544            for process in &mut self.processes {
545                process.close_without_runtime();
546            }
547        }
548    }
549}
550
551impl LiveProcessSubstRunner {
552    fn sync_isolated_runtime_with_parent(&mut self, parent: &mut WorkerRuntime) {
553        let Some(runtime) = self.isolated_runtime.as_mut() else {
554            return;
555        };
556        if parent.vm.cancellation_token().is_cancelled() {
557            runtime.vm.cancellation_token().cancel();
558        }
559        let current_steps = runtime.vm.steps;
560        if current_steps > self.synced_steps {
561            let delta = current_steps - self.synced_steps;
562            parent.vm.steps = parent.vm.steps.saturating_add(delta);
563            parent.vm.budget.steps = parent.vm.steps;
564            self.synced_steps = current_steps;
565            if parent.vm.steps > parent.vm.limits.step_limit && parent.vm.limits.step_limit > 0 {
566                let reason = ExhaustionReason {
567                    category: BudgetCategory::Steps,
568                    used: parent.vm.steps,
569                    limit: parent.vm.limits.step_limit,
570                };
571                parent.mark_budget_exhaustion(reason.clone());
572                parent.vm.emit_diagnostic(
573                    wasmsh_vm::DiagLevel::Error,
574                    wasmsh_vm::DiagCategory::Budget,
575                    reason.diagnostic_message(),
576                );
577                runtime.vm.cancellation_token().cancel();
578            }
579        }
580    }
581
582    fn drain_final_pipe(&mut self) -> bool {
583        let mut progressed = false;
584        loop {
585            let mut buffer = [0u8; 4096];
586            let read_result = {
587                let mut pipe = self.final_pipe.borrow_mut();
588                pipe.read(&mut buffer)
589            };
590            match read_result {
591                ReadResult::Read(read) => {
592                    self.captured_stdout.extend_from_slice(&buffer[..read]);
593                    progressed = true;
594                }
595                ReadResult::WouldBlock | ReadResult::Eof => break,
596            }
597        }
598        progressed
599    }
600
601    fn finalize_stderr(&mut self) {
602        for (idx, stderr) in self.stage_stderr.iter().enumerate() {
603            if self.stage_pipe_stderr[idx] {
604                continue;
605            }
606            let data = stderr.borrow();
607            if !data.is_empty() {
608                self.captured_stderr.extend_from_slice(&data);
609            }
610        }
611        if let Some(runtime) = self.isolated_runtime.as_mut() {
612            self.captured_diagnostics
613                .append(&mut runtime.vm.diagnostics);
614        }
615    }
616
617    fn pump(&mut self, parent: Option<&mut WorkerRuntime>) -> bool {
618        if self.done {
619            return false;
620        }
621        let mut progressed = if self.isolated_runtime.is_some() {
622            self.pump_isolated_with_parent(parent)
623        } else {
624            self.pump_without_runtime_pass()
625        };
626        if self.drain_final_pipe() {
627            progressed = true;
628        }
629        if self.finished.iter().all(|done| *done) {
630            self.finalize_stderr();
631            self.done = true;
632        }
633        progressed
634    }
635
636    fn pump_isolated_with_parent(&mut self, parent: Option<&mut WorkerRuntime>) -> bool {
637        let mut parent = parent;
638        if let Some(parent_rt) = parent.as_deref_mut() {
639            self.sync_isolated_runtime_with_parent(parent_rt);
640        }
641        let progressed = self.pump_isolated_pass();
642        if let Some(parent_rt) = parent {
643            self.sync_isolated_runtime_with_parent(parent_rt);
644        }
645        progressed
646    }
647
648    fn pump_isolated_pass(&mut self) -> bool {
649        let runtime = self
650            .isolated_runtime
651            .as_mut()
652            .expect("isolated process substitution runtime missing");
653        let mut progressed = false;
654        for idx in (0..self.processes.len()).rev() {
655            if self.finished[idx] {
656                continue;
657            }
658            let outcome = self.processes[idx].poll(runtime.as_mut());
659            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
660                progressed = true;
661            }
662        }
663        progressed
664    }
665
666    fn pump_without_runtime_pass(&mut self) -> bool {
667        let mut progressed = false;
668        for idx in (0..self.processes.len()).rev() {
669            if self.finished[idx] {
670                continue;
671            }
672            let outcome = self.processes[idx].poll_without_runtime();
673            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
674                progressed = true;
675            }
676        }
677        progressed
678    }
679
680    fn write_input(&mut self, data: &[u8]) {
681        let mut offset = 0;
682        while offset < data.len() && !self.done {
683            let write_result = {
684                let mut pipe = self.source_pipe.borrow_mut();
685                pipe.write(&data[offset..])
686            };
687            match write_result {
688                WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
689                    offset += written;
690                    let _ = self.pump(None);
691                }
692                WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
693                    if !self.pump(None) {
694                        break;
695                    }
696                }
697                WriteResult::BrokenPipe => {
698                    self.source_pipe.borrow_mut().close_write();
699                    while self.pump(None) {}
700                    break;
701                }
702            }
703        }
704    }
705
706    fn write_input_with_parent(&mut self, parent: &mut WorkerRuntime, data: &[u8]) {
707        let mut offset = 0;
708        while offset < data.len() && !self.done {
709            let write_result = {
710                let mut pipe = self.source_pipe.borrow_mut();
711                pipe.write(&data[offset..])
712            };
713            match write_result {
714                WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
715                    offset += written;
716                    let _ = self.pump(Some(parent));
717                }
718                WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
719                    if !self.pump(Some(parent)) {
720                        break;
721                    }
722                }
723                WriteResult::BrokenPipe => {
724                    self.source_pipe.borrow_mut().close_write();
725                    while self.pump(Some(parent)) {}
726                    break;
727                }
728            }
729        }
730    }
731
732    fn finish(&mut self) {
733        if self.done {
734            return;
735        }
736        self.source_pipe.borrow_mut().close_write();
737        while self.pump(None) {}
738        if !self.done {
739            self.finalize_stderr();
740            self.done = true;
741        }
742        let _ = self.drain_final_pipe();
743    }
744
745    fn finish_with_parent(&mut self, parent: &mut WorkerRuntime) {
746        if self.done {
747            return;
748        }
749        self.source_pipe.borrow_mut().close_write();
750        while self.pump(Some(parent)) {}
751        if !self.done {
752            self.finalize_stderr();
753            self.done = true;
754        }
755        self.sync_isolated_runtime_with_parent(parent);
756        let _ = self.drain_final_pipe();
757    }
758}
759
760enum PendingProcessSubstOutMode {
761    Buffered { data: Vec<u8> },
762    Live { runner: LiveProcessSubstRunner },
763}
764
765struct PendingProcessSubstOut {
766    path: String,
767    inner: String,
768    mode: PendingProcessSubstOutMode,
769}
770
771impl std::fmt::Debug for PendingProcessSubstOut {
772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773        f.debug_struct("PendingProcessSubstOut")
774            .field("path", &self.path)
775            .field("inner", &self.inner)
776            .finish_non_exhaustive()
777    }
778}
779
780struct PendingProcessSubstIn {
781    path: String,
782    stderr: Option<Rc<RefCell<Vec<u8>>>>,
783    diagnostics: Option<Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>>,
784}
785
786impl PendingProcessSubstOut {
787    fn clear(&mut self) {
788        match &mut self.mode {
789            PendingProcessSubstOutMode::Buffered { data } => data.clear(),
790            PendingProcessSubstOutMode::Live { .. } => {}
791        }
792    }
793
794    fn write(&mut self, data: &[u8]) {
795        match &mut self.mode {
796            PendingProcessSubstOutMode::Buffered { data: buffered } => {
797                buffered.extend_from_slice(data);
798            }
799            PendingProcessSubstOutMode::Live { runner } => runner.write_input(data),
800        }
801    }
802
803    fn write_with_parent(&mut self, runtime: &mut WorkerRuntime, data: &[u8]) {
804        match &mut self.mode {
805            PendingProcessSubstOutMode::Buffered { data: buffered } => {
806                buffered.extend_from_slice(data);
807            }
808            PendingProcessSubstOutMode::Live { runner } => {
809                if runner.isolated_runtime.is_some() {
810                    runner.write_input_with_parent(runtime, data);
811                } else {
812                    runner.write_input(data);
813                }
814            }
815        }
816    }
817}
818
819#[derive(Clone, Debug)]
820enum BufferedPipelineCommand {
821    Argv(Vec<String>),
822    Hir(HirCommand),
823}
824
825enum StreamingPipeProcess<'a> {
826    Read(PipeReadProcess<'a>),
827    Head(HeadPipeProcess),
828    Tee(TeePipeProcess<'a>),
829    Buffered(BufferedPipeProcess),
830}
831
832impl StreamingPipeProcess<'_> {
833    fn poll(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
834        match self {
835            Self::Read(process) => process.poll(),
836            Self::Head(process) => process.poll(),
837            Self::Tee(process) => process.poll(),
838            Self::Buffered(process) => process.poll(runtime),
839        }
840    }
841
842    fn close(&mut self, runtime: &mut WorkerRuntime) {
843        match self {
844            Self::Tee(process) => process.close(),
845            Self::Buffered(process) => process.close(runtime),
846            Self::Read(_) | Self::Head(_) => {}
847        }
848    }
849
850    fn poll_without_runtime(&mut self) -> PipeProcessPoll {
851        match self {
852            Self::Read(process) => process.poll(),
853            Self::Head(process) => process.poll(),
854            Self::Tee(process) => process.poll(),
855            Self::Buffered(_) => {
856                unreachable!("buffered pipeline stage requires runtime access")
857            }
858        }
859    }
860
861    fn close_without_runtime(&mut self) {
862        match self {
863            Self::Tee(process) => process.close(),
864            Self::Read(_) | Self::Head(_) => {}
865            Self::Buffered(_) => {
866                unreachable!("buffered pipeline stage requires runtime access")
867            }
868        }
869    }
870}
871
872struct BufferedPipeProcess {
873    input: Option<Rc<RefCell<PipeBuffer>>>,
874    output: Rc<RefCell<PipeBuffer>>,
875    command: BufferedPipelineCommand,
876    pipe_stderr: bool,
877    pending_stdout: Vec<u8>,
878    pending_offset: usize,
879    finished: bool,
880    command_ran: bool,
881    stage_stderr: Rc<RefCell<Vec<u8>>>,
882    stage_status: Rc<RefCell<i32>>,
883    staging_path: Option<String>,
884    staging_handle: Option<FileHandle>,
885}
886
887impl BufferedPipeProcess {
888    fn new(
889        input: Option<Rc<RefCell<PipeBuffer>>>,
890        output: Rc<RefCell<PipeBuffer>>,
891        command: BufferedPipelineCommand,
892        pipe_stderr: bool,
893        stage_stderr: Rc<RefCell<Vec<u8>>>,
894        stage_status: Rc<RefCell<i32>>,
895    ) -> Self {
896        Self {
897            input,
898            output,
899            command,
900            pipe_stderr,
901            pending_stdout: Vec::new(),
902            pending_offset: 0,
903            finished: false,
904            command_ran: false,
905            stage_stderr,
906            stage_status,
907            staging_path: None,
908            staging_handle: None,
909        }
910    }
911
912    fn command_label(&self) -> String {
913        match &self.command {
914            BufferedPipelineCommand::Argv(argv) => argv
915                .first()
916                .cloned()
917                .unwrap_or_else(|| "command".to_string()),
918            BufferedPipelineCommand::Hir(cmd) => Self::hir_command_label(cmd).to_string(),
919        }
920    }
921
922    fn hir_command_label(cmd: &HirCommand) -> &'static str {
923        match cmd {
924            HirCommand::Exec(_) => "exec",
925            HirCommand::Assign(_) => "assign",
926            HirCommand::RedirectOnly(_) => "redirect",
927            HirCommand::If(_) => "if",
928            HirCommand::While(_) => "while",
929            HirCommand::Until(_) => "until",
930            HirCommand::For(_) => "for",
931            HirCommand::Subshell(_) => "subshell",
932            HirCommand::Group(_) => "group",
933            HirCommand::FunctionDef(_) => "function",
934            HirCommand::Case(_) => "case",
935            HirCommand::DoubleBracket(_) => "[[",
936            HirCommand::ArithFor(_) => "arith-for",
937            HirCommand::ArithCommand(_) => "arith",
938            HirCommand::Select(_) => "select",
939            _ => "command",
940        }
941    }
942
943    fn ensure_staging_handle(
944        &mut self,
945        runtime: &mut WorkerRuntime,
946    ) -> Result<(String, FileHandle), String> {
947        if let (Some(path), Some(handle)) = (&self.staging_path, self.staging_handle) {
948            return Ok((path.clone(), handle));
949        }
950        let path = format!(
951            "/tmp/_wasmsh_pipe_{}",
952            WorkerRuntime::next_pending_input_id()
953        );
954        let create_handle = runtime
955            .fs
956            .open(&path, OpenOptions::write())
957            .map_err(|err| err.to_string())?;
958        runtime.fs.close(create_handle);
959        let handle = runtime
960            .fs
961            .open(&path, OpenOptions::append())
962            .map_err(|err| err.to_string())?;
963        self.staging_path = Some(path.clone());
964        self.staging_handle = Some(handle);
965        Ok((path, handle))
966    }
967
968    fn emit_error(
969        &mut self,
970        runtime: &mut WorkerRuntime,
971        cmd_name: &str,
972        err: &str,
973    ) -> PipeProcessPoll {
974        *self.stage_status.borrow_mut() = 1;
975        self.stage_stderr.borrow_mut().extend_from_slice(
976            format!("wasmsh: {cmd_name}: failed to stage pipeline input for streaming: {err}\n")
977                .as_bytes(),
978        );
979        self.output.borrow_mut().close_write();
980        self.close(runtime);
981        self.finished = true;
982        PipeProcessPoll::Exited
983    }
984
985    fn run_command(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
986        if let Some(handle) = self.staging_handle.take() {
987            runtime.fs.close(handle);
988        }
989        let saved_exec_io = runtime.current_exec_io.take();
990        if let Some(path) = self.staging_path.take() {
991            runtime.set_pending_input_file(path, true);
992        }
993        let ((), captured) =
994            runtime.with_output_capture(true, self.pipe_stderr, |runtime| match &self.command {
995                BufferedPipelineCommand::Argv(argv) => runtime.execute_argv_command(argv),
996                BufferedPipelineCommand::Hir(cmd) => runtime.execute_command(cmd),
997            });
998        *self.stage_status.borrow_mut() = runtime.vm.state.last_status;
999        if self.pipe_stderr {
1000            self.pending_stdout = captured.stdout;
1001            self.pending_stdout.extend_from_slice(&captured.stderr);
1002        } else {
1003            self.pending_stdout = captured.stdout;
1004            self.stage_stderr
1005                .borrow_mut()
1006                .extend_from_slice(&captured.stderr);
1007        }
1008        runtime.clear_pending_input();
1009        runtime.current_exec_io = saved_exec_io;
1010        self.pending_offset = 0;
1011        self.command_ran = true;
1012        if self.pending_stdout.is_empty() {
1013            self.output.borrow_mut().close_write();
1014            self.finished = true;
1015            PipeProcessPoll::Exited
1016        } else {
1017            PipeProcessPoll::Ready
1018        }
1019    }
1020
1021    fn close(&mut self, runtime: &mut WorkerRuntime) {
1022        if let Some(handle) = self.staging_handle.take() {
1023            runtime.fs.close(handle);
1024        }
1025        if let Some(path) = self.staging_path.take() {
1026            let _ = runtime.fs.remove_file(&path);
1027        }
1028    }
1029
1030    fn poll(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
1031        if self.finished {
1032            return PipeProcessPoll::Exited;
1033        }
1034        if self.pending_offset < self.pending_stdout.len() {
1035            return self.buffered_drain_pending();
1036        }
1037        if self.command_ran {
1038            self.output.borrow_mut().close_write();
1039            self.finished = true;
1040            return PipeProcessPoll::Exited;
1041        }
1042        self.buffered_pump_input(runtime)
1043    }
1044
1045    fn buffered_drain_pending(&mut self) -> PipeProcessPoll {
1046        let write_result = {
1047            let mut pipe = self.output.borrow_mut();
1048            pipe.write(&self.pending_stdout[self.pending_offset..])
1049        };
1050        match write_result {
1051            WriteResult::Written(written) => {
1052                self.pending_offset += written;
1053                if self.pending_offset == self.pending_stdout.len() {
1054                    self.pending_stdout.clear();
1055                    self.pending_offset = 0;
1056                    if self.command_ran {
1057                        self.output.borrow_mut().close_write();
1058                        self.finished = true;
1059                        return PipeProcessPoll::Exited;
1060                    }
1061                }
1062                PipeProcessPoll::Ready
1063            }
1064            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1065            WriteResult::WouldBlock(written) => {
1066                self.pending_offset += written;
1067                PipeProcessPoll::Ready
1068            }
1069            WriteResult::BrokenPipe => {
1070                self.output.borrow_mut().close_write();
1071                self.finished = true;
1072                PipeProcessPoll::Exited
1073            }
1074        }
1075    }
1076
1077    fn buffered_pump_input(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
1078        let Some(input) = &self.input else {
1079            return self.run_command(runtime);
1080        };
1081        let cmd_name = self.command_label();
1082        let mut scratch = [0u8; 4096];
1083        let read_result = {
1084            let mut input = input.borrow_mut();
1085            input.read(&mut scratch)
1086        };
1087        match read_result {
1088            ReadResult::Read(read) => {
1089                let (_, handle) = match self.ensure_staging_handle(runtime) {
1090                    Ok(parts) => parts,
1091                    Err(err) => return self.emit_error(runtime, &cmd_name, &err),
1092                };
1093                if let Err(err) = runtime.fs.write_file(handle, &scratch[..read]) {
1094                    return self.emit_error(runtime, &cmd_name, &err.to_string());
1095                }
1096                PipeProcessPoll::Ready
1097            }
1098            ReadResult::WouldBlock => PipeProcessPoll::PendingRead,
1099            ReadResult::Eof => {
1100                input.borrow_mut().close_read();
1101                self.run_command(runtime)
1102            }
1103        }
1104    }
1105}
1106
1107struct HeadPipeProcess {
1108    input: Rc<RefCell<PipeBuffer>>,
1109    output: Rc<RefCell<PipeBuffer>>,
1110    mode: StreamingHeadMode,
1111    pending: Vec<u8>,
1112    pending_offset: usize,
1113    lines_seen: usize,
1114    input_closed: bool,
1115    stream_complete: bool,
1116    finished: bool,
1117}
1118
1119impl HeadPipeProcess {
1120    fn new(
1121        input: Rc<RefCell<PipeBuffer>>,
1122        output: Rc<RefCell<PipeBuffer>>,
1123        mode: StreamingHeadMode,
1124    ) -> Self {
1125        Self {
1126            input,
1127            output,
1128            mode,
1129            pending: Vec::new(),
1130            pending_offset: 0,
1131            lines_seen: 0,
1132            input_closed: false,
1133            stream_complete: false,
1134            finished: false,
1135        }
1136    }
1137
1138    fn close_input(&mut self) {
1139        if !self.input_closed {
1140            self.input.borrow_mut().close_read();
1141            self.input_closed = true;
1142        }
1143    }
1144
1145    fn finish(&mut self) -> PipeProcessPoll {
1146        self.close_input();
1147        self.output.borrow_mut().close_write();
1148        self.finished = true;
1149        PipeProcessPoll::Exited
1150    }
1151
1152    fn try_flush_pending(&mut self) -> Option<PipeProcessPoll> {
1153        if self.pending_offset >= self.pending.len() {
1154            return None;
1155        }
1156        let write_result = {
1157            let mut pipe = self.output.borrow_mut();
1158            pipe.write(&self.pending[self.pending_offset..])
1159        };
1160        match write_result {
1161            WriteResult::Written(written) => {
1162                self.pending_offset += written;
1163                if self.pending_offset == self.pending.len() {
1164                    self.pending.clear();
1165                    self.pending_offset = 0;
1166                    if self.stream_complete {
1167                        return Some(self.finish());
1168                    }
1169                }
1170                Some(PipeProcessPoll::Ready)
1171            }
1172            WriteResult::WouldBlock(0) => Some(PipeProcessPoll::PendingWrite),
1173            WriteResult::WouldBlock(written) => {
1174                self.pending_offset += written;
1175                Some(PipeProcessPoll::Ready)
1176            }
1177            WriteResult::BrokenPipe => Some(self.finish()),
1178        }
1179    }
1180
1181    fn update_head_limit(&mut self, byte: u8, read: usize) {
1182        match &mut self.mode {
1183            StreamingHeadMode::Bytes(remaining) => {
1184                *remaining = remaining.saturating_sub(read);
1185                if *remaining == 0 {
1186                    self.stream_complete = true;
1187                    self.close_input();
1188                }
1189            }
1190            StreamingHeadMode::Lines(limit) => {
1191                if byte == b'\n' {
1192                    self.lines_seen += 1;
1193                    if self.lines_seen >= *limit {
1194                        self.stream_complete = true;
1195                        self.close_input();
1196                    }
1197                }
1198            }
1199        }
1200    }
1201
1202    fn poll(&mut self) -> PipeProcessPoll {
1203        if self.finished {
1204            return PipeProcessPoll::Exited;
1205        }
1206        loop {
1207            if let Some(result) = self.try_flush_pending() {
1208                return result;
1209            }
1210            if self.stream_complete {
1211                return self.finish();
1212            }
1213
1214            let mut one = [0u8; 1];
1215            let read_result = {
1216                let mut input = self.input.borrow_mut();
1217                input.read(&mut one)
1218            };
1219            match read_result {
1220                ReadResult::Read(read) => {
1221                    self.pending.extend_from_slice(&one[..read]);
1222                    self.update_head_limit(one[0], read);
1223                }
1224                ReadResult::WouldBlock => return PipeProcessPoll::PendingRead,
1225                ReadResult::Eof => {
1226                    self.stream_complete = true;
1227                    self.close_input();
1228                }
1229            }
1230        }
1231    }
1232}
1233
1234struct PipeReadProcess<'a> {
1235    reader: Option<Box<dyn Read + 'a>>,
1236    output: Rc<RefCell<PipeBuffer>>,
1237    pending: Vec<u8>,
1238    pending_offset: usize,
1239    stderr_offset: usize,
1240    finished: bool,
1241    stderr: Rc<RefCell<Vec<u8>>>,
1242    status: Rc<RefCell<i32>>,
1243    label: &'static str,
1244    pipe_stderr: bool,
1245    reader_done: bool,
1246}
1247
1248impl<'a> PipeReadProcess<'a> {
1249    fn new(
1250        reader: Box<dyn Read + 'a>,
1251        output: Rc<RefCell<PipeBuffer>>,
1252        stderr: Rc<RefCell<Vec<u8>>>,
1253        status: Rc<RefCell<i32>>,
1254        label: &'static str,
1255        pipe_stderr: bool,
1256    ) -> Self {
1257        Self {
1258            reader: Some(reader),
1259            output,
1260            pending: Vec::new(),
1261            pending_offset: 0,
1262            stderr_offset: 0,
1263            finished: false,
1264            stderr,
1265            status,
1266            label,
1267            pipe_stderr,
1268            reader_done: false,
1269        }
1270    }
1271
1272    fn finish(&mut self) -> PipeProcessPoll {
1273        self.output.borrow_mut().close_write();
1274        self.reader = None;
1275        self.finished = true;
1276        PipeProcessPoll::Exited
1277    }
1278
1279    fn poll_stderr(&mut self) -> Option<PipeProcessPoll> {
1280        if !self.pipe_stderr {
1281            return None;
1282        }
1283        let len = self.stderr.borrow().len();
1284        if self.stderr_offset >= len {
1285            return None;
1286        }
1287        let chunk = {
1288            let stderr = self.stderr.borrow();
1289            stderr[self.stderr_offset..].to_vec()
1290        };
1291        let write_result = {
1292            let mut output = self.output.borrow_mut();
1293            output.write(&chunk)
1294        };
1295        match write_result {
1296            WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
1297                self.stderr_offset += written;
1298                Some(PipeProcessPoll::Ready)
1299            }
1300            WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
1301                Some(PipeProcessPoll::PendingWrite)
1302            }
1303            WriteResult::BrokenPipe => Some(self.finish()),
1304        }
1305    }
1306
1307    fn poll(&mut self) -> PipeProcessPoll {
1308        if self.finished {
1309            return PipeProcessPoll::Exited;
1310        }
1311        loop {
1312            if let Some(poll) = self.read_drain_pending() {
1313                return poll;
1314            }
1315            if let Some(poll) = self.poll_stderr() {
1316                return poll;
1317            }
1318            if self.reader_done {
1319                return self.finish();
1320            }
1321            if let Some(poll) = self.read_fill_from_reader() {
1322                return poll;
1323            }
1324        }
1325    }
1326
1327    fn read_drain_pending(&mut self) -> Option<PipeProcessPoll> {
1328        if self.pending_offset >= self.pending.len() {
1329            return None;
1330        }
1331        let write_result = {
1332            let mut pipe = self.output.borrow_mut();
1333            pipe.write(&self.pending[self.pending_offset..])
1334        };
1335        Some(match write_result {
1336            WriteResult::Written(written) => {
1337                self.pending_offset += written;
1338                if self.pending_offset == self.pending.len() {
1339                    self.pending.clear();
1340                    self.pending_offset = 0;
1341                }
1342                PipeProcessPoll::Ready
1343            }
1344            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1345            WriteResult::WouldBlock(written) => {
1346                self.pending_offset += written;
1347                PipeProcessPoll::Ready
1348            }
1349            WriteResult::BrokenPipe => self.finish(),
1350        })
1351    }
1352
1353    fn read_fill_from_reader(&mut self) -> Option<PipeProcessPoll> {
1354        let mut buffer = [0u8; 4096];
1355        let reader = self
1356            .reader
1357            .as_mut()
1358            .expect("pipe read process polled after reader finished");
1359        match reader.read(&mut buffer) {
1360            Ok(0) => {
1361                self.reader_done = true;
1362                None
1363            }
1364            Ok(read) => {
1365                self.pending.extend_from_slice(&buffer[..read]);
1366                None
1367            }
1368            Err(err) if err.kind() == ErrorKind::WouldBlock => Some(PipeProcessPoll::PendingRead),
1369            Err(err) => {
1370                *self.status.borrow_mut() = 1;
1371                self.stderr.borrow_mut().extend_from_slice(
1372                    format!(
1373                        "wasmsh: {}: streaming pipeline read error: {err}\n",
1374                        self.label
1375                    )
1376                    .as_bytes(),
1377                );
1378                self.reader_done = true;
1379                None
1380            }
1381        }
1382    }
1383}
1384
1385struct TeePipeProcess<'a> {
1386    reader: Option<Box<dyn Read + 'a>>,
1387    output: Rc<RefCell<PipeBuffer>>,
1388    pending: Vec<u8>,
1389    pending_offset: usize,
1390    stderr_offset: usize,
1391    finished: bool,
1392    stderr: Rc<RefCell<Vec<u8>>>,
1393    status: Rc<RefCell<i32>>,
1394    targets: Vec<TeeTarget>,
1395    pipe_stderr: bool,
1396    reader_done: bool,
1397}
1398
1399impl<'a> TeePipeProcess<'a> {
1400    fn new(
1401        reader: Box<dyn Read + 'a>,
1402        output: Rc<RefCell<PipeBuffer>>,
1403        fs: &mut BackendFs,
1404        cwd: &str,
1405        stage: &StreamingTeeStage,
1406        stderr: Rc<RefCell<Vec<u8>>>,
1407        status: Rc<RefCell<i32>>,
1408        pipe_stderr: bool,
1409    ) -> Self {
1410        let mut targets = Vec::new();
1411        for path in &stage.paths {
1412            let resolved = resolve_path_from_cwd(cwd, path);
1413            match fs.open_write_sink(&resolved, stage.append) {
1414                Ok(sink) => targets.push(TeeTarget {
1415                    display_path: path.clone(),
1416                    sink,
1417                }),
1418                Err(err) => {
1419                    stderr
1420                        .borrow_mut()
1421                        .extend_from_slice(format!("tee: {path}: {err}\n").as_bytes());
1422                    *status.borrow_mut() = 1;
1423                }
1424            }
1425        }
1426        Self {
1427            reader: Some(reader),
1428            output,
1429            pending: Vec::new(),
1430            pending_offset: 0,
1431            stderr_offset: 0,
1432            finished: false,
1433            stderr,
1434            status,
1435            targets,
1436            pipe_stderr,
1437            reader_done: false,
1438        }
1439    }
1440
1441    fn close(&mut self) {
1442        self.reader = None;
1443        self.targets.clear();
1444    }
1445
1446    fn finish(&mut self) -> PipeProcessPoll {
1447        self.output.borrow_mut().close_write();
1448        self.close();
1449        self.finished = true;
1450        PipeProcessPoll::Exited
1451    }
1452
1453    fn write_targets(&mut self, chunk: &[u8]) {
1454        for target in &mut self.targets {
1455            if let Err(err) = target.sink.write(chunk) {
1456                self.stderr
1457                    .borrow_mut()
1458                    .extend_from_slice(format!("tee: {}: {err}\n", target.display_path).as_bytes());
1459                *self.status.borrow_mut() = 1;
1460            }
1461        }
1462    }
1463
1464    fn poll(&mut self) -> PipeProcessPoll {
1465        if self.finished {
1466            return PipeProcessPoll::Exited;
1467        }
1468        loop {
1469            if let Some(poll) = self.tee_drain_pending() {
1470                return poll;
1471            }
1472            if let Some(poll) = self.tee_drain_stderr() {
1473                return poll;
1474            }
1475            if self.reader_done {
1476                return self.finish();
1477            }
1478            if let Some(poll) = self.tee_fill_from_reader() {
1479                return poll;
1480            }
1481        }
1482    }
1483
1484    fn tee_drain_pending(&mut self) -> Option<PipeProcessPoll> {
1485        if self.pending_offset >= self.pending.len() {
1486            return None;
1487        }
1488        let write_result = {
1489            let mut pipe = self.output.borrow_mut();
1490            pipe.write(&self.pending[self.pending_offset..])
1491        };
1492        Some(match write_result {
1493            WriteResult::Written(written) => {
1494                let end = self.pending_offset + written;
1495                let chunk = self.pending[self.pending_offset..end].to_vec();
1496                self.write_targets(&chunk);
1497                self.pending_offset += written;
1498                if self.pending_offset == self.pending.len() {
1499                    self.pending.clear();
1500                    self.pending_offset = 0;
1501                }
1502                PipeProcessPoll::Ready
1503            }
1504            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1505            WriteResult::WouldBlock(written) => {
1506                let end = self.pending_offset + written;
1507                let chunk = self.pending[self.pending_offset..end].to_vec();
1508                self.write_targets(&chunk);
1509                self.pending_offset += written;
1510                PipeProcessPoll::Ready
1511            }
1512            WriteResult::BrokenPipe => self.finish(),
1513        })
1514    }
1515
1516    fn tee_drain_stderr(&mut self) -> Option<PipeProcessPoll> {
1517        if !self.pipe_stderr {
1518            return None;
1519        }
1520        let len = self.stderr.borrow().len();
1521        if self.stderr_offset >= len {
1522            return None;
1523        }
1524        let chunk = {
1525            let stderr = self.stderr.borrow();
1526            stderr[self.stderr_offset..].to_vec()
1527        };
1528        let write_result = {
1529            let mut output = self.output.borrow_mut();
1530            output.write(&chunk)
1531        };
1532        Some(match write_result {
1533            WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
1534                self.stderr_offset += written;
1535                PipeProcessPoll::Ready
1536            }
1537            WriteResult::Written(_) | WriteResult::WouldBlock(_) => PipeProcessPoll::PendingWrite,
1538            WriteResult::BrokenPipe => self.finish(),
1539        })
1540    }
1541
1542    fn tee_fill_from_reader(&mut self) -> Option<PipeProcessPoll> {
1543        let mut buffer = [0u8; 4096];
1544        let reader = self
1545            .reader
1546            .as_mut()
1547            .expect("tee pipe process polled after reader finished");
1548        match reader.read(&mut buffer) {
1549            Ok(0) => {
1550                self.reader_done = true;
1551                None
1552            }
1553            Ok(read) => {
1554                self.pending.extend_from_slice(&buffer[..read]);
1555                None
1556            }
1557            Err(err) if err.kind() == ErrorKind::WouldBlock => Some(PipeProcessPoll::PendingRead),
1558            Err(err) => {
1559                *self.status.borrow_mut() = 1;
1560                self.stderr.borrow_mut().extend_from_slice(
1561                    format!("wasmsh: tee: streaming pipeline read error: {err}\n").as_bytes(),
1562                );
1563                self.reader_done = true;
1564                None
1565            }
1566        }
1567    }
1568}
1569
1570#[derive(Clone, Copy, Debug)]
1571enum StreamingHeadMode {
1572    Lines(usize),
1573    Bytes(usize),
1574}
1575
1576#[derive(Clone, Copy, Debug)]
1577enum StreamingTailMode {
1578    Lines(usize),
1579    Bytes(usize),
1580}
1581
1582struct YesStreamReader {
1583    line: Vec<u8>,
1584    offset: usize,
1585    remaining_lines: usize,
1586}
1587
1588impl YesStreamReader {
1589    fn new(line: Vec<u8>, remaining_lines: usize) -> Self {
1590        Self {
1591            line,
1592            offset: 0,
1593            remaining_lines,
1594        }
1595    }
1596}
1597
1598impl Read for YesStreamReader {
1599    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1600        if buf.is_empty() || self.line.is_empty() || self.remaining_lines == 0 {
1601            return Ok(0);
1602        }
1603        let mut written = 0usize;
1604        while written < buf.len() && self.remaining_lines > 0 {
1605            let remaining_line = &self.line[self.offset..];
1606            let to_copy = remaining_line.len().min(buf.len() - written);
1607            buf[written..written + to_copy].copy_from_slice(&remaining_line[..to_copy]);
1608            written += to_copy;
1609            self.offset += to_copy;
1610            if self.offset == self.line.len() {
1611                self.offset = 0;
1612                self.remaining_lines = self.remaining_lines.saturating_sub(1);
1613            }
1614        }
1615        Ok(written)
1616    }
1617}
1618
1619struct HeadStreamReader<R> {
1620    inner: R,
1621    mode: StreamingHeadMode,
1622    finished: bool,
1623    pending: Vec<u8>,
1624    pending_offset: usize,
1625    lines_seen: usize,
1626}
1627
1628struct TailStreamReader<R> {
1629    inner: R,
1630    mode: StreamingTailMode,
1631    output_pending: Vec<u8>,
1632    output_offset: usize,
1633    finalized: bool,
1634    byte_ring: VecDeque<u8>,
1635    line_ring: VecDeque<Vec<u8>>,
1636    current_line: Vec<u8>,
1637}
1638
1639#[derive(Clone, Copy, Debug)]
1640struct StreamingBatStage {
1641    show_numbers: bool,
1642    show_header: bool,
1643    line_range: Option<(Option<usize>, Option<usize>)>,
1644    show_all: bool,
1645}
1646
1647struct BatStreamReader<R> {
1648    inner: R,
1649    stage: StreamingBatStage,
1650    input_pending: Vec<u8>,
1651    output_pending: Vec<u8>,
1652    output_offset: usize,
1653    finished: bool,
1654    header_emitted: bool,
1655    footer_emitted: bool,
1656    line_num: usize,
1657}
1658
1659#[derive(Clone, Debug)]
1660struct StreamingPasteStage {
1661    delimiter: String,
1662    serial: bool,
1663}
1664
1665struct PasteStreamReader<R> {
1666    inner: R,
1667    stage: StreamingPasteStage,
1668    input_pending: Vec<u8>,
1669    output_pending: Vec<u8>,
1670    output_offset: usize,
1671    finalized: bool,
1672    ended_with_newline: bool,
1673    serial_first: bool,
1674}
1675
1676#[derive(Clone, Copy, Debug)]
1677struct StreamingColumnStage;
1678
1679struct ColumnStreamReader<R> {
1680    inner: R,
1681    output_pending: Vec<u8>,
1682    output_offset: usize,
1683    finalized: bool,
1684    ended_with_newline: bool,
1685}
1686
1687#[derive(Clone, Debug)]
1688struct StreamingTeeStage {
1689    append: bool,
1690    paths: Vec<String>,
1691}
1692
1693struct TeeTarget {
1694    display_path: String,
1695    sink: Box<dyn VfsWriteSink>,
1696}
1697
1698#[derive(Clone, Copy, Debug)]
1699#[allow(clippy::struct_excessive_bools)]
1700struct StreamingWcFlags {
1701    lines: bool,
1702    words: bool,
1703    bytes: bool,
1704    max_line_length: bool,
1705}
1706
1707#[allow(clippy::struct_excessive_bools)]
1708struct WcStreamReader<R> {
1709    inner: R,
1710    flags: StreamingWcFlags,
1711    summary: Vec<u8>,
1712    summary_offset: usize,
1713    finalized: bool,
1714    lines: usize,
1715    words: usize,
1716    bytes: usize,
1717    max_line_length: usize,
1718    current_line_length: usize,
1719    in_word: bool,
1720    saw_input: bool,
1721    ended_with_newline: bool,
1722}
1723
1724#[derive(Copy, Clone, Debug)]
1725enum StreamingSedStep {
1726    Advance(usize),
1727    Break,
1728}
1729
1730#[derive(Default)]
1731#[allow(clippy::struct_excessive_bools)]
1732struct TypeFlags {
1733    all: bool,
1734    skip_functions: bool,
1735    path_only: bool,
1736    force_path: bool,
1737    type_only: bool,
1738}
1739
1740impl<R> WcStreamReader<R> {
1741    fn new(inner: R, flags: StreamingWcFlags) -> Self {
1742        Self {
1743            inner,
1744            flags,
1745            summary: Vec::new(),
1746            summary_offset: 0,
1747            finalized: false,
1748            lines: 0,
1749            words: 0,
1750            bytes: 0,
1751            max_line_length: 0,
1752            current_line_length: 0,
1753            in_word: false,
1754            saw_input: false,
1755            ended_with_newline: false,
1756        }
1757    }
1758
1759    fn take_summary(&mut self, buf: &mut [u8]) -> usize {
1760        if self.summary_offset >= self.summary.len() {
1761            return 0;
1762        }
1763        let remaining = &self.summary[self.summary_offset..];
1764        let to_copy = remaining.len().min(buf.len());
1765        buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
1766        self.summary_offset += to_copy;
1767        to_copy
1768    }
1769
1770    fn process_chunk(&mut self, chunk: &[u8]) {
1771        if chunk.is_empty() {
1772            return;
1773        }
1774        self.saw_input = true;
1775        self.bytes += chunk.len();
1776        for &byte in chunk {
1777            let is_whitespace = byte.is_ascii_whitespace();
1778            if is_whitespace {
1779                self.in_word = false;
1780            } else if !self.in_word {
1781                self.words += 1;
1782                self.in_word = true;
1783            }
1784
1785            if byte == b'\n' {
1786                self.lines += 1;
1787                self.max_line_length = self.max_line_length.max(self.current_line_length);
1788                self.current_line_length = 0;
1789                self.ended_with_newline = true;
1790            } else {
1791                self.current_line_length += 1;
1792                self.ended_with_newline = false;
1793            }
1794        }
1795    }
1796
1797    fn finalize_summary(&mut self) {
1798        if self.finalized {
1799            return;
1800        }
1801        self.finalized = true;
1802        if self.saw_input && !self.ended_with_newline {
1803            self.lines += 1;
1804            self.max_line_length = self.max_line_length.max(self.current_line_length);
1805        }
1806
1807        let mut parts = Vec::new();
1808        if self.flags.lines {
1809            parts.push(self.lines.to_string());
1810        }
1811        if self.flags.words {
1812            parts.push(self.words.to_string());
1813        }
1814        if self.flags.bytes {
1815            parts.push(self.bytes.to_string());
1816        }
1817        if self.flags.max_line_length {
1818            parts.push(self.max_line_length.to_string());
1819        }
1820        let mut output = parts.join(" ");
1821        output.push('\n');
1822        self.summary = output.into_bytes();
1823    }
1824}
1825
1826impl<R: Read> Read for WcStreamReader<R> {
1827    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1828        if buf.is_empty() {
1829            return Ok(0);
1830        }
1831        let copied = self.take_summary(buf);
1832        if copied > 0 {
1833            return Ok(copied);
1834        }
1835        if self.finalized {
1836            return Ok(0);
1837        }
1838
1839        let mut scratch = [0u8; 4096];
1840        loop {
1841            let read = self.inner.read(&mut scratch)?;
1842            if read == 0 {
1843                self.finalize_summary();
1844                return Ok(self.take_summary(buf));
1845            }
1846            self.process_chunk(&scratch[..read]);
1847        }
1848    }
1849}
1850
1851impl<R> HeadStreamReader<R> {
1852    fn new(inner: R, mode: StreamingHeadMode) -> Self {
1853        Self {
1854            inner,
1855            mode,
1856            finished: false,
1857            pending: Vec::new(),
1858            pending_offset: 0,
1859            lines_seen: 0,
1860        }
1861    }
1862
1863    fn take_from_pending(&mut self, buf: &mut [u8]) -> usize {
1864        if self.pending_offset >= self.pending.len() {
1865            self.pending.clear();
1866            self.pending_offset = 0;
1867            return 0;
1868        }
1869        let remaining = &self.pending[self.pending_offset..];
1870        let to_copy = remaining.len().min(buf.len());
1871        buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
1872        self.pending_offset += to_copy;
1873        if self.pending_offset == self.pending.len() {
1874            self.pending.clear();
1875            self.pending_offset = 0;
1876        }
1877        to_copy
1878    }
1879}
1880
1881impl<R: Read> Read for HeadStreamReader<R> {
1882    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1883        if buf.is_empty() {
1884            return Ok(0);
1885        }
1886        let copied = self.take_from_pending(buf);
1887        if copied > 0 {
1888            return Ok(copied);
1889        }
1890        if self.finished {
1891            return Ok(0);
1892        }
1893        match self.mode {
1894            StreamingHeadMode::Bytes(_) => self.read_bytes_mode(buf),
1895            StreamingHeadMode::Lines(limit) => self.read_lines_mode(buf, limit),
1896        }
1897    }
1898}
1899
1900impl<R: Read> HeadStreamReader<R> {
1901    fn read_bytes_mode(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1902        let StreamingHeadMode::Bytes(ref mut remaining) = self.mode else {
1903            unreachable!("read_bytes_mode called in non-Bytes mode")
1904        };
1905        if *remaining == 0 {
1906            self.finished = true;
1907            return Ok(0);
1908        }
1909        let to_read = (*remaining).min(buf.len());
1910        let read = self.inner.read(&mut buf[..to_read])?;
1911        *remaining = remaining.saturating_sub(read);
1912        if read == 0 || *remaining == 0 {
1913            self.finished = true;
1914        }
1915        Ok(read)
1916    }
1917
1918    fn read_lines_mode(&mut self, buf: &mut [u8], limit: usize) -> std::io::Result<usize> {
1919        if self.lines_seen >= limit {
1920            self.finished = true;
1921            return Ok(0);
1922        }
1923        let mut produced = 0usize;
1924        while produced < buf.len() && self.lines_seen < limit {
1925            match self.read_one_line_byte(&mut buf[produced..=produced], produced)? {
1926                HeadLinesStep::Produced => produced += 1,
1927                HeadLinesStep::EofBreak => break,
1928                HeadLinesStep::WouldBlockYield => return Ok(produced),
1929            }
1930        }
1931        if self.lines_seen >= limit {
1932            self.finished = true;
1933        }
1934        Ok(produced)
1935    }
1936
1937    fn read_one_line_byte(
1938        &mut self,
1939        slot: &mut [u8],
1940        produced: usize,
1941    ) -> std::io::Result<HeadLinesStep> {
1942        let read = match self.inner.read(slot) {
1943            Ok(n) => n,
1944            Err(err) if err.kind() == ErrorKind::WouldBlock && produced > 0 => {
1945                return Ok(HeadLinesStep::WouldBlockYield);
1946            }
1947            Err(err) => return Err(err),
1948        };
1949        if read == 0 {
1950            self.finished = true;
1951            return Ok(HeadLinesStep::EofBreak);
1952        }
1953        if slot[0] == b'\n' {
1954            self.lines_seen += 1;
1955        }
1956        Ok(HeadLinesStep::Produced)
1957    }
1958}
1959
1960enum HeadLinesStep {
1961    Produced,
1962    EofBreak,
1963    WouldBlockYield,
1964}
1965
1966impl<R> TailStreamReader<R> {
1967    fn new(inner: R, mode: StreamingTailMode) -> Self {
1968        Self {
1969            inner,
1970            mode,
1971            output_pending: Vec::new(),
1972            output_offset: 0,
1973            finalized: false,
1974            byte_ring: VecDeque::new(),
1975            line_ring: VecDeque::new(),
1976            current_line: Vec::new(),
1977        }
1978    }
1979
1980    fn push_tail_byte(&mut self, byte: u8) {
1981        let StreamingTailMode::Bytes(limit) = self.mode else {
1982            return;
1983        };
1984        if limit == 0 {
1985            return;
1986        }
1987        if self.byte_ring.len() == limit {
1988            self.byte_ring.pop_front();
1989        }
1990        self.byte_ring.push_back(byte);
1991    }
1992
1993    fn push_tail_line(&mut self, line: Vec<u8>) {
1994        let StreamingTailMode::Lines(limit) = self.mode else {
1995            return;
1996        };
1997        if limit == 0 {
1998            return;
1999        }
2000        if self.line_ring.len() == limit {
2001            self.line_ring.pop_front();
2002        }
2003        self.line_ring.push_back(line);
2004    }
2005
2006    fn process_chunk(&mut self, chunk: &[u8]) {
2007        match self.mode {
2008            StreamingTailMode::Bytes(_) => {
2009                for &byte in chunk {
2010                    self.push_tail_byte(byte);
2011                }
2012            }
2013            StreamingTailMode::Lines(_) => {
2014                for &byte in chunk {
2015                    if byte == b'\n' {
2016                        let line = std::mem::take(&mut self.current_line);
2017                        self.push_tail_line(line);
2018                    } else {
2019                        self.current_line.push(byte);
2020                    }
2021                }
2022            }
2023        }
2024    }
2025
2026    fn finalize_output(&mut self) {
2027        if self.finalized {
2028            return;
2029        }
2030        match self.mode {
2031            StreamingTailMode::Bytes(_) => {
2032                self.output_pending.extend(self.byte_ring.drain(..));
2033            }
2034            StreamingTailMode::Lines(_) => {
2035                if !self.current_line.is_empty() {
2036                    let line = std::mem::take(&mut self.current_line);
2037                    self.push_tail_line(line);
2038                }
2039                for line in self.line_ring.drain(..) {
2040                    self.output_pending.extend_from_slice(&line);
2041                    self.output_pending.push(b'\n');
2042                }
2043            }
2044        }
2045        self.finalized = true;
2046    }
2047}
2048
2049impl<R: Read> Read for TailStreamReader<R> {
2050    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2051        if buf.is_empty() {
2052            return Ok(0);
2053        }
2054        let copied = take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2055        if copied > 0 {
2056            return Ok(copied);
2057        }
2058        if self.finalized {
2059            return Ok(0);
2060        }
2061        loop {
2062            let mut scratch = [0u8; 4096];
2063            match self.inner.read(&mut scratch) {
2064                Ok(0) => {
2065                    self.finalize_output();
2066                    return Ok(take_pending_output(
2067                        &mut self.output_pending,
2068                        &mut self.output_offset,
2069                        buf,
2070                    ));
2071                }
2072                Ok(read) => self.process_chunk(&scratch[..read]),
2073                Err(err) => return Err(err),
2074            }
2075        }
2076    }
2077}
2078
2079fn streaming_bat_in_range(line_num: usize, range: Option<(Option<usize>, Option<usize>)>) -> bool {
2080    let Some((start, end)) = range else {
2081        return true;
2082    };
2083    if start.is_some_and(|s| line_num < s) {
2084        return false;
2085    }
2086    end.is_none_or(|e| line_num <= e)
2087}
2088
2089fn streaming_make_visible(s: &str) -> String {
2090    let mut out = String::with_capacity(s.len());
2091    for ch in s.chars() {
2092        if ch == '\t' {
2093            out.push_str("\\t");
2094        } else if ch == '\r' {
2095            out.push_str("\\r");
2096        } else if ch.is_control() {
2097            let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\x{:02x}", ch as u32));
2098        } else {
2099            out.push(ch);
2100        }
2101    }
2102    out
2103}
2104
2105impl<R> BatStreamReader<R> {
2106    fn new(inner: R, stage: StreamingBatStage) -> Self {
2107        Self {
2108            inner,
2109            stage,
2110            input_pending: Vec::new(),
2111            output_pending: Vec::new(),
2112            output_offset: 0,
2113            finished: false,
2114            header_emitted: false,
2115            footer_emitted: false,
2116            line_num: 0,
2117        }
2118    }
2119
2120    fn emit_header(&mut self) {
2121        if !self.stage.show_header || self.header_emitted {
2122            return;
2123        }
2124        self.header_emitted = true;
2125        let separator = "\u{2500}";
2126        let rule_left: String = separator.repeat(7);
2127        let rule_right: String = separator.repeat(20);
2128        let top_corner = "\u{252C}";
2129        let mid_corner = "\u{253C}";
2130        self.output_pending
2131            .extend_from_slice(format!("{rule_left}{top_corner}{rule_right}\n").as_bytes());
2132        self.output_pending
2133            .extend_from_slice(format!("{rule_left}{mid_corner}{rule_right}\n").as_bytes());
2134    }
2135
2136    fn emit_footer(&mut self) {
2137        if !self.stage.show_header || self.footer_emitted {
2138            return;
2139        }
2140        self.footer_emitted = true;
2141        let separator = "\u{2500}";
2142        let rule_left: String = separator.repeat(7);
2143        let rule_right: String = separator.repeat(20);
2144        let bot_corner = "\u{2534}";
2145        self.output_pending
2146            .extend_from_slice(format!("{rule_left}{bot_corner}{rule_right}\n").as_bytes());
2147    }
2148}
2149
2150impl<R: Read> Read for BatStreamReader<R> {
2151    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2152        if buf.is_empty() {
2153            return Ok(0);
2154        }
2155        loop {
2156            let copied =
2157                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2158            if copied > 0 {
2159                return Ok(copied);
2160            }
2161            if self.finished {
2162                return Ok(0);
2163            }
2164            self.emit_header();
2165            let copied =
2166                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2167            if copied > 0 {
2168                return Ok(copied);
2169            }
2170            self.pump_next_bat_line()?;
2171        }
2172    }
2173}
2174
2175impl<R: Read> BatStreamReader<R> {
2176    fn pump_next_bat_line(&mut self) -> std::io::Result<()> {
2177        if let Some((line, _had_newline)) =
2178            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2179        {
2180            self.line_num += 1;
2181            if streaming_bat_in_range(self.line_num, self.stage.line_range) {
2182                self.emit_bat_line(&line);
2183            }
2184        } else {
2185            self.emit_footer();
2186            self.finished = true;
2187        }
2188        Ok(())
2189    }
2190
2191    fn emit_bat_line(&mut self, line: &str) {
2192        let display_line = if self.stage.show_all {
2193            streaming_make_visible(line)
2194        } else {
2195            line.to_string()
2196        };
2197        if self.stage.show_numbers {
2198            self.output_pending.extend_from_slice(
2199                format!("{:>5}   \u{2502} {display_line}\n", self.line_num).as_bytes(),
2200            );
2201        } else {
2202            self.output_pending
2203                .extend_from_slice(format!("{display_line}\n").as_bytes());
2204        }
2205    }
2206}
2207
2208pub(crate) fn streaming_simple_grep_match(line: &str, pattern: &str) -> bool {
2209    if let Some(rest) = pattern.strip_prefix('^') {
2210        if let Some(mid) = rest.strip_suffix('$') {
2211            line == mid
2212        } else {
2213            line.starts_with(rest)
2214        }
2215    } else if let Some(rest) = pattern.strip_suffix('$') {
2216        line.ends_with(rest)
2217    } else {
2218        line.contains(pattern)
2219    }
2220}
2221
2222impl<R> PasteStreamReader<R> {
2223    fn new(inner: R, stage: StreamingPasteStage) -> Self {
2224        Self {
2225            inner,
2226            stage,
2227            input_pending: Vec::new(),
2228            output_pending: Vec::new(),
2229            output_offset: 0,
2230            finalized: false,
2231            ended_with_newline: true,
2232            serial_first: true,
2233        }
2234    }
2235
2236    fn finalize_serial(&mut self) -> std::io::Result<()>
2237    where
2238        R: Read,
2239    {
2240        while let Some((line, _had_newline)) =
2241            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2242        {
2243            if !self.serial_first {
2244                self.output_pending
2245                    .extend_from_slice(self.stage.delimiter.as_bytes());
2246            }
2247            self.output_pending.extend_from_slice(line.as_bytes());
2248            self.serial_first = false;
2249        }
2250        self.output_pending.push(b'\n');
2251        Ok(())
2252    }
2253}
2254
2255impl<R: Read> Read for PasteStreamReader<R> {
2256    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2257        if buf.is_empty() {
2258            return Ok(0);
2259        }
2260        loop {
2261            let copied =
2262                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2263            if copied > 0 {
2264                return Ok(copied);
2265            }
2266            if self.finalized {
2267                return Ok(0);
2268            }
2269
2270            if self.stage.serial {
2271                self.finalize_serial()?;
2272                self.finalized = true;
2273                continue;
2274            }
2275
2276            let mut scratch = [0u8; 4096];
2277            let read = self.inner.read(&mut scratch)?;
2278            if read == 0 {
2279                if !self.ended_with_newline {
2280                    self.output_pending.push(b'\n');
2281                }
2282                self.finalized = true;
2283                continue;
2284            }
2285            self.ended_with_newline = scratch[read - 1] == b'\n';
2286            self.output_pending.extend_from_slice(&scratch[..read]);
2287        }
2288    }
2289}
2290
2291impl<R> ColumnStreamReader<R> {
2292    fn new(inner: R) -> Self {
2293        Self {
2294            inner,
2295            output_pending: Vec::new(),
2296            output_offset: 0,
2297            finalized: false,
2298            ended_with_newline: true,
2299        }
2300    }
2301}
2302
2303impl<R: Read> Read for ColumnStreamReader<R> {
2304    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2305        if buf.is_empty() {
2306            return Ok(0);
2307        }
2308        loop {
2309            let copied =
2310                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2311            if copied > 0 {
2312                return Ok(copied);
2313            }
2314            if self.finalized {
2315                return Ok(0);
2316            }
2317
2318            let mut scratch = [0u8; 4096];
2319            let read = self.inner.read(&mut scratch)?;
2320            if read == 0 {
2321                if !self.ended_with_newline {
2322                    self.output_pending.push(b'\n');
2323                }
2324                self.finalized = true;
2325                continue;
2326            }
2327            self.ended_with_newline = scratch[read - 1] == b'\n';
2328            self.output_pending.extend_from_slice(&scratch[..read]);
2329        }
2330    }
2331}
2332
2333pub(crate) fn take_pending_output(
2334    pending: &mut Vec<u8>,
2335    pending_offset: &mut usize,
2336    buf: &mut [u8],
2337) -> usize {
2338    if *pending_offset >= pending.len() {
2339        pending.clear();
2340        *pending_offset = 0;
2341        return 0;
2342    }
2343    let remaining = &pending[*pending_offset..];
2344    let to_copy = remaining.len().min(buf.len());
2345    buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
2346    *pending_offset += to_copy;
2347    if *pending_offset == pending.len() {
2348        pending.clear();
2349        *pending_offset = 0;
2350    }
2351    to_copy
2352}
2353
2354pub(crate) fn streaming_read_next_line(
2355    reader: &mut dyn Read,
2356    pending: &mut Vec<u8>,
2357) -> std::io::Result<Option<(String, bool)>> {
2358    loop {
2359        if let Some(pos) = pending.iter().position(|&b| b == b'\n') {
2360            let mut line = pending.drain(..=pos).collect::<Vec<u8>>();
2361            let _ = line.pop();
2362            return Ok(Some((String::from_utf8_lossy(&line).to_string(), true)));
2363        }
2364
2365        let mut buffer = [0u8; 4096];
2366        match reader.read(&mut buffer) {
2367            Ok(0) => {
2368                if pending.is_empty() {
2369                    return Ok(None);
2370                }
2371                let line = std::mem::take(pending);
2372                return Ok(Some((String::from_utf8_lossy(&line).to_string(), false)));
2373            }
2374            Ok(read) => pending.extend_from_slice(&buffer[..read]),
2375            Err(err) => return Err(err),
2376        }
2377    }
2378}
2379
2380struct RevStreamReader<R> {
2381    inner: R,
2382    input_pending: Vec<u8>,
2383    output_pending: Vec<u8>,
2384    output_offset: usize,
2385    finished: bool,
2386}
2387
2388impl<R> RevStreamReader<R> {
2389    fn new(inner: R) -> Self {
2390        Self {
2391            inner,
2392            input_pending: Vec::new(),
2393            output_pending: Vec::new(),
2394            output_offset: 0,
2395            finished: false,
2396        }
2397    }
2398}
2399
2400impl<R: Read> Read for RevStreamReader<R> {
2401    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2402        if buf.is_empty() {
2403            return Ok(0);
2404        }
2405        let copied = take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2406        if copied > 0 {
2407            return Ok(copied);
2408        }
2409        if self.finished {
2410            return Ok(0);
2411        }
2412
2413        if let Some((line, _had_newline)) =
2414            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2415        {
2416            let reversed: String = line.chars().rev().collect();
2417            self.output_pending.extend_from_slice(reversed.as_bytes());
2418            self.output_pending.push(b'\n');
2419            Ok(take_pending_output(
2420                &mut self.output_pending,
2421                &mut self.output_offset,
2422                buf,
2423            ))
2424        } else {
2425            self.finished = true;
2426            Ok(0)
2427        }
2428    }
2429}
2430
2431/// Result from an external command handler.
2432#[derive(Debug)]
2433pub struct ExternalCommandResult {
2434    /// Data written to stdout.
2435    pub stdout: Vec<u8>,
2436    /// Data written to stderr.
2437    pub stderr: Vec<u8>,
2438    /// Exit code (0 = success).
2439    pub status: i32,
2440}
2441
2442pub struct ExternalCommandStdin<'a> {
2443    reader: Box<dyn Read + 'a>,
2444}
2445
2446impl std::fmt::Debug for ExternalCommandStdin<'_> {
2447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2448        f.debug_struct("ExternalCommandStdin")
2449            .finish_non_exhaustive()
2450    }
2451}
2452
2453impl<'a> ExternalCommandStdin<'a> {
2454    #[must_use]
2455    pub fn from_bytes(data: &'a [u8]) -> Self {
2456        Self {
2457            reader: Box::new(Cursor::new(data)),
2458        }
2459    }
2460
2461    #[must_use]
2462    pub fn from_reader<R>(reader: R) -> Self
2463    where
2464        R: Read + 'a,
2465    {
2466        Self {
2467            reader: Box::new(reader),
2468        }
2469    }
2470
2471    pub fn read_chunk(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2472        self.reader.read(buf)
2473    }
2474}
2475
2476impl Read for ExternalCommandStdin<'_> {
2477    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2478        self.read_chunk(buf)
2479    }
2480}
2481
2482/// Callback type for external (host-provided) commands.
2483///
2484/// Called with `(command_name, argv, stdin)`. Returns `Some(result)` if
2485/// the command was handled, `None` to fall through to "command not found".
2486pub type ExternalCommandHandler = Box<
2487    dyn FnMut(&str, &[String], Option<ExternalCommandStdin<'_>>) -> Option<ExternalCommandResult>,
2488>;
2489
2490#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2491enum RuntimeCommandKind {
2492    Local,
2493    Break,
2494    Continue,
2495    Exit,
2496    Eval,
2497    Source,
2498    Declare,
2499    Let,
2500    Shopt,
2501    Alias,
2502    Unalias,
2503    BuiltinKeyword,
2504    Mapfile,
2505    Type,
2506    CommandKeyword,
2507    ExecKeyword,
2508    Hash,
2509    Times,
2510    Dirs,
2511    Pushd,
2512    Popd,
2513    Umask,
2514    Wait,
2515    Ulimit,
2516}
2517
2518#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2519enum UtilityCommandKind {
2520    Plain,
2521    FindWithExec,
2522    Xargs,
2523}
2524
2525#[derive(Clone, Debug)]
2526enum ResolvedCommand {
2527    Runtime(RuntimeCommandKind),
2528    ShellScript,
2529    /// A file with `#!/bin/bash` or similar shebang, executed directly by path.
2530    ShebangScript,
2531    Function(HirCommand),
2532    Builtin(wasmsh_builtins::BuiltinFn),
2533    Utility(UtilityCommandKind, wasmsh_utils::UtilFn),
2534    External,
2535}
2536
2537#[derive(Clone, Debug)]
2538struct ActiveRun {
2539    input: String,
2540    hir: HirProgram,
2541    complete_index: usize,
2542    and_or_index: usize,
2543}
2544
2545impl ActiveRun {
2546    fn new(input: String, hir: HirProgram) -> Self {
2547        Self {
2548            input,
2549            hir,
2550            complete_index: 0,
2551            and_or_index: 0,
2552        }
2553    }
2554
2555    fn is_done(&self) -> bool {
2556        self.complete_index >= self.hir.items.len()
2557    }
2558}
2559
2560#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2561enum ActiveRunStep {
2562    Pending,
2563    Done,
2564}
2565
2566#[derive(Clone, Debug, PartialEq, Eq)]
2567pub enum ExecutionPoll {
2568    Yield(Vec<WorkerEvent>),
2569    Done(Vec<WorkerEvent>),
2570}
2571
2572#[derive(Clone, Debug, PartialEq, Eq)]
2573enum VmSubsetFallbackReason {
2574    Disabled,
2575    Lowering(LoweringError),
2576    AssignmentShape,
2577    UnsupportedWord,
2578    ShellExpansion,
2579    AliasExpansion,
2580    NonBuiltinCommand,
2581    CommandEnvPrefixes,
2582    UnsupportedRedirection,
2583}
2584
2585struct RuntimeVmExecutor<'a> {
2586    fs: &'a mut BackendFs,
2587    builtins: &'a wasmsh_builtins::BuiltinRegistry,
2588    current_exec_io: &'a mut Option<ExecIo>,
2589    proc_subst_out_scopes: &'a mut Vec<Vec<PendingProcessSubstOut>>,
2590    exec: &'a mut ExecState,
2591}
2592
2593impl RuntimeVmExecutor<'_> {
2594    fn prepare_exec_io(
2595        &mut self,
2596        state: &mut ShellState,
2597        redirections: &[IrRedirection],
2598    ) -> Result<Option<ExecIo>, String> {
2599        let mut exec_io = self.current_exec_io.clone().unwrap_or_default();
2600        let mut handled_any = false;
2601
2602        for redirection in redirections {
2603            let fd = redirection.fd.unwrap_or(1);
2604            let append = matches!(redirection.op, RedirectionOp::Append);
2605            let target = wasmsh_expand::expand_word(&redirection.target, state);
2606            let path = resolve_path_from_cwd(&state.cwd, &target);
2607            if matches!(redirection.op, RedirectionOp::Output)
2608                && state.get_var("SHOPT_C").as_deref() == Some("1")
2609                && self.fs.stat(&path).is_ok()
2610            {
2611                return Err(format!(
2612                    "wasmsh: {target}: cannot overwrite existing file\n"
2613                ));
2614            }
2615            let sink = match self.fs.open_write_sink(&path, append) {
2616                Ok(sink) => sink,
2617                Err(err) => {
2618                    return Err(format!("wasmsh: {target}: {err}\n"));
2619                }
2620            };
2621            exec_io.fds_mut().open_output(
2622                fd,
2623                OutputTarget::File {
2624                    path,
2625                    append,
2626                    sink: Rc::new(RefCell::new(sink)),
2627                },
2628            );
2629            handled_any = true;
2630        }
2631
2632        Ok(handled_any.then_some(exec_io))
2633    }
2634
2635    fn with_exec_io_scope<T>(
2636        current_exec_io: &mut Option<ExecIo>,
2637        proc_subst_out_scopes: &mut Vec<Vec<PendingProcessSubstOut>>,
2638        exec: &mut ExecState,
2639        exec_io: Option<ExecIo>,
2640        f: impl FnOnce(&mut Option<ExecIo>, &mut Vec<Vec<PendingProcessSubstOut>>, &mut ExecState) -> T,
2641    ) -> T {
2642        if let Some(exec_io) = exec_io {
2643            let saved = current_exec_io.replace(exec_io);
2644            let result = f(current_exec_io, proc_subst_out_scopes, exec);
2645            let current = current_exec_io.take();
2646            *current_exec_io = match (saved, current) {
2647                (Some(mut saved), Some(mut current)) => {
2648                    let stdin = current.take_stdin();
2649                    saved.fds_mut().set_input(stdin);
2650                    Some(saved)
2651                }
2652                (saved, _) => saved,
2653            };
2654            result
2655        } else {
2656            f(current_exec_io, proc_subst_out_scopes, exec)
2657        }
2658    }
2659
2660    fn write_visible_stderr(&mut self, vm: &mut Vm, data: &[u8]) {
2661        let mut router = RuntimeOutputRouter {
2662            exec: self.exec,
2663            exec_io: self.current_exec_io.as_mut(),
2664            proc_subst_out_scopes: self.proc_subst_out_scopes,
2665            vm_stdout: &mut vm.stdout,
2666            vm_stderr: &mut vm.stderr,
2667            vm_output_bytes: &mut vm.output_bytes,
2668            vm_output_limit: vm.limits.output_byte_limit,
2669            vm_diagnostics: &mut vm.diagnostics,
2670        };
2671        router.write_stderr(data);
2672    }
2673
2674    fn take_pending_input_reader(
2675        &mut self,
2676        cmd_name: &str,
2677    ) -> Result<Option<Box<dyn Read>>, String> {
2678        let Some(exec_io) = self.current_exec_io.as_mut() else {
2679            return Ok(None);
2680        };
2681        match exec_io.take_stdin() {
2682            InputTarget::Inherit | InputTarget::Closed => Ok(None),
2683            InputTarget::Bytes(data) => Ok(Some(Box::new(Cursor::new(data)))),
2684            InputTarget::File {
2685                path,
2686                remove_after_read,
2687            } => {
2688                let handle = self
2689                    .fs
2690                    .open(&path, OpenOptions::read())
2691                    .map_err(|err| format!("wasmsh: {cmd_name}: {err}\n"))?;
2692                let reader = self
2693                    .fs
2694                    .stream_file(handle)
2695                    .map_err(|err| format!("wasmsh: {cmd_name}: {err}\n"));
2696                self.fs.close(handle);
2697                if remove_after_read {
2698                    let _ = self.fs.remove_file(&path);
2699                }
2700                reader.map(Some)
2701            }
2702            InputTarget::Pipe(pipe) => Ok(Some(Box::new(PipeReader::new(pipe)))),
2703        }
2704    }
2705
2706    fn take_builtin_stdin(
2707        &mut self,
2708        cmd_name: &str,
2709    ) -> Result<Option<wasmsh_builtins::BuiltinStdin<'static>>, String> {
2710        let reader = self.take_pending_input_reader(cmd_name)?;
2711        Ok(reader.map(wasmsh_builtins::BuiltinStdin::from_reader))
2712    }
2713
2714    /// Drain a pending nounset error from parameter expansion so the VM-subset
2715    /// path reports it the same way the fallback interpreter does.
2716    fn consume_nounset_error(&mut self, vm: &mut Vm) -> bool {
2717        let Some(var_name) = vm.state.take_nounset_error() else {
2718            return false;
2719        };
2720        let msg = format!("wasmsh: {var_name}: unbound variable\n");
2721        self.write_visible_stderr(vm, msg.as_bytes());
2722        vm.state.last_status = 1;
2723        true
2724    }
2725}
2726
2727impl VmExecutor for RuntimeVmExecutor<'_> {
2728    fn assign(&mut self, vm: &mut Vm, name: &str, value: Option<&Word>) {
2729        let value = value.map_or_else(String::new, |word| {
2730            wasmsh_expand::expand_word(word, &mut vm.state)
2731        });
2732        if self.consume_nounset_error(vm) {
2733            return;
2734        }
2735        let trimmed = value.trim();
2736        if trimmed.starts_with('(') && trimmed.ends_with(')') {
2737            let inner = &trimmed[1..trimmed.len() - 1];
2738            let elements = WorkerRuntime::parse_array_elements(inner);
2739            let name_key = smol_str::SmolStr::from(name);
2740
2741            if WorkerRuntime::is_assoc_array_assignment(inner, &elements) {
2742                vm.state.init_assoc_array(name_key.clone());
2743                for (key, value) in WorkerRuntime::parse_assoc_pairs(inner) {
2744                    vm.state.set_array_element(
2745                        name_key.clone(),
2746                        &key,
2747                        smol_str::SmolStr::from(value.as_str()),
2748                    );
2749                }
2750            } else {
2751                vm.state.init_indexed_array(name_key.clone());
2752                for (idx, element) in elements.iter().enumerate() {
2753                    vm.state
2754                        .set_array_element(name_key.clone(), &idx.to_string(), element.clone());
2755                }
2756            }
2757            vm.state.last_status = 0;
2758            return;
2759        }
2760
2761        let assigned = if vm.state.env.get(name).is_some_and(|var| var.integer) {
2762            wasmsh_expand::eval_arithmetic(trimmed, &mut vm.state).to_string()
2763        } else {
2764            value
2765        };
2766        vm.state.set_var(name.into(), assigned.into());
2767        vm.state.last_status = 0;
2768    }
2769
2770    fn execute_builtin(
2771        &mut self,
2772        vm: &mut Vm,
2773        name: &str,
2774        argv: &[Word],
2775        redirections: &[IrRedirection],
2776    ) -> i32 {
2777        let Some(builtin_fn) = self.builtins.get(name) else {
2778            vm.emit_diagnostic(
2779                wasmsh_vm::DiagLevel::Error,
2780                wasmsh_vm::DiagCategory::Builtin,
2781                format!("unknown builtin: {name}"),
2782            );
2783            vm.state.last_status = 127;
2784            return 127;
2785        };
2786        let expanded: Vec<String> = argv
2787            .iter()
2788            .map(|word| wasmsh_expand::expand_word(word, &mut vm.state))
2789            .collect();
2790        if self.consume_nounset_error(vm) {
2791            return 1;
2792        }
2793        let argv_refs: Vec<&str> = expanded.iter().map(String::as_str).collect();
2794        let stdin = match self.take_builtin_stdin(name) {
2795            Ok(stdin) => stdin,
2796            Err(message) => {
2797                self.write_visible_stderr(vm, message.as_bytes());
2798                vm.state.last_status = 1;
2799                return 1;
2800            }
2801        };
2802        let exec_io = match self.prepare_exec_io(&mut vm.state, redirections) {
2803            Ok(exec_io) => exec_io,
2804            Err(message) => {
2805                self.write_visible_stderr(vm, message.as_bytes());
2806                vm.state.last_status = 1;
2807                return 1;
2808            }
2809        };
2810
2811        let fs = &*self.fs;
2812        let status = Self::with_exec_io_scope(
2813            &mut *self.current_exec_io,
2814            &mut *self.proc_subst_out_scopes,
2815            &mut *self.exec,
2816            exec_io,
2817            |current_exec_io, proc_subst_out_scopes, exec| {
2818                let mut router = RuntimeOutputRouter {
2819                    exec,
2820                    exec_io: current_exec_io.as_mut(),
2821                    proc_subst_out_scopes,
2822                    vm_stdout: &mut vm.stdout,
2823                    vm_stderr: &mut vm.stderr,
2824                    vm_output_bytes: &mut vm.output_bytes,
2825                    vm_output_limit: vm.limits.output_byte_limit,
2826                    vm_diagnostics: &mut vm.diagnostics,
2827                };
2828                let mut sink = RuntimeBuiltinSink {
2829                    router: &mut router,
2830                };
2831                {
2832                    let mut ctx = wasmsh_builtins::BuiltinContext {
2833                        state: &mut vm.state,
2834                        output: &mut sink,
2835                        fs: Some(fs),
2836                        stdin,
2837                    };
2838                    builtin_fn(&mut ctx, &argv_refs)
2839                }
2840            },
2841        );
2842        if let Some(last) = expanded.last() {
2843            vm.state.set_last_argument(last.as_str());
2844        }
2845        vm.state.last_status = status;
2846        status
2847    }
2848}
2849
2850/// The worker-side runtime that processes host commands.
2851#[allow(missing_debug_implementations)]
2852pub struct WorkerRuntime {
2853    config: BrowserConfig,
2854    vm: Vm,
2855    fs: BackendFs,
2856    utils: UtilRegistry,
2857    builtins: wasmsh_builtins::BuiltinRegistry,
2858    initialized: bool,
2859    /// Command-scoped stdin/stdout/stderr routing for the currently executing command.
2860    current_exec_io: Option<ExecIo>,
2861    /// Deferred `>(...)` sinks scoped to the currently executing command.
2862    proc_subst_out_scopes: Vec<Vec<PendingProcessSubstOut>>,
2863    /// Deferred `<(...)` cleanup and stderr flush scoped to the current command.
2864    proc_subst_in_scopes: Vec<Vec<PendingProcessSubstIn>>,
2865    /// Registered shell functions (name → HIR body).
2866    functions: IndexMap<String, HirCommand>,
2867    /// Transient execution state (loop control, exit, locals).
2868    exec: ExecState,
2869    /// Shell aliases (name → replacement text).
2870    aliases: IndexMap<String, String>,
2871    /// Optional handler for external commands (e.g. python3 in Pyodide).
2872    external_handler: Option<ExternalCommandHandler>,
2873    /// Optional network backend for curl/wget utilities.
2874    network: Option<Box<dyn wasmsh_utils::net_types::NetworkBackend>>,
2875    /// Active top-level execution, if a run has been started and not yet completed.
2876    active_run: Option<ActiveRun>,
2877    /// Signals queued for the next progressive poll.
2878    pending_signals: VecDeque<&'static RuntimeSignalSpec>,
2879}
2880
2881/// Action to take for a character during array element parsing.
2882enum ArrayCharAction {
2883    Append(char),
2884    Skip,
2885    SplitField,
2886}
2887
2888enum StreamingPipelineStage {
2889    Literal(Vec<u8>),
2890    File(String),
2891    Yes { line: Vec<u8> },
2892    BufferedCommand(BufferedPipelineCommand),
2893    Cat,
2894    Head(StreamingHeadMode),
2895    Tail(StreamingTailMode),
2896    Bat(StreamingBatStage),
2897    Sed(StreamingSedStage),
2898    Tee(StreamingTeeStage),
2899    Paste(StreamingPasteStage),
2900    Column(StreamingColumnStage),
2901    Grep(StreamingGrepStage),
2902    Uniq(StreamingUniqFlags),
2903    Rev,
2904    Cut(StreamingCutStage),
2905    Tr(StreamingTrStage),
2906    Wc(StreamingWcFlags),
2907}
2908
2909struct StreamingStageCtx<'a> {
2910    stages: &'a [StreamingPipelineStage],
2911    stage_pipe_stderr: &'a [bool],
2912    stage_statuses: &'a [Rc<RefCell<i32>>],
2913    stage_stderr: &'a [Rc<RefCell<Vec<u8>>>],
2914    output_pipes: &'a [Rc<RefCell<PipeBuffer>>],
2915}
2916
2917/// Quoting state for parsing array elements.
2918#[derive(Default)]
2919struct ArrayParseState {
2920    in_single_quote: bool,
2921    in_double_quote: bool,
2922    escape_next: bool,
2923}
2924
2925impl ArrayParseState {
2926    fn process_char(&mut self, ch: char) -> ArrayCharAction {
2927        if self.escape_next {
2928            self.escape_next = false;
2929            return ArrayCharAction::Append(ch);
2930        }
2931        if ch == '\\' && !self.in_single_quote {
2932            self.escape_next = true;
2933            return ArrayCharAction::Skip;
2934        }
2935        if ch == '\'' && !self.in_double_quote {
2936            self.in_single_quote = !self.in_single_quote;
2937            return ArrayCharAction::Skip;
2938        }
2939        if ch == '"' && !self.in_single_quote {
2940            self.in_double_quote = !self.in_double_quote;
2941            return ArrayCharAction::Skip;
2942        }
2943        if ch.is_ascii_whitespace() && !self.in_single_quote && !self.in_double_quote {
2944            return ArrayCharAction::SplitField;
2945        }
2946        ArrayCharAction::Append(ch)
2947    }
2948}
2949
2950/// Parsed flags for `declare`/`typeset`.
2951#[allow(clippy::struct_excessive_bools)]
2952struct DeclareFlags {
2953    is_assoc: bool,
2954    is_indexed: bool,
2955    is_integer: bool,
2956    is_export: bool,
2957    is_readonly: bool,
2958    is_lower: bool,
2959    is_upper: bool,
2960    is_print: bool,
2961    is_nameref: bool,
2962    is_functions: bool,
2963    is_function_names: bool,
2964    is_trace: bool,
2965}
2966
2967#[derive(Clone, Copy, Debug)]
2968enum CommandLookupKind {
2969    Alias,
2970    Function,
2971    Builtin,
2972    File,
2973}
2974
2975#[derive(Clone, Debug)]
2976struct CommandLookup {
2977    kind: CommandLookupKind,
2978    name: String,
2979    detail: String,
2980}
2981
2982fn format_command_verbose(lookup: &CommandLookup) -> String {
2983    match lookup.kind {
2984        CommandLookupKind::Alias => format!("alias {}='{}'", lookup.name, lookup.detail),
2985        CommandLookupKind::Function | CommandLookupKind::Builtin => lookup.name.clone(),
2986        CommandLookupKind::File => lookup.detail.clone(),
2987    }
2988}
2989
2990fn format_type_lookup(lookup: &CommandLookup, type_only: bool, path_only: bool) -> String {
2991    if type_only {
2992        return match lookup.kind {
2993            CommandLookupKind::Alias => "alias".to_string(),
2994            CommandLookupKind::Function => "function".to_string(),
2995            CommandLookupKind::Builtin => "builtin".to_string(),
2996            CommandLookupKind::File => "file".to_string(),
2997        };
2998    }
2999    if path_only {
3000        return lookup.detail.clone();
3001    }
3002    match lookup.kind {
3003        CommandLookupKind::Alias => {
3004            format!("{} is aliased to `{}`", lookup.name, lookup.detail)
3005        }
3006        CommandLookupKind::Function => format!("{} is a function", lookup.name),
3007        CommandLookupKind::Builtin => format!("{} is a shell builtin", lookup.name),
3008        CommandLookupKind::File => format!("{} is {}", lookup.name, lookup.detail),
3009    }
3010}
3011
3012#[derive(Clone, Debug)]
3013struct MapfileOptions {
3014    strip_delimiter: bool,
3015    delimiter: u8,
3016    count: Option<usize>,
3017    origin: usize,
3018    skip: usize,
3019    fd: u32,
3020    array_name: String,
3021}
3022
3023/// Parse declare/typeset flags from argv, returning (flags, `name_indices`).
3024fn parse_declare_flags(argv: &[String]) -> (DeclareFlags, Vec<usize>) {
3025    let mut flags = DeclareFlags {
3026        is_assoc: false,
3027        is_indexed: false,
3028        is_integer: false,
3029        is_export: false,
3030        is_readonly: false,
3031        is_lower: false,
3032        is_upper: false,
3033        is_print: false,
3034        is_nameref: false,
3035        is_functions: false,
3036        is_function_names: false,
3037        is_trace: false,
3038    };
3039    let mut names = Vec::new();
3040
3041    for (i, arg) in argv[1..].iter().enumerate() {
3042        if arg.starts_with('-') && arg.len() > 1 {
3043            for ch in arg[1..].chars() {
3044                match ch {
3045                    'A' => flags.is_assoc = true,
3046                    'a' => flags.is_indexed = true,
3047                    'i' => flags.is_integer = true,
3048                    'x' => flags.is_export = true,
3049                    'r' => flags.is_readonly = true,
3050                    'l' => flags.is_lower = true,
3051                    'u' => flags.is_upper = true,
3052                    'p' => flags.is_print = true,
3053                    'n' => flags.is_nameref = true,
3054                    'f' => flags.is_functions = true,
3055                    'F' => flags.is_function_names = true,
3056                    't' => flags.is_trace = true,
3057                    _ => {}
3058                }
3059            }
3060        } else {
3061            names.push(i + 1);
3062        }
3063    }
3064    (flags, names)
3065}
3066
3067impl WorkerRuntime {
3068    #[must_use]
3069    pub fn new() -> Self {
3070        Self {
3071            config: BrowserConfig::default(),
3072            vm: Vm::with_limits(ShellState::new(), ExecutionLimits::default()),
3073            fs: BackendFs::new(),
3074            utils: UtilRegistry::new(),
3075            builtins: wasmsh_builtins::BuiltinRegistry::new(),
3076            initialized: false,
3077            current_exec_io: None,
3078            proc_subst_out_scopes: Vec::new(),
3079            proc_subst_in_scopes: Vec::new(),
3080            functions: IndexMap::new(),
3081            exec: ExecState::new(),
3082            aliases: IndexMap::new(),
3083            external_handler: None,
3084            network: None,
3085            active_run: None,
3086            pending_signals: VecDeque::new(),
3087        }
3088    }
3089
3090    /// Register a handler for external commands (e.g. `python3` in Pyodide).
3091    pub fn set_external_handler(&mut self, handler: ExternalCommandHandler) {
3092        self.external_handler = Some(handler);
3093    }
3094
3095    /// Register a network backend for `curl`/`wget` utilities.
3096    pub fn set_network_backend(
3097        &mut self,
3098        backend: Box<dyn wasmsh_utils::net_types::NetworkBackend>,
3099    ) {
3100        self.network = Some(backend);
3101    }
3102
3103    /// Process a host command and return a list of events to send back.
3104    pub fn handle_command(&mut self, cmd: HostCommand) -> Vec<WorkerEvent> {
3105        match cmd {
3106            HostCommand::Init {
3107                step_budget,
3108                allowed_hosts,
3109            } => self.handle_init_command(step_budget, allowed_hosts),
3110            HostCommand::Run { input } => self.handle_run_command(input, true),
3111            HostCommand::StartRun { input } => self.handle_run_command(input, false),
3112            HostCommand::PollRun => self.handle_poll_run_command(),
3113            HostCommand::Signal { signal } => self.handle_signal_command(&signal),
3114            HostCommand::Cancel => {
3115                self.cancel_active_execution();
3116                vec![WorkerEvent::Diagnostic(
3117                    DiagnosticLevel::Info,
3118                    "cancel received".into(),
3119                )]
3120            }
3121            HostCommand::ReadFile { path } => self.handle_read_file_command(&path),
3122            HostCommand::WriteFile { path, data } => self.handle_write_file_command(path, &data),
3123            HostCommand::ListDir { path } => self.handle_list_dir_command(&path),
3124            HostCommand::Mount { .. } => {
3125                vec![WorkerEvent::Diagnostic(
3126                    DiagnosticLevel::Warning,
3127                    "mount not yet implemented".into(),
3128                )]
3129            }
3130            _ => vec![WorkerEvent::Diagnostic(
3131                DiagnosticLevel::Warning,
3132                "unknown command".into(),
3133            )],
3134        }
3135    }
3136
3137    fn handle_init_command(
3138        &mut self,
3139        step_budget: u64,
3140        allowed_hosts: Vec<String>,
3141    ) -> Vec<WorkerEvent> {
3142        self.config.step_budget = step_budget;
3143        self.config.allowed_hosts = allowed_hosts;
3144        self.vm = Vm::with_limits(
3145            ShellState::new(),
3146            ExecutionLimits {
3147                step_limit: step_budget,
3148                output_byte_limit: self.config.output_byte_limit,
3149                pipe_byte_limit: self.config.pipe_byte_limit,
3150                recursion_limit: self.config.recursion_limit,
3151            },
3152        );
3153        self.fs = BackendFs::new();
3154        self.current_exec_io = None;
3155        self.proc_subst_out_scopes.clear();
3156        self.proc_subst_in_scopes.clear();
3157        self.functions = IndexMap::new();
3158        self.exec.reset();
3159        self.aliases = IndexMap::new();
3160        self.active_run = None;
3161        self.pending_signals.clear();
3162        self.initialized = true;
3163        // Set default shopt options (bash defaults)
3164        self.vm.state.set_var("SHOPT_extglob".into(), "1".into());
3165        self.vm
3166            .state
3167            .set_var("SHOPT_expand_aliases".into(), "1".into());
3168        self.vm.state.set_var("SHOPT_sourcepath".into(), "1".into());
3169        vec![WorkerEvent::Version(PROTOCOL_VERSION.to_string())]
3170    }
3171
3172    fn handle_run_command(&mut self, input: String, run_to_completion: bool) -> Vec<WorkerEvent> {
3173        if !self.initialized {
3174            return vec![WorkerEvent::Diagnostic(
3175                DiagnosticLevel::Error,
3176                "runtime not initialized".into(),
3177            )];
3178        }
3179        match self.start_execution(input) {
3180            Ok(()) => {
3181                if run_to_completion {
3182                    self.poll_active_run_to_completion()
3183                } else {
3184                    vec![WorkerEvent::Yielded]
3185                }
3186            }
3187            Err(events) => events,
3188        }
3189    }
3190
3191    fn handle_poll_run_command(&mut self) -> Vec<WorkerEvent> {
3192        match self.poll_active_run() {
3193            Some(ExecutionPoll::Yield(mut events)) => {
3194                events.push(WorkerEvent::Yielded);
3195                events
3196            }
3197            Some(ExecutionPoll::Done(events)) => events,
3198            None => vec![WorkerEvent::Diagnostic(
3199                DiagnosticLevel::Error,
3200                "no active run".into(),
3201            )],
3202        }
3203    }
3204
3205    fn handle_read_file_command(&mut self, path: &str) -> Vec<WorkerEvent> {
3206        use wasmsh_fs::OpenOptions;
3207        let handle = match self.fs.open(path, OpenOptions::read()) {
3208            Ok(h) => h,
3209            Err(e) => {
3210                return vec![WorkerEvent::Diagnostic(
3211                    DiagnosticLevel::Error,
3212                    format!("read error: {e}"),
3213                )];
3214            }
3215        };
3216        let result = self.fs.read_file(handle);
3217        self.fs.close(handle);
3218        match result {
3219            Ok(data) => vec![WorkerEvent::Stdout(data)],
3220            Err(e) => vec![WorkerEvent::Diagnostic(
3221                DiagnosticLevel::Error,
3222                format!("read error: {path}: {e}"),
3223            )],
3224        }
3225    }
3226
3227    fn handle_write_file_command(&mut self, path: String, data: &[u8]) -> Vec<WorkerEvent> {
3228        use wasmsh_fs::OpenOptions;
3229        match self.fs.open(&path, OpenOptions::write()) {
3230            Ok(h) => {
3231                if let Err(e) = self.fs.write_file(h, data) {
3232                    self.write_stderr(format!("wasmsh: write error: {e}\n").as_bytes());
3233                }
3234                self.fs.close(h);
3235                vec![WorkerEvent::FsChanged(path)]
3236            }
3237            Err(e) => vec![WorkerEvent::Diagnostic(
3238                DiagnosticLevel::Error,
3239                format!("write error: {e}"),
3240            )],
3241        }
3242    }
3243
3244    fn handle_list_dir_command(&mut self, path: &str) -> Vec<WorkerEvent> {
3245        match self.fs.read_dir(path) {
3246            Ok(entries) => {
3247                let names: Vec<u8> = entries
3248                    .iter()
3249                    .map(|e| e.name.as_str())
3250                    .collect::<Vec<_>>()
3251                    .join("\n")
3252                    .into_bytes();
3253                vec![WorkerEvent::Stdout(names)]
3254            }
3255            Err(e) => vec![WorkerEvent::Diagnostic(
3256                DiagnosticLevel::Error,
3257                format!("readdir error: {e}"),
3258            )],
3259        }
3260    }
3261
3262    pub fn start_execution(&mut self, input: String) -> Result<(), Vec<WorkerEvent>> {
3263        if !self.initialized {
3264            return Err(vec![WorkerEvent::Diagnostic(
3265                DiagnosticLevel::Error,
3266                "runtime not initialized".into(),
3267            )]);
3268        }
3269        if self.active_run.is_some() {
3270            return Err(vec![WorkerEvent::Diagnostic(
3271                DiagnosticLevel::Error,
3272                "execution already active".into(),
3273            )]);
3274        }
3275
3276        let hir = match wasmsh_parse::parse(&input) {
3277            Ok(ast) => wasmsh_hir::lower(&ast),
3278            Err(e) => {
3279                self.vm.state.last_status = 2;
3280                return Err(vec![
3281                    WorkerEvent::Stderr(format!("wasmsh: parse error: {e}\n").into_bytes()),
3282                    WorkerEvent::Exit(2),
3283                ]);
3284            }
3285        };
3286
3287        self.exec.reset();
3288        self.current_exec_io = None;
3289        self.proc_subst_out_scopes.clear();
3290        self.proc_subst_in_scopes.clear();
3291        self.vm.steps = 0;
3292        self.vm.budget.steps = 0;
3293        self.vm.budget.visible_output_bytes = self.vm.output_bytes;
3294        self.vm.budget.pipe_bytes = 0;
3295        self.vm.budget.recursion_depth = 0;
3296        self.vm.budget.clear_stop_reason();
3297        self.vm.cancellation_token().reset();
3298        self.pending_signals.clear();
3299        self.active_run = Some(ActiveRun::new(input, hir));
3300        Ok(())
3301    }
3302
3303    /// Minimum per-poll step limit so that small batch sizes (e.g. `step_budget=1`
3304    /// for progressive yield-per-command) still allow enough internal steps for
3305    /// pipelines and compound commands to complete.
3306    const MIN_POLL_STEPS: u64 = 100;
3307
3308    pub fn poll_active_run(&mut self) -> Option<ExecutionPoll> {
3309        let mut run = self.active_run.take()?;
3310        let previous_step_limit = self.vm.limits.step_limit;
3311        self.vm.steps = 0;
3312        self.vm.budget.steps = 0;
3313        // Keep the VM step_limit active so that loops (while/for) can enforce
3314        // the budget via `check_resource_limits()` on each iteration.  The
3315        // outer `remaining` counter governs how many top-level commands we
3316        // execute per poll; the VM limit catches runaway inner loops.
3317        self.vm.limits.step_limit = if self.config.step_budget == 0 {
3318            0
3319        } else {
3320            self.config.step_budget.max(Self::MIN_POLL_STEPS)
3321        };
3322
3323        let mut remaining = if self.config.step_budget == 0 {
3324            usize::MAX
3325        } else {
3326            self.config.step_budget as usize
3327        };
3328        let pending_signal_events = self.drain_pending_signal_events();
3329        let mut finished = run.is_done();
3330
3331        while !finished && remaining > 0 {
3332            // Check cancellation without advancing the step counter — the
3333            // step counter is advanced inside command/loop dispatch.
3334            if self.vm.cancellation_token().is_cancelled() {
3335                self.vm.budget.note_cancelled();
3336                self.exec.resource_exhausted = true;
3337            }
3338            if self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
3339                finished = true;
3340                break;
3341            }
3342
3343            let step_outcome = self.poll_active_run_step(&mut run);
3344            remaining -= 1;
3345            finished = matches!(step_outcome, ActiveRunStep::Done);
3346        }
3347
3348        self.vm.limits.step_limit = previous_step_limit;
3349
3350        if finished || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
3351            self.ensure_stop_reason();
3352            let mut events = pending_signal_events;
3353            self.run_exit_trap_if_needed(&mut events);
3354            self.drain_io_events(&mut events);
3355            self.drain_diagnostic_events(&mut events);
3356            let exit_status = self.current_run_exit_status();
3357            events.push(WorkerEvent::Exit(exit_status));
3358            self.active_run = None;
3359            Some(ExecutionPoll::Done(events))
3360        } else {
3361            let mut events = pending_signal_events;
3362            events.extend(self.drain_partial_run_events());
3363            self.active_run = Some(run);
3364            Some(ExecutionPoll::Yield(events))
3365        }
3366    }
3367
3368    pub fn cancel_active_execution(&mut self) {
3369        self.vm.cancellation_token().cancel();
3370    }
3371
3372    fn handle_signal_command(&mut self, signal: &str) -> Vec<WorkerEvent> {
3373        if !self.initialized {
3374            return vec![WorkerEvent::Diagnostic(
3375                DiagnosticLevel::Error,
3376                "runtime not initialized".into(),
3377            )];
3378        }
3379
3380        let Some(spec) = find_runtime_signal_spec(signal) else {
3381            return vec![WorkerEvent::Diagnostic(
3382                DiagnosticLevel::Error,
3383                format!("unsupported signal: {signal}"),
3384            )];
3385        };
3386
3387        if self.active_run.is_some() {
3388            self.pending_signals.push_back(spec);
3389            if self.signal_trap_handler(spec).is_some()
3390                || self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1")
3391            {
3392                return Vec::new();
3393            }
3394            return match spec.default_action {
3395                SignalDefaultAction::Terminate => vec![WorkerEvent::Diagnostic(
3396                    DiagnosticLevel::Info,
3397                    format!("signal {} received", spec.name),
3398                )],
3399                SignalDefaultAction::Ignore => Vec::new(),
3400                SignalDefaultAction::StopLike => vec![WorkerEvent::Diagnostic(
3401                    DiagnosticLevel::Warning,
3402                    format!(
3403                        "signal {} requires job-control stop semantics and is not modeled yet",
3404                        spec.name
3405                    ),
3406                )],
3407                SignalDefaultAction::ContinueLike => vec![WorkerEvent::Diagnostic(
3408                    DiagnosticLevel::Info,
3409                    format!(
3410                        "signal {} has no effect without a stopped job in the current sandbox model",
3411                        spec.name
3412                    ),
3413                )],
3414            };
3415        }
3416
3417        if let Some(handler) = self.signal_trap_handler(spec) {
3418            let mut events = self.run_signal_trap(spec, &handler);
3419            self.drain_diagnostic_events(&mut events);
3420            if self.exec.exit_requested.is_some() {
3421                events.extend(self.finish_idle_signal_exit());
3422            }
3423            return events;
3424        }
3425
3426        if self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1") {
3427            return Vec::new();
3428        }
3429
3430        match spec.default_action {
3431            SignalDefaultAction::Terminate => {
3432                self.exec.exit_requested = Some(128 + spec.number);
3433                if self.active_run.is_some() {
3434                    vec![WorkerEvent::Diagnostic(
3435                        DiagnosticLevel::Info,
3436                        format!("signal {} received", spec.name),
3437                    )]
3438                } else {
3439                    self.finish_idle_signal_exit()
3440                }
3441            }
3442            SignalDefaultAction::Ignore => Vec::new(),
3443            SignalDefaultAction::StopLike => vec![WorkerEvent::Diagnostic(
3444                DiagnosticLevel::Warning,
3445                format!(
3446                    "signal {} requires job-control stop semantics and is not modeled yet",
3447                    spec.name
3448                ),
3449            )],
3450            SignalDefaultAction::ContinueLike => vec![WorkerEvent::Diagnostic(
3451                DiagnosticLevel::Info,
3452                format!(
3453                    "signal {} has no effect without a stopped job in the current sandbox model",
3454                    spec.name
3455                ),
3456            )],
3457        }
3458    }
3459
3460    fn drain_pending_signal_events(&mut self) -> Vec<WorkerEvent> {
3461        let mut events = Vec::new();
3462        while let Some(spec) = self.pending_signals.pop_front() {
3463            if let Some(handler) = self.signal_trap_handler(spec) {
3464                events.extend(self.run_signal_trap(spec, &handler));
3465                self.drain_diagnostic_events(&mut events);
3466            } else if self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1") {
3467                continue;
3468            } else {
3469                match spec.default_action {
3470                    SignalDefaultAction::Terminate => {
3471                        self.exec.exit_requested = Some(128 + spec.number);
3472                    }
3473                    SignalDefaultAction::Ignore => {}
3474                    SignalDefaultAction::StopLike => events.push(WorkerEvent::Diagnostic(
3475                        DiagnosticLevel::Warning,
3476                        format!(
3477                            "signal {} requires job-control stop semantics and is not modeled yet",
3478                            spec.name
3479                        ),
3480                    )),
3481                    SignalDefaultAction::ContinueLike => events.push(WorkerEvent::Diagnostic(
3482                        DiagnosticLevel::Info,
3483                        format!(
3484                            "signal {} has no effect without a stopped job in the current sandbox model",
3485                            spec.name
3486                        ),
3487                    )),
3488                }
3489            }
3490
3491            if self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
3492                break;
3493            }
3494        }
3495        events
3496    }
3497
3498    fn finish_idle_signal_exit(&mut self) -> Vec<WorkerEvent> {
3499        let mut events = Vec::new();
3500        self.run_exit_trap_if_needed(&mut events);
3501        self.drain_io_events(&mut events);
3502        self.drain_diagnostic_events(&mut events);
3503        let exit_status = self.current_run_exit_status();
3504        events.push(WorkerEvent::Exit(exit_status));
3505        self.exec.reset();
3506        events
3507    }
3508
3509    fn poll_active_run_to_completion(&mut self) -> Vec<WorkerEvent> {
3510        let mut events = Vec::new();
3511        while let Some(poll) = self.poll_active_run() {
3512            match poll {
3513                ExecutionPoll::Yield(mut batch) => {
3514                    events.append(&mut batch);
3515                }
3516                ExecutionPoll::Done(mut batch) => {
3517                    events.append(&mut batch);
3518                    break;
3519                }
3520            }
3521        }
3522        events
3523    }
3524
3525    fn poll_active_run_step(&mut self, run: &mut ActiveRun) -> ActiveRunStep {
3526        if run.is_done() || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
3527            return ActiveRunStep::Done;
3528        }
3529
3530        let cc = &run.hir.items[run.complete_index];
3531        if run.and_or_index == 0 {
3532            self.vm.state.lineno = Self::line_number_for_offset(&run.input, cc.span.start as usize);
3533            self.maybe_write_verbose_input(&run.input, cc);
3534        }
3535        if self.is_set_option_enabled('n') {
3536            run.complete_index += 1;
3537            run.and_or_index = 0;
3538            return if run.is_done()
3539                || self.exec.exit_requested.is_some()
3540                || self.exec.resource_exhausted
3541            {
3542                ActiveRunStep::Done
3543            } else {
3544                ActiveRunStep::Pending
3545            };
3546        }
3547        let and_or = &cc.list[run.and_or_index];
3548        self.execute_and_or(and_or);
3549        self.handle_post_and_or(and_or);
3550
3551        run.and_or_index += 1;
3552        if run.and_or_index >= cc.list.len() {
3553            run.complete_index += 1;
3554            run.and_or_index = 0;
3555        }
3556
3557        if run.is_done() || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
3558            ActiveRunStep::Done
3559        } else {
3560            ActiveRunStep::Pending
3561        }
3562    }
3563
3564    fn drain_partial_run_events(&mut self) -> Vec<WorkerEvent> {
3565        let mut events = Vec::new();
3566        self.drain_io_events(&mut events);
3567        self.drain_diagnostic_events(&mut events);
3568        events
3569    }
3570
3571    fn current_run_exit_status(&self) -> i32 {
3572        if self.exec.resource_exhausted {
3573            match self.exec.stop_reason.as_ref() {
3574                Some(StopReason::Cancelled) => 130,
3575                _ => 128,
3576            }
3577        } else {
3578            self.exec
3579                .exit_requested
3580                .unwrap_or(self.vm.state.last_status)
3581        }
3582    }
3583
3584    fn mark_stop_reason(&mut self, reason: StopReason) {
3585        self.exec.resource_exhausted = true;
3586        self.exec.stop_reason = Some(reason);
3587    }
3588
3589    fn mark_budget_exhaustion(&mut self, reason: ExhaustionReason) {
3590        self.mark_stop_reason(StopReason::Exhausted(reason));
3591    }
3592
3593    fn ensure_stop_reason(&mut self) {
3594        if !self.exec.resource_exhausted || self.exec.stop_reason.is_some() {
3595            return;
3596        }
3597        if self.vm.cancellation_token().is_cancelled() {
3598            self.mark_stop_reason(StopReason::Cancelled);
3599            return;
3600        }
3601        if let Some(reason) = self.vm.stop_reason().cloned() {
3602            self.mark_stop_reason(reason);
3603            return;
3604        }
3605        let limit = self.vm.limits.output_byte_limit;
3606        if limit > 0 && self.vm.output_bytes > limit {
3607            self.mark_budget_exhaustion(ExhaustionReason {
3608                category: BudgetCategory::VisibleOutputBytes,
3609                used: self.vm.output_bytes,
3610                limit,
3611            });
3612        }
3613    }
3614
3615    fn sync_pipe_budget(&mut self, used: u64) {
3616        if self.exec.resource_exhausted {
3617            return;
3618        }
3619        let limit = self.vm.limits.pipe_byte_limit;
3620        if let Err(reason) = self.vm.budget.set_pipe_bytes(used, limit) {
3621            self.mark_budget_exhaustion(reason.clone());
3622            self.vm.emit_diagnostic(
3623                wasmsh_vm::DiagLevel::Error,
3624                wasmsh_vm::DiagCategory::Budget,
3625                reason.diagnostic_message(),
3626            );
3627        }
3628    }
3629
3630    pub fn set_output_byte_limit(&mut self, limit: u64) {
3631        self.config.output_byte_limit = limit;
3632        self.vm.limits.output_byte_limit = limit;
3633    }
3634
3635    pub fn set_pipe_byte_limit(&mut self, limit: u64) {
3636        self.config.pipe_byte_limit = limit;
3637        self.vm.limits.pipe_byte_limit = limit;
3638    }
3639
3640    pub fn set_recursion_limit(&mut self, limit: u32) {
3641        self.config.recursion_limit = limit;
3642        self.vm.limits.recursion_limit = limit;
3643    }
3644
3645    pub fn set_vm_subset_enabled(&mut self, enabled: bool) {
3646        self.config.vm_subset_enabled = enabled;
3647    }
3648
3649    fn execute_and_or(&mut self, and_or: &HirAndOr) {
3650        if let Ok(program) = self.lower_vm_subset_and_or(and_or) {
3651            self.run_debug_trap_if_needed();
3652            self.execute_ir_program(&program);
3653            return;
3654        }
3655        self.execute_pipeline_chain(and_or);
3656    }
3657
3658    fn execute_ir_program(&mut self, program: &IrProgram) {
3659        let mut executor = RuntimeVmExecutor {
3660            fs: &mut self.fs,
3661            builtins: &self.builtins,
3662            current_exec_io: &mut self.current_exec_io,
3663            proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
3664            exec: &mut self.exec,
3665        };
3666        let _ = self.vm.run_with_executor(program, &mut executor);
3667    }
3668
3669    fn lower_vm_subset_and_or(
3670        &self,
3671        and_or: &HirAndOr,
3672    ) -> Result<IrProgram, VmSubsetFallbackReason> {
3673        if !self.config.vm_subset_enabled {
3674            return Err(VmSubsetFallbackReason::Disabled);
3675        }
3676
3677        self.validate_vm_subset_and_or(and_or)?;
3678        lower_supported_and_or(and_or).map_err(VmSubsetFallbackReason::Lowering)
3679    }
3680
3681    fn validate_vm_subset_and_or(&self, and_or: &HirAndOr) -> Result<(), VmSubsetFallbackReason> {
3682        self.validate_vm_subset_pipeline(&and_or.first)?;
3683        for (_, pipeline) in &and_or.rest {
3684            self.validate_vm_subset_pipeline(pipeline)?;
3685        }
3686        Ok(())
3687    }
3688
3689    fn validate_vm_subset_pipeline(
3690        &self,
3691        pipeline: &HirPipeline,
3692    ) -> Result<(), VmSubsetFallbackReason> {
3693        if pipeline.timed || pipeline.time_posix || pipeline.negated || pipeline.commands.len() != 1
3694        {
3695            return Err(VmSubsetFallbackReason::Lowering(
3696                LoweringError::Unsupported("pipeline shape is outside the VM subset"),
3697            ));
3698        }
3699        self.validate_vm_subset_command(&pipeline.commands[0])
3700    }
3701
3702    fn validate_vm_subset_command(&self, cmd: &HirCommand) -> Result<(), VmSubsetFallbackReason> {
3703        match cmd {
3704            HirCommand::Assign(node) => Self::validate_vm_subset_assign(node),
3705            HirCommand::Exec(node) => self.validate_vm_subset_exec(node),
3706            _ => Err(VmSubsetFallbackReason::Lowering(
3707                LoweringError::Unsupported("command kind is outside the VM subset"),
3708            )),
3709        }
3710    }
3711
3712    fn validate_vm_subset_assign(
3713        node: &wasmsh_hir::HirAssign,
3714    ) -> Result<(), VmSubsetFallbackReason> {
3715        if !node.redirections.is_empty()
3716            || node
3717                .assignments
3718                .iter()
3719                .any(|a| !Self::vm_supported_assignment_name(&a.name))
3720            || node
3721                .assignments
3722                .iter()
3723                .filter_map(|a| a.value.as_ref())
3724                .any(|word| !Self::vm_supported_word(word))
3725        {
3726            return Err(VmSubsetFallbackReason::AssignmentShape);
3727        }
3728        Ok(())
3729    }
3730
3731    fn validate_vm_subset_exec(
3732        &self,
3733        node: &wasmsh_hir::HirExec,
3734    ) -> Result<(), VmSubsetFallbackReason> {
3735        if !node.env.is_empty() {
3736            return Err(VmSubsetFallbackReason::CommandEnvPrefixes);
3737        }
3738        if node.argv.is_empty() || node.argv.iter().any(|word| !Self::vm_supported_word(word)) {
3739            return Err(VmSubsetFallbackReason::UnsupportedWord);
3740        }
3741        if node
3742            .redirections
3743            .iter()
3744            .any(|redir| !Self::vm_supported_redirection(redir))
3745        {
3746            return Err(VmSubsetFallbackReason::UnsupportedRedirection);
3747        }
3748        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1")
3749            || node
3750                .argv
3751                .iter()
3752                .any(Self::vm_word_requires_full_shell_execution)
3753        {
3754            return Err(VmSubsetFallbackReason::ShellExpansion);
3755        }
3756        let Some(name) = Self::literal_word_text(&node.argv[0]) else {
3757            return Err(VmSubsetFallbackReason::UnsupportedWord);
3758        };
3759        if self.get_shopt_value("expand_aliases") && self.aliases.contains_key(name.as_str()) {
3760            return Err(VmSubsetFallbackReason::AliasExpansion);
3761        }
3762        let argv = vec![name.to_string()];
3763        if !matches!(
3764            self.resolve_command(name.as_str(), &argv),
3765            ResolvedCommand::Builtin(_)
3766        ) {
3767            return Err(VmSubsetFallbackReason::NonBuiltinCommand);
3768        }
3769        Ok(())
3770    }
3771
3772    fn vm_supported_assignment_name(name: &smol_str::SmolStr) -> bool {
3773        !name.as_str().contains('[') && !name.as_str().ends_with('+')
3774    }
3775
3776    fn vm_supported_redirection(redirection: &HirRedirection) -> bool {
3777        matches!(
3778            redirection.op,
3779            RedirectionOp::Output | RedirectionOp::Append
3780        ) && redirection.fd.unwrap_or(1) == 1
3781            && redirection.here_doc_body.is_none()
3782            && Self::vm_supported_word(&redirection.target)
3783    }
3784
3785    fn vm_supported_word(word: &Word) -> bool {
3786        word.parts.iter().all(Self::vm_supported_word_part)
3787    }
3788
3789    fn vm_word_requires_full_shell_execution(word: &Word) -> bool {
3790        word.parts
3791            .iter()
3792            .any(Self::vm_word_part_requires_full_shell_execution)
3793    }
3794
3795    fn vm_word_part_requires_full_shell_execution(part: &WordPart) -> bool {
3796        match part {
3797            WordPart::Literal(text) => Self::text_has_brace_or_glob_literal(text),
3798            WordPart::SingleQuoted(_)
3799            | WordPart::DoubleQuoted(_)
3800            | WordPart::Parameter(_)
3801            | WordPart::Arithmetic(_) => false,
3802            WordPart::CommandSubstitution(_)
3803            | WordPart::ProcessSubstIn(_)
3804            | WordPart::ProcessSubstOut(_)
3805            | _ => true,
3806        }
3807    }
3808
3809    fn vm_supported_word_part(part: &WordPart) -> bool {
3810        match part {
3811            WordPart::Literal(_)
3812            | WordPart::SingleQuoted(_)
3813            | WordPart::Parameter(_)
3814            | WordPart::Arithmetic(_) => true,
3815            WordPart::DoubleQuoted(parts) => parts.iter().all(Self::vm_supported_word_part),
3816            WordPart::CommandSubstitution(_)
3817            | WordPart::ProcessSubstIn(_)
3818            | WordPart::ProcessSubstOut(_)
3819            | _ => false,
3820        }
3821    }
3822
3823    fn literal_word_text(word: &Word) -> Option<smol_str::SmolStr> {
3824        fn append_literal(part: &WordPart, out: &mut String) -> Option<()> {
3825            match part {
3826                WordPart::Literal(text) | WordPart::SingleQuoted(text) => {
3827                    out.push_str(text);
3828                    Some(())
3829                }
3830                WordPart::DoubleQuoted(parts) => {
3831                    for part in parts {
3832                        append_literal(part, out)?;
3833                    }
3834                    Some(())
3835                }
3836                _ => None,
3837            }
3838        }
3839
3840        let mut text = String::new();
3841        for part in &word.parts {
3842            append_literal(part, &mut text)?;
3843        }
3844        Some(text.into())
3845    }
3846
3847    fn line_number_for_offset(input: &str, offset: usize) -> u32 {
3848        input
3849            .as_bytes()
3850            .iter()
3851            .take(offset)
3852            .filter(|&&b| b == b'\n')
3853            .count() as u32
3854            + 1
3855    }
3856
3857    /// Execute input and return collected events (used by eval/source).
3858    fn execute_input_inner(&mut self, input: &str) -> Vec<WorkerEvent> {
3859        self.exec.recursion_depth += 1;
3860        if let Err(reason) = self
3861            .vm
3862            .budget
3863            .enter_recursion(self.vm.limits.recursion_limit)
3864        {
3865            self.exec.recursion_depth -= 1;
3866            self.mark_budget_exhaustion(reason);
3867            return vec![WorkerEvent::Stderr(
3868                b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
3869            )];
3870        }
3871        let result = self.execute_input_inner_impl(input);
3872        self.exec.recursion_depth -= 1;
3873        self.vm.budget.exit_recursion();
3874        result
3875    }
3876
3877    /// Inner implementation of `execute_input_inner` (after recursion check).
3878    fn execute_input_inner_impl(&mut self, input: &str) -> Vec<WorkerEvent> {
3879        let ast = match wasmsh_parse::parse(input) {
3880            Ok(ast) => ast,
3881            Err(e) => {
3882                self.vm.state.last_status = 2;
3883                return vec![WorkerEvent::Stderr(
3884                    format!("wasmsh: parse error: {e}\n").into_bytes(),
3885                )];
3886            }
3887        };
3888        let hir = wasmsh_hir::lower(&ast);
3889        for cc in &hir.items {
3890            if self.exec.exit_requested.is_some() {
3891                break;
3892            }
3893            // Update $LINENO from span position
3894            let line = input
3895                .as_bytes()
3896                .iter()
3897                .take(cc.span.start as usize)
3898                .filter(|&&b| b == b'\n')
3899                .count() as u32
3900                + 1;
3901            self.vm.state.lineno = line;
3902            self.maybe_write_verbose_input(input, cc);
3903            if self.is_set_option_enabled('n') {
3904                continue;
3905            }
3906            self.execute_complete_command(cc);
3907        }
3908        // Drain stdout/stderr into events
3909        let mut events = Vec::new();
3910        if !self.vm.stdout.is_empty() {
3911            events.push(WorkerEvent::Stdout(std::mem::take(&mut self.vm.stdout)));
3912        }
3913        if !self.vm.stderr.is_empty() {
3914            events.push(WorkerEvent::Stderr(std::mem::take(&mut self.vm.stderr)));
3915        }
3916        events
3917    }
3918
3919    fn run_exit_trap_if_needed(&mut self, events: &mut Vec<WorkerEvent>) {
3920        let Some(exit_code) = self.exec.exit_requested else {
3921            return;
3922        };
3923        let Some(handler_str) = self.trap_handler("_TRAP_EXIT", "_TRAP_IGNORE_EXIT") else {
3924            return;
3925        };
3926        if self.exec.trap_depth > 0 {
3927            return;
3928        }
3929        self.exec.trap_depth += 1;
3930        self.exec.exit_requested = None;
3931        self.vm.state.last_status = exit_code;
3932        events.extend(self.execute_input_inner(&handler_str));
3933        self.exec.trap_depth -= 1;
3934        if self.exec.exit_requested.is_none() {
3935            self.exec.exit_requested = Some(exit_code);
3936        }
3937        self.vm.state.last_status = self.exec.exit_requested.unwrap_or(exit_code);
3938    }
3939
3940    fn handle_post_and_or(&mut self, and_or: &HirAndOr) {
3941        self.run_err_trap_if_needed(and_or);
3942        if self.should_errexit(and_or) {
3943            self.exec.exit_requested = Some(self.vm.state.last_status);
3944        }
3945    }
3946
3947    fn should_run_err_trap(&self, and_or: &HirAndOr) -> bool {
3948        !self.exec.errexit_suppressed
3949            && (self.exec.nested_shell_depth == 0 || self.is_set_option_enabled('E'))
3950            && and_or.rest.is_empty()
3951            && !and_or.first.negated
3952            && self.vm.state.last_status != 0
3953            && self.exec.exit_requested.is_none()
3954            && self.exec.trap_depth == 0
3955    }
3956
3957    fn run_err_trap_if_needed(&mut self, and_or: &HirAndOr) {
3958        if !self.should_run_err_trap(and_or) {
3959            return;
3960        }
3961        self.run_trap_and_merge(
3962            "_TRAP_ERR",
3963            "_TRAP_IGNORE_ERR",
3964            self.vm.state.last_status,
3965            true,
3966        );
3967    }
3968
3969    fn run_debug_trap_if_needed(&mut self) {
3970        if self.exec.trap_depth > 0
3971            || self.exec.resource_exhausted
3972            || (self.exec.nested_shell_depth > 0 && !self.is_set_option_enabled('T'))
3973        {
3974            return;
3975        }
3976        self.run_trap_and_merge(
3977            "_TRAP_DEBUG",
3978            "_TRAP_IGNORE_DEBUG",
3979            self.vm.state.last_status,
3980            true,
3981        );
3982    }
3983
3984    fn run_return_trap_if_needed(&mut self) {
3985        if self.exec.trap_depth > 0
3986            || self.exec.resource_exhausted
3987            || (self.exec.nested_shell_depth > 0 && !self.is_set_option_enabled('T'))
3988        {
3989            return;
3990        }
3991        self.run_trap_and_merge(
3992            "_TRAP_RETURN",
3993            "_TRAP_IGNORE_RETURN",
3994            self.vm.state.last_status,
3995            true,
3996        );
3997    }
3998
3999    fn run_trap_and_merge(
4000        &mut self,
4001        handler_var: &str,
4002        ignore_var: &str,
4003        trigger_status: i32,
4004        restore_status: bool,
4005    ) {
4006        let Some(handler) = self.trap_handler(handler_var, ignore_var) else {
4007            return;
4008        };
4009        let saved_status = self.vm.state.last_status;
4010        let saved_exit_requested = self.exec.exit_requested;
4011        self.exec.trap_depth += 1;
4012        self.vm.state.last_status = trigger_status;
4013        let events = self.execute_input_inner(&handler);
4014        self.exec.trap_depth -= 1;
4015        self.merge_sub_events_with_diagnostics(events);
4016        if restore_status
4017            && !self.exec.resource_exhausted
4018            && self.exec.exit_requested == saved_exit_requested
4019        {
4020            self.vm.state.last_status = saved_status;
4021        }
4022    }
4023
4024    fn trap_handler(&self, handler_var: &str, ignore_var: &str) -> Option<String> {
4025        if self.exec.trap_depth > 0 || self.vm.state.get_var(ignore_var).as_deref() == Some("1") {
4026            return None;
4027        }
4028        let handler = self.vm.state.get_var(handler_var)?;
4029        if handler.is_empty() {
4030            return None;
4031        }
4032        Some(handler.to_string())
4033    }
4034
4035    fn signal_trap_handler(&self, spec: &RuntimeSignalSpec) -> Option<String> {
4036        if !spec.trappable {
4037            return None;
4038        }
4039        self.trap_handler(spec.handler_var, spec.ignore_var)
4040    }
4041
4042    fn run_signal_trap(&mut self, spec: &RuntimeSignalSpec, handler: &str) -> Vec<WorkerEvent> {
4043        let saved_status = self.vm.state.last_status;
4044        let saved_exit_requested = self.exec.exit_requested;
4045        let saved_exec_io = self.current_exec_io.take();
4046        let saved_output_captures = std::mem::take(&mut self.exec.output_captures);
4047        self.exec.trap_depth += 1;
4048        self.vm.state.last_status = 128 + spec.number;
4049        let events = self.execute_input_inner(handler);
4050        self.exec.trap_depth -= 1;
4051        self.current_exec_io = saved_exec_io;
4052        self.exec.output_captures = saved_output_captures;
4053        if !self.exec.resource_exhausted && self.exec.exit_requested == saved_exit_requested {
4054            self.vm.state.last_status = saved_status;
4055        }
4056        events
4057    }
4058
4059    fn with_nested_shell_scope<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
4060        self.exec.nested_shell_depth += 1;
4061        let out = f(self);
4062        self.exec.nested_shell_depth -= 1;
4063        out
4064    }
4065
4066    fn drain_io_events(&mut self, events: &mut Vec<WorkerEvent>) {
4067        self.push_buffer_event(events, true);
4068        self.push_buffer_event(events, false);
4069    }
4070
4071    fn push_buffer_event(&mut self, events: &mut Vec<WorkerEvent>, stdout: bool) {
4072        let buffer = if stdout {
4073            &mut self.vm.stdout
4074        } else {
4075            &mut self.vm.stderr
4076        };
4077        if buffer.is_empty() {
4078            return;
4079        }
4080
4081        let data = std::mem::take(buffer);
4082        events.push(if stdout {
4083            WorkerEvent::Stdout(data)
4084        } else {
4085            WorkerEvent::Stderr(data)
4086        });
4087    }
4088
4089    fn push_output_capture(&mut self, capture_stdout: bool, capture_stderr: bool) {
4090        self.exec.output_captures.push(OutputCapture {
4091            capture_stdout,
4092            capture_stderr,
4093            ..OutputCapture::default()
4094        });
4095    }
4096
4097    fn pop_output_capture(&mut self) -> CapturedOutput {
4098        let capture = self
4099            .exec
4100            .output_captures
4101            .pop()
4102            .expect("output capture stack underflow");
4103        CapturedOutput {
4104            stdout: capture.stdout,
4105            stderr: capture.stderr,
4106        }
4107    }
4108
4109    fn with_output_capture<T>(
4110        &mut self,
4111        capture_stdout: bool,
4112        capture_stderr: bool,
4113        f: impl FnOnce(&mut Self) -> T,
4114    ) -> (T, CapturedOutput) {
4115        self.push_output_capture(capture_stdout, capture_stderr);
4116        let result = f(self);
4117        let captured = self.pop_output_capture();
4118        (result, captured)
4119    }
4120
4121    fn with_exec_io_scope<T>(
4122        &mut self,
4123        exec_io: Option<ExecIo>,
4124        f: impl FnOnce(&mut Self) -> T,
4125    ) -> T {
4126        if let Some(exec_io) = exec_io {
4127            let saved = self.current_exec_io.replace(exec_io);
4128            let result = f(self);
4129            let current = self.current_exec_io.take();
4130            self.current_exec_io = match (saved, current) {
4131                (Some(mut saved), Some(mut current)) => {
4132                    let stdin = current.take_stdin();
4133                    saved.fds_mut().set_input(stdin);
4134                    Some(saved)
4135                }
4136                (saved, _) => saved,
4137            };
4138            result
4139        } else {
4140            f(self)
4141        }
4142    }
4143
4144    fn append_visible_output_direct(&mut self, data: &[u8], stdout: bool) {
4145        if stdout {
4146            self.vm.stdout.extend_from_slice(data);
4147        } else {
4148            self.vm.stderr.extend_from_slice(data);
4149        }
4150    }
4151
4152    fn write_output_destination_direct(&mut self, destination: &OutputTarget, data: &[u8]) -> bool {
4153        match destination {
4154            OutputTarget::InheritStdout => {
4155                self.append_visible_output_direct(data, true);
4156                true
4157            }
4158            OutputTarget::InheritStderr => {
4159                self.append_visible_output_direct(data, false);
4160                true
4161            }
4162            OutputTarget::File { path, sink, .. } => {
4163                if let Err(err) = sink.borrow_mut().write(data) {
4164                    let msg = format!("wasmsh: write error: {err}\n");
4165                    self.emit_visible_stderr_direct(msg.as_bytes());
4166                    self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
4167                        level: wasmsh_vm::DiagLevel::Error,
4168                        category: wasmsh_vm::DiagCategory::Filesystem,
4169                        message: format!("write failed for {path}: {err}"),
4170                    });
4171                }
4172                false
4173            }
4174            OutputTarget::ProcessSubst { path } => {
4175                if let Some(sink) = self.process_subst_out_sink_mut(path) {
4176                    sink.write(data);
4177                } else {
4178                    let msg = format!("wasmsh: {path}: process substitution sink not found\n");
4179                    self.emit_visible_stderr_direct(msg.as_bytes());
4180                }
4181                false
4182            }
4183            OutputTarget::Pipe(pipe) => {
4184                pipe.borrow_mut().write_all(data);
4185                false
4186            }
4187            OutputTarget::Closed => false,
4188        }
4189    }
4190
4191    fn emit_visible_stderr_direct(&mut self, data: &[u8]) {
4192        self.append_visible_output_direct(data, false);
4193        self.account_output(data.len());
4194    }
4195
4196    fn route_output(&mut self, data: &[u8], stdout: bool) -> bool {
4197        let mut routed_stdout = stdout;
4198        if let Some(exec_io) = self.current_exec_io.as_ref() {
4199            let destination = exec_io.output_target(stdout);
4200            match destination {
4201                OutputTarget::InheritStdout => {
4202                    routed_stdout = true;
4203                }
4204                OutputTarget::InheritStderr => {
4205                    routed_stdout = false;
4206                }
4207                OutputTarget::File { .. }
4208                | OutputTarget::ProcessSubst { .. }
4209                | OutputTarget::Pipe(_)
4210                | OutputTarget::Closed => {
4211                    return self.write_output_destination_direct(&destination, data);
4212                }
4213            }
4214        }
4215
4216        for capture in self.exec.output_captures.iter_mut().rev() {
4217            let should_capture = if routed_stdout {
4218                capture.capture_stdout
4219            } else {
4220                capture.capture_stderr
4221            };
4222            if !should_capture {
4223                continue;
4224            }
4225            if routed_stdout {
4226                capture.stdout.extend_from_slice(data);
4227            } else {
4228                capture.stderr.extend_from_slice(data);
4229            }
4230            return false;
4231        }
4232
4233        if routed_stdout {
4234            self.vm.stdout.extend_from_slice(data);
4235        } else {
4236            self.vm.stderr.extend_from_slice(data);
4237        }
4238        true
4239    }
4240
4241    fn account_output(&mut self, bytes: usize) {
4242        self.vm.track_output(bytes as u64);
4243        self.flag_output_limit_if_needed();
4244    }
4245
4246    fn write_stdout(&mut self, data: &[u8]) {
4247        if self.route_output(data, true) {
4248            self.account_output(data.len());
4249        }
4250    }
4251
4252    fn write_stderr(&mut self, data: &[u8]) {
4253        if self.route_output(data, false) {
4254            self.account_output(data.len());
4255        }
4256    }
4257
4258    fn write_streams(&mut self, stdout: &[u8], stderr: &[u8]) {
4259        let visible_stdout = self.route_output(stdout, true);
4260        let visible_stderr = self.route_output(stderr, false);
4261        let visible_bytes =
4262            usize::from(visible_stdout) * stdout.len() + usize::from(visible_stderr) * stderr.len();
4263        if visible_bytes > 0 {
4264            self.account_output(visible_bytes);
4265        }
4266    }
4267
4268    fn flag_output_limit_if_needed(&mut self) {
4269        if self.exec.resource_exhausted {
4270            return;
4271        }
4272        if self.vm.check_output_limit().is_err() {
4273            self.exec.resource_exhausted = true;
4274        }
4275    }
4276
4277    fn drain_diagnostic_events(&mut self, events: &mut Vec<WorkerEvent>) {
4278        for diag in self.vm.diagnostics.drain(..) {
4279            events.push(WorkerEvent::Diagnostic(
4280                Self::to_protocol_diag_level(diag.level),
4281                diag.message,
4282            ));
4283        }
4284    }
4285
4286    fn to_protocol_diag_level(level: wasmsh_vm::DiagLevel) -> DiagnosticLevel {
4287        match level {
4288            wasmsh_vm::DiagLevel::Trace => DiagnosticLevel::Trace,
4289            wasmsh_vm::DiagLevel::Info => DiagnosticLevel::Info,
4290            wasmsh_vm::DiagLevel::Warning => DiagnosticLevel::Warning,
4291            wasmsh_vm::DiagLevel::Error => DiagnosticLevel::Error,
4292        }
4293    }
4294
4295    fn execute_pipeline_chain(&mut self, and_or: &HirAndOr) {
4296        self.execute_pipeline(&and_or.first);
4297        for (op, pipeline) in &and_or.rest {
4298            match op {
4299                HirAndOrOp::And => {
4300                    if self.vm.state.last_status == 0 {
4301                        self.execute_pipeline(pipeline);
4302                    }
4303                }
4304                HirAndOrOp::Or => {
4305                    if self.vm.state.last_status != 0 {
4306                        self.execute_pipeline(pipeline);
4307                    }
4308                }
4309            }
4310        }
4311    }
4312
4313    #[allow(clippy::let_unit_value)]
4314    fn execute_pipeline(&mut self, pipeline: &HirPipeline) {
4315        let started = pipeline_started_at();
4316        let cmds = &pipeline.commands;
4317        self.execute_scheduled_pipeline(cmds, pipeline);
4318        if pipeline.negated {
4319            self.vm.state.last_status = i32::from(self.vm.state.last_status == 0);
4320        }
4321        if pipeline.timed {
4322            self.emit_pipeline_timing(pipeline.time_posix, started_elapsed_seconds(started));
4323        }
4324    }
4325
4326    fn execute_scheduled_pipeline(&mut self, cmds: &[HirCommand], pipeline: &HirPipeline) {
4327        self.execute_scheduled_pipeline_with_source_reader(cmds, pipeline, None);
4328    }
4329
4330    fn execute_scheduled_pipeline_with_source_reader(
4331        &mut self,
4332        cmds: &[HirCommand],
4333        pipeline: &HirPipeline,
4334        source_reader: Option<Box<dyn Read>>,
4335    ) {
4336        let pipefail = self.vm.state.get_var("SHOPT_o_pipefail").as_deref() == Some("1");
4337        let (stages, stage_last_args) = self.compile_pipeline_stages(cmds, source_reader.is_none());
4338        if source_reader.is_none() && stages.len() == 1 {
4339            self.run_single_pipeline_stage(&cmds[0], &stages[0], stage_last_args[0].as_deref());
4340            return;
4341        }
4342        let stage_statuses = Self::seed_stage_statuses(&stages);
4343        let stage_stderr: Vec<Rc<RefCell<Vec<u8>>>> = stages
4344            .iter()
4345            .map(|_| Rc::new(RefCell::new(Vec::new())))
4346            .collect();
4347        let stage_pipe_stderr: Vec<bool> = (0..stages.len())
4348            .map(|idx| pipeline.pipe_stderr.get(idx).copied().unwrap_or(false))
4349            .collect();
4350
4351        self.execute_pipebuffer_streaming_pipeline(
4352            source_reader,
4353            &stages,
4354            &stage_pipe_stderr,
4355            &stage_statuses,
4356            &stage_stderr,
4357        );
4358
4359        let statuses: Vec<i32> = stage_statuses
4360            .iter()
4361            .map(|status| *status.borrow())
4362            .collect();
4363        if let Some(last_arg) = stage_last_args.iter().rev().flatten().next() {
4364            self.vm.state.set_last_argument(last_arg.as_str());
4365        }
4366        self.set_pipestatus(&statuses);
4367        if !self.exec.resource_exhausted {
4368            self.vm.state.last_status = Self::resolve_pipeline_exit_status(&statuses, pipefail);
4369        }
4370    }
4371
4372    fn compile_pipeline_stages(
4373        &mut self,
4374        cmds: &[HirCommand],
4375        no_source_reader: bool,
4376    ) -> (Vec<StreamingPipelineStage>, Vec<Option<String>>) {
4377        cmds.iter()
4378            .enumerate()
4379            .map(|(idx, cmd)| {
4380                self.compile_pipeline_stage_with_last_argument(cmd, idx == 0 && no_source_reader)
4381            })
4382            .unzip()
4383    }
4384
4385    fn run_single_pipeline_stage(
4386        &mut self,
4387        cmd: &HirCommand,
4388        stage: &StreamingPipelineStage,
4389        last_arg: Option<&str>,
4390    ) {
4391        if self.command_needs_full_single_stage_execution(cmd) {
4392            self.execute_command(cmd);
4393            let status = self.vm.state.last_status;
4394            self.set_pipestatus(&[status]);
4395            return;
4396        }
4397        if !matches!(stage, StreamingPipelineStage::BufferedCommand(_))
4398            && !Self::command_requires_runtime_expansion(cmd)
4399        {
4400            if let Some(argv) = self.resolve_streaming_pipeline_argv(cmd) {
4401                self.trace_command(&argv);
4402            }
4403        }
4404        let status = self.execute_scheduled_single_stage(stage);
4405        if let Some(last_arg) = last_arg {
4406            self.vm.state.set_last_argument(last_arg);
4407        }
4408        self.set_pipestatus(&[status]);
4409        if !self.exec.resource_exhausted {
4410            self.vm.state.last_status = status;
4411        }
4412    }
4413
4414    fn seed_stage_statuses(stages: &[StreamingPipelineStage]) -> Vec<Rc<RefCell<i32>>> {
4415        stages
4416            .iter()
4417            .map(|stage| {
4418                Rc::new(RefCell::new(i32::from(matches!(
4419                    stage,
4420                    StreamingPipelineStage::Grep(_)
4421                ))))
4422            })
4423            .collect()
4424    }
4425
4426    fn resolve_pipeline_exit_status(statuses: &[i32], pipefail: bool) -> i32 {
4427        if pipefail {
4428            statuses
4429                .iter()
4430                .rev()
4431                .copied()
4432                .find(|status| *status != 0)
4433                .unwrap_or(0)
4434        } else {
4435            statuses.last().copied().unwrap_or(0)
4436        }
4437    }
4438
4439    fn execute_scheduled_single_stage(&mut self, stage: &StreamingPipelineStage) -> i32 {
4440        match stage {
4441            StreamingPipelineStage::Literal(data) => {
4442                self.write_stdout(data);
4443                0
4444            }
4445            StreamingPipelineStage::File(path) => self.execute_single_stage_file(path),
4446            StreamingPipelineStage::Yes { line } => self.execute_single_stage_yes(line),
4447            StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(argv)) => {
4448                self.trace_command(argv);
4449                self.execute_argv_command(argv);
4450                self.vm.state.last_status
4451            }
4452            StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(cmd)) => {
4453                self.execute_command(cmd);
4454                self.vm.state.last_status
4455            }
4456            _ => {
4457                self.vm.state.last_status = 1;
4458                self.write_stderr(b"wasmsh: unsupported single-stage scheduler node\n");
4459                1
4460            }
4461        }
4462    }
4463
4464    fn execute_single_stage_file(&mut self, path: &str) -> i32 {
4465        let resolved = self.resolve_cwd_path(path);
4466        let Ok(mut reader) = self.open_streaming_file_reader(&resolved, "cat") else {
4467            return self.vm.state.last_status;
4468        };
4469        let mut buffer = [0u8; 4096];
4470        loop {
4471            match reader.read(&mut buffer) {
4472                Ok(0) => return 0,
4473                Ok(read) => {
4474                    self.write_stdout(&buffer[..read]);
4475                    if self.exec.resource_exhausted {
4476                        return 1;
4477                    }
4478                }
4479                Err(err) => {
4480                    self.write_stderr(format!("wasmsh: cat: stdin read error: {err}\n").as_bytes());
4481                    return 1;
4482                }
4483            }
4484        }
4485    }
4486
4487    fn execute_single_stage_yes(&mut self, line: &[u8]) -> i32 {
4488        for _ in 0..STREAMING_YES_MAX_LINES {
4489            self.write_stdout(line);
4490            if self.exec.resource_exhausted {
4491                return 1;
4492            }
4493        }
4494        0
4495    }
4496
4497    fn compile_pipeline_stage(
4498        &mut self,
4499        cmd: &HirCommand,
4500        is_first: bool,
4501    ) -> StreamingPipelineStage {
4502        let resolved_argv = self.resolve_streaming_pipeline_argv(cmd);
4503        self.compile_pipeline_stage_from_argv(cmd, is_first, resolved_argv)
4504    }
4505
4506    fn compile_pipeline_stage_with_last_argument(
4507        &mut self,
4508        cmd: &HirCommand,
4509        is_first: bool,
4510    ) -> (StreamingPipelineStage, Option<String>) {
4511        let resolved_argv = self.resolve_streaming_pipeline_argv(cmd);
4512        let last_arg = resolved_argv.as_ref().and_then(|argv| argv.last().cloned());
4513        (
4514            self.compile_pipeline_stage_from_argv(cmd, is_first, resolved_argv),
4515            last_arg,
4516        )
4517    }
4518
4519    fn compile_pipeline_stage_from_argv(
4520        &mut self,
4521        cmd: &HirCommand,
4522        is_first: bool,
4523        resolved_argv: Option<Vec<String>>,
4524    ) -> StreamingPipelineStage {
4525        if let Some(argv) = resolved_argv {
4526            if self.get_shopt_value("expand_aliases")
4527                && argv
4528                    .first()
4529                    .is_some_and(|name| self.aliases.contains_key(name))
4530            {
4531                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(
4532                    cmd.clone(),
4533                ));
4534            }
4535            if argv
4536                .first()
4537                .is_some_and(|name| self.functions.contains_key(name))
4538            {
4539                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(
4540                    argv,
4541                ));
4542            }
4543            if let Some(stage) = self.parse_streaming_stage(&argv, is_first) {
4544                if Self::uses_native_pipe_scheduler(&stage) {
4545                    return stage;
4546                }
4547                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(
4548                    argv,
4549                ));
4550            }
4551            return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(
4552                cmd.clone(),
4553            ));
4554        }
4555        StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(cmd.clone()))
4556    }
4557
4558    fn uses_native_pipe_scheduler(stage: &StreamingPipelineStage) -> bool {
4559        !matches!(stage, StreamingPipelineStage::BufferedCommand(_))
4560    }
4561
4562    fn execute_pipebuffer_streaming_pipeline(
4563        &mut self,
4564        source_reader: Option<Box<dyn Read>>,
4565        stages: &[StreamingPipelineStage],
4566        stage_pipe_stderr: &[bool],
4567        stage_statuses: &[Rc<RefCell<i32>>],
4568        stage_stderr: &[Rc<RefCell<Vec<u8>>>],
4569    ) -> bool {
4570        let mut processes = Vec::new();
4571        let output_pipes: Vec<Rc<RefCell<PipeBuffer>>> = (0..stages.len())
4572            .map(|_| Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY))))
4573            .collect();
4574        let ctx = StreamingStageCtx {
4575            stages,
4576            stage_pipe_stderr,
4577            stage_statuses,
4578            stage_stderr,
4579            output_pipes: &output_pipes,
4580        };
4581
4582        if let Some(early) = self.setup_first_streaming_process(source_reader, &ctx, &mut processes)
4583        {
4584            return early;
4585        }
4586        for idx in 1..stages.len() {
4587            if !self.setup_later_streaming_stage(idx, &ctx, &mut processes) {
4588                return false;
4589            }
4590        }
4591
4592        let final_pipe = output_pipes
4593            .last()
4594            .cloned()
4595            .expect("final pipe missing for streaming pipeline");
4596        self.drive_streaming_pipeline(&mut processes, &output_pipes, &final_pipe);
4597
4598        for process in &mut processes {
4599            process.close(self);
4600        }
4601        self.drain_streaming_stage_stderr(stage_pipe_stderr, stage_stderr);
4602        true
4603    }
4604
4605    fn setup_first_streaming_process(
4606        &mut self,
4607        source_reader: Option<Box<dyn Read>>,
4608        ctx: &StreamingStageCtx<'_>,
4609        processes: &mut Vec<StreamingPipeProcess<'static>>,
4610    ) -> Option<bool> {
4611        if let Some(source_reader) = source_reader {
4612            self.setup_first_with_source(source_reader, ctx, processes)
4613        } else {
4614            self.setup_first_without_source(ctx, processes)
4615        }
4616    }
4617
4618    fn setup_first_with_source(
4619        &mut self,
4620        source_reader: Box<dyn Read>,
4621        ctx: &StreamingStageCtx<'_>,
4622        processes: &mut Vec<StreamingPipeProcess<'static>>,
4623    ) -> Option<bool> {
4624        let source_pipe = Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY)));
4625        let source_stderr = Rc::new(RefCell::new(Vec::new()));
4626        let source_status = Rc::new(RefCell::new(0));
4627        processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4628            source_reader,
4629            source_pipe.clone(),
4630            source_stderr,
4631            source_status,
4632            "source",
4633            false,
4634        )));
4635        match &ctx.stages[0] {
4636            StreamingPipelineStage::Tee(stage) => {
4637                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
4638                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
4639                    reader,
4640                    ctx.output_pipes[0].clone(),
4641                    &mut self.fs,
4642                    self.vm.state.cwd.as_str(),
4643                    stage,
4644                    ctx.stage_stderr[0].clone(),
4645                    ctx.stage_statuses[0].clone(),
4646                    ctx.stage_pipe_stderr[0],
4647                )));
4648                None
4649            }
4650            StreamingPipelineStage::BufferedCommand(argv) => {
4651                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
4652                    Some(source_pipe),
4653                    ctx.output_pipes[0].clone(),
4654                    argv.clone(),
4655                    ctx.stage_pipe_stderr[0],
4656                    ctx.stage_stderr[0].clone(),
4657                    ctx.stage_statuses[0].clone(),
4658                )));
4659                None
4660            }
4661            _ => {
4662                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
4663                let Some(stage_reader) = Self::wrap_non_tee_streaming_stage(
4664                    reader,
4665                    &ctx.stages[0],
4666                    0,
4667                    ctx.stage_statuses,
4668                ) else {
4669                    return Some(false);
4670                };
4671                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4672                    stage_reader,
4673                    ctx.output_pipes[0].clone(),
4674                    ctx.stage_stderr[0].clone(),
4675                    ctx.stage_statuses[0].clone(),
4676                    "stage",
4677                    ctx.stage_pipe_stderr[0],
4678                )));
4679                None
4680            }
4681        }
4682    }
4683
4684    fn setup_first_without_source(
4685        &mut self,
4686        ctx: &StreamingStageCtx<'_>,
4687        processes: &mut Vec<StreamingPipeProcess<'static>>,
4688    ) -> Option<bool> {
4689        match &ctx.stages[0] {
4690            StreamingPipelineStage::Literal(data) => {
4691                let first_reader: Box<dyn Read> = Box::new(Cursor::new(data.clone()));
4692                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4693                    first_reader,
4694                    ctx.output_pipes[0].clone(),
4695                    ctx.stage_stderr[0].clone(),
4696                    ctx.stage_statuses[0].clone(),
4697                    "source",
4698                    ctx.stage_pipe_stderr[0],
4699                )));
4700                None
4701            }
4702            StreamingPipelineStage::File(path) => {
4703                let resolved = self.resolve_cwd_path(path);
4704                let Ok(first_reader) = self.open_streaming_file_reader(&resolved, "cat") else {
4705                    *ctx.stage_statuses[0].borrow_mut() = self.vm.state.last_status;
4706                    return Some(true);
4707                };
4708                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4709                    first_reader,
4710                    ctx.output_pipes[0].clone(),
4711                    ctx.stage_stderr[0].clone(),
4712                    ctx.stage_statuses[0].clone(),
4713                    "source",
4714                    ctx.stage_pipe_stderr[0],
4715                )));
4716                None
4717            }
4718            StreamingPipelineStage::Yes { line } => {
4719                let first_reader: Box<dyn Read> =
4720                    Box::new(YesStreamReader::new(line.clone(), STREAMING_YES_MAX_LINES));
4721                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4722                    first_reader,
4723                    ctx.output_pipes[0].clone(),
4724                    ctx.stage_stderr[0].clone(),
4725                    ctx.stage_statuses[0].clone(),
4726                    "source",
4727                    ctx.stage_pipe_stderr[0],
4728                )));
4729                None
4730            }
4731            StreamingPipelineStage::BufferedCommand(argv) => {
4732                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
4733                    None,
4734                    ctx.output_pipes[0].clone(),
4735                    argv.clone(),
4736                    ctx.stage_pipe_stderr[0],
4737                    ctx.stage_stderr[0].clone(),
4738                    ctx.stage_statuses[0].clone(),
4739                )));
4740                None
4741            }
4742            _ => unreachable!("unexpected first pipeline stage"),
4743        }
4744    }
4745
4746    fn setup_later_streaming_stage(
4747        &mut self,
4748        idx: usize,
4749        ctx: &StreamingStageCtx<'_>,
4750        processes: &mut Vec<StreamingPipeProcess<'static>>,
4751    ) -> bool {
4752        match &ctx.stages[idx] {
4753            StreamingPipelineStage::Head(mode) => {
4754                processes.push(StreamingPipeProcess::Head(HeadPipeProcess::new(
4755                    ctx.output_pipes[idx - 1].clone(),
4756                    ctx.output_pipes[idx].clone(),
4757                    *mode,
4758                )));
4759            }
4760            StreamingPipelineStage::Tee(stage) => {
4761                let reader =
4762                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
4763                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
4764                    reader,
4765                    ctx.output_pipes[idx].clone(),
4766                    &mut self.fs,
4767                    self.vm.state.cwd.as_str(),
4768                    stage,
4769                    ctx.stage_stderr[idx].clone(),
4770                    ctx.stage_statuses[idx].clone(),
4771                    ctx.stage_pipe_stderr[idx],
4772                )));
4773            }
4774            StreamingPipelineStage::BufferedCommand(argv) => {
4775                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
4776                    Some(ctx.output_pipes[idx - 1].clone()),
4777                    ctx.output_pipes[idx].clone(),
4778                    argv.clone(),
4779                    ctx.stage_pipe_stderr[idx],
4780                    ctx.stage_stderr[idx].clone(),
4781                    ctx.stage_statuses[idx].clone(),
4782                )));
4783            }
4784            _ => {
4785                let reader =
4786                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
4787                let Some(stage_reader) = Self::wrap_non_tee_streaming_stage(
4788                    reader,
4789                    &ctx.stages[idx],
4790                    idx,
4791                    ctx.stage_statuses,
4792                ) else {
4793                    return false;
4794                };
4795                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
4796                    stage_reader,
4797                    ctx.output_pipes[idx].clone(),
4798                    ctx.stage_stderr[idx].clone(),
4799                    ctx.stage_statuses[idx].clone(),
4800                    "stage",
4801                    ctx.stage_pipe_stderr[idx],
4802                )));
4803            }
4804        }
4805        true
4806    }
4807
4808    fn drive_streaming_pipeline(
4809        &mut self,
4810        processes: &mut [StreamingPipeProcess<'static>],
4811        output_pipes: &[Rc<RefCell<PipeBuffer>>],
4812        final_pipe: &Rc<RefCell<PipeBuffer>>,
4813    ) {
4814        let mut finished = vec![false; processes.len()];
4815        loop {
4816            if self.check_resource_limits() {
4817                final_pipe.borrow_mut().close_read();
4818                break;
4819            }
4820
4821            let mut progressed = self.poll_streaming_processes(processes, &mut finished);
4822
4823            let buffered_pipe_bytes = output_pipes
4824                .iter()
4825                .map(|pipe| pipe.borrow().len() as u64)
4826                .sum();
4827            self.sync_pipe_budget(buffered_pipe_bytes);
4828            if self.exec.resource_exhausted {
4829                final_pipe.borrow_mut().close_read();
4830                break;
4831            }
4832
4833            if self.drain_final_pipe_to_stdout(final_pipe, &mut progressed) {
4834                break;
4835            }
4836
4837            if self.exec.resource_exhausted || finished.iter().all(|done| *done) || !progressed {
4838                break;
4839            }
4840        }
4841    }
4842
4843    fn poll_streaming_processes(
4844        &mut self,
4845        processes: &mut [StreamingPipeProcess<'static>],
4846        finished: &mut [bool],
4847    ) -> bool {
4848        let mut progressed = false;
4849        for idx in (0..processes.len()).rev() {
4850            if finished[idx] {
4851                continue;
4852            }
4853            match processes[idx].poll(self) {
4854                PipeProcessPoll::Ready => progressed = true,
4855                PipeProcessPoll::PendingRead | PipeProcessPoll::PendingWrite => {}
4856                PipeProcessPoll::Exited => {
4857                    finished[idx] = true;
4858                    progressed = true;
4859                }
4860            }
4861        }
4862        progressed
4863    }
4864
4865    fn drain_final_pipe_to_stdout(
4866        &mut self,
4867        final_pipe: &Rc<RefCell<PipeBuffer>>,
4868        progressed: &mut bool,
4869    ) -> bool {
4870        loop {
4871            let mut buffer = [0u8; 4096];
4872            let read_result = {
4873                let mut pipe = final_pipe.borrow_mut();
4874                pipe.read(&mut buffer)
4875            };
4876            match read_result {
4877                ReadResult::Read(read) => {
4878                    self.write_stdout(&buffer[..read]);
4879                    *progressed = true;
4880                    if self.exec.resource_exhausted {
4881                        final_pipe.borrow_mut().close_read();
4882                        return true;
4883                    }
4884                }
4885                ReadResult::WouldBlock | ReadResult::Eof => return false,
4886            }
4887        }
4888    }
4889
4890    fn drain_streaming_stage_stderr(
4891        &mut self,
4892        stage_pipe_stderr: &[bool],
4893        stage_stderr: &[Rc<RefCell<Vec<u8>>>],
4894    ) {
4895        for (idx, stderr) in stage_stderr.iter().enumerate() {
4896            if stage_pipe_stderr[idx] {
4897                continue;
4898            }
4899            let data = stderr.borrow();
4900            if !data.is_empty() {
4901                self.write_stderr(&data);
4902            }
4903        }
4904    }
4905
4906    fn wrap_non_tee_streaming_stage<'a>(
4907        reader: Box<dyn Read + 'a>,
4908        stage: &StreamingPipelineStage,
4909        idx: usize,
4910        stage_statuses: &[Rc<RefCell<i32>>],
4911    ) -> Option<Box<dyn Read + 'a>> {
4912        match stage {
4913            StreamingPipelineStage::Cat => Some(reader),
4914            StreamingPipelineStage::Head(mode) => Some(match mode {
4915                StreamingHeadMode::Lines(limit) => Box::new(HeadStreamReader::new(
4916                    reader,
4917                    StreamingHeadMode::Lines(*limit),
4918                )),
4919                StreamingHeadMode::Bytes(limit) => Box::new(HeadStreamReader::new(
4920                    reader,
4921                    StreamingHeadMode::Bytes(*limit),
4922                )),
4923            }),
4924            StreamingPipelineStage::Tail(mode) => Some(match mode {
4925                StreamingTailMode::Lines(limit) => Box::new(TailStreamReader::new(
4926                    reader,
4927                    StreamingTailMode::Lines(*limit),
4928                )),
4929                StreamingTailMode::Bytes(limit) => Box::new(TailStreamReader::new(
4930                    reader,
4931                    StreamingTailMode::Bytes(*limit),
4932                )),
4933            }),
4934            StreamingPipelineStage::Bat(stage) => {
4935                Some(Box::new(BatStreamReader::new(reader, *stage)))
4936            }
4937            StreamingPipelineStage::Sed(stage) => {
4938                Some(Box::new(SedStreamReader::new(reader, stage.clone())))
4939            }
4940            StreamingPipelineStage::Paste(stage) => {
4941                Some(Box::new(PasteStreamReader::new(reader, stage.clone())))
4942            }
4943            StreamingPipelineStage::Column(_) => Some(Box::new(ColumnStreamReader::new(reader))),
4944            StreamingPipelineStage::Grep(stage) => Some(Box::new(GrepStreamReader::new(
4945                reader,
4946                stage.clone(),
4947                stage_statuses[idx].clone(),
4948            ))),
4949            StreamingPipelineStage::Uniq(flags) => {
4950                Some(Box::new(UniqStreamReader::new(reader, flags.clone())))
4951            }
4952            StreamingPipelineStage::Rev => Some(Box::new(RevStreamReader::new(reader))),
4953            StreamingPipelineStage::Cut(stage) => {
4954                Some(Box::new(CutStreamReader::new(reader, stage.clone())))
4955            }
4956            StreamingPipelineStage::Tr(stage) => {
4957                Some(Box::new(TrStreamReader::new(reader, stage.clone())))
4958            }
4959            StreamingPipelineStage::Wc(flags) => {
4960                Some(Box::new(WcStreamReader::new(reader, *flags)))
4961            }
4962            StreamingPipelineStage::Tee(_)
4963            | StreamingPipelineStage::Literal(_)
4964            | StreamingPipelineStage::File(_)
4965            | StreamingPipelineStage::Yes { .. }
4966            | StreamingPipelineStage::BufferedCommand(_) => None,
4967        }
4968    }
4969
4970    fn resolve_streaming_pipeline_argv(&mut self, cmd: &HirCommand) -> Option<Vec<String>> {
4971        let HirCommand::Exec(exec) = cmd else {
4972            return None;
4973        };
4974        if !exec.env.is_empty()
4975            || !exec.redirections.is_empty()
4976            || Self::command_requires_runtime_expansion(cmd)
4977        {
4978            return None;
4979        }
4980        let resolved = self.resolve_command_subst(&exec.argv);
4981        if self.exec.expansion_failed {
4982            return None;
4983        }
4984        let expanded = expand_words_argv(&resolved, &mut self.vm.state);
4985        if self.check_nounset_error() || expanded.is_empty() {
4986            return None;
4987        }
4988        let tagged: Vec<(String, bool)> = expanded
4989            .into_iter()
4990            .flat_map(|ew| {
4991                if ew.was_quoted {
4992                    vec![(ew.text, true)]
4993                } else {
4994                    wasmsh_expand::expand_braces(&ew.text)
4995                        .into_iter()
4996                        .map(|s| (s, false))
4997                        .collect()
4998                }
4999            })
5000            .collect();
5001        Some(self.expand_globs_tagged(tagged))
5002    }
5003
5004    fn parse_streaming_stage(
5005        &self,
5006        argv: &[String],
5007        is_first: bool,
5008    ) -> Option<StreamingPipelineStage> {
5009        let cmd_name = argv.first()?.as_str();
5010        if let Some(stage) = Self::parse_streaming_first_stage(cmd_name, argv, is_first) {
5011            return Some(stage);
5012        }
5013        if let Some(stage) = Self::parse_streaming_internal_stage(cmd_name, argv, is_first) {
5014            return Some(stage);
5015        }
5016        if self.is_buffered_stage_candidate(cmd_name) {
5017            return Some(StreamingPipelineStage::BufferedCommand(
5018                BufferedPipelineCommand::Argv(argv.to_vec()),
5019            ));
5020        }
5021        None
5022    }
5023
5024    fn parse_streaming_first_stage(
5025        cmd_name: &str,
5026        argv: &[String],
5027        is_first: bool,
5028    ) -> Option<StreamingPipelineStage> {
5029        if !is_first {
5030            return None;
5031        }
5032        match cmd_name {
5033            "echo" => Some(StreamingPipelineStage::Literal(Self::streaming_echo_bytes(
5034                &argv[1..],
5035            ))),
5036            "yes" => {
5037                let text = if argv.len() > 1 {
5038                    argv[1..].join(" ")
5039                } else {
5040                    "y".to_string()
5041                };
5042                Some(StreamingPipelineStage::Yes {
5043                    line: format!("{text}\n").into_bytes(),
5044                })
5045            }
5046            _ => None,
5047        }
5048    }
5049
5050    fn parse_streaming_internal_stage(
5051        cmd_name: &str,
5052        argv: &[String],
5053        is_first: bool,
5054    ) -> Option<StreamingPipelineStage> {
5055        if cmd_name == "cat" {
5056            return Self::parse_streaming_cat_stage(&argv[1..], is_first);
5057        }
5058        if is_first {
5059            return None;
5060        }
5061        match cmd_name {
5062            "head" => Self::parse_streaming_head_stage(&argv[1..]),
5063            "tail" => Self::parse_streaming_tail_stage(&argv[1..]),
5064            "bat" => Self::parse_streaming_bat_stage(&argv[1..]),
5065            "sed" => Self::parse_streaming_sed_stage(&argv[1..]),
5066            "tee" => Self::parse_streaming_tee_stage(&argv[1..]),
5067            "paste" => Self::parse_streaming_paste_stage(&argv[1..]),
5068            "column" => Self::parse_streaming_column_stage(&argv[1..]),
5069            "grep" => Self::parse_streaming_grep_stage(&argv[1..]),
5070            "uniq" => Self::parse_streaming_uniq_stage(&argv[1..]),
5071            "rev" => Self::parse_streaming_rev_stage(&argv[1..]),
5072            "cut" => Self::parse_streaming_cut_stage(&argv[1..]),
5073            "tr" => Self::parse_streaming_tr_stage(&argv[1..]),
5074            "wc" => Self::parse_streaming_wc_stage(&argv[1..]),
5075            _ => None,
5076        }
5077    }
5078
5079    fn is_buffered_stage_candidate(&self, cmd_name: &str) -> bool {
5080        cmd_name == "bash"
5081            || cmd_name == "sh"
5082            || cmd_name == "builtin"
5083            || self.functions.contains_key(cmd_name)
5084            || self.builtins.is_builtin(cmd_name)
5085            || self.utils.is_utility(cmd_name)
5086            || self.external_handler.is_some()
5087    }
5088
5089    fn streaming_echo_bytes(args: &[String]) -> Vec<u8> {
5090        let mut suppress_newline = false;
5091        let mut interpret_escapes = false;
5092        let mut start = 0usize;
5093
5094        for (i, arg) in args.iter().enumerate() {
5095            let bytes = arg.as_bytes();
5096            if bytes.first() != Some(&b'-') || bytes.len() < 2 {
5097                break;
5098            }
5099            if !bytes[1..].iter().all(|b| matches!(b, b'n' | b'e')) {
5100                break;
5101            }
5102            for &byte in &bytes[1..] {
5103                match byte {
5104                    b'n' => suppress_newline = true,
5105                    b'e' => interpret_escapes = true,
5106                    _ => {}
5107                }
5108            }
5109            start = i + 1;
5110        }
5111
5112        let text = args[start..].join(" ");
5113        let rendered = if interpret_escapes {
5114            Self::process_streaming_echo_escapes(&text)
5115        } else {
5116            text
5117        };
5118        let mut output = rendered.into_bytes();
5119        if !suppress_newline {
5120            output.push(b'\n');
5121        }
5122        output
5123    }
5124
5125    fn process_streaming_echo_escapes(text: &str) -> String {
5126        let bytes = text.as_bytes();
5127        let mut output = String::new();
5128        let mut i = 0usize;
5129        while i < bytes.len() {
5130            if bytes[i] == b'\\' && i + 1 < bytes.len() {
5131                match bytes[i + 1] {
5132                    b'n' => output.push('\n'),
5133                    b't' => output.push('\t'),
5134                    b'r' => output.push('\r'),
5135                    b'\\' => output.push('\\'),
5136                    other => {
5137                        output.push('\\');
5138                        output.push(other as char);
5139                    }
5140                }
5141                i += 2;
5142            } else {
5143                output.push(bytes[i] as char);
5144                i += 1;
5145            }
5146        }
5147        output
5148    }
5149
5150    fn parse_streaming_cat_stage(
5151        args: &[String],
5152        is_first: bool,
5153    ) -> Option<StreamingPipelineStage> {
5154        let non_separator: Vec<&String> = args.iter().filter(|arg| arg.as_str() != "--").collect();
5155        if non_separator.iter().any(|arg| arg.starts_with('-')) {
5156            return None;
5157        }
5158        if is_first {
5159            if non_separator.len() == 1 {
5160                return Some(StreamingPipelineStage::File(non_separator[0].clone()));
5161            }
5162            return None;
5163        }
5164        Some(StreamingPipelineStage::Cat)
5165    }
5166
5167    fn parse_streaming_head_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5168        let mut mode = StreamingHeadMode::Lines(10);
5169        let mut files: Vec<&str> = Vec::new();
5170        let mut i = 0usize;
5171        while i < args.len() {
5172            i = Self::apply_streaming_head_arg(args, i, &mut mode, &mut files)?;
5173        }
5174        files
5175            .is_empty()
5176            .then_some(StreamingPipelineStage::Head(mode))
5177    }
5178
5179    fn apply_streaming_head_arg<'a>(
5180        args: &'a [String],
5181        i: usize,
5182        mode: &mut StreamingHeadMode,
5183        files: &mut Vec<&'a str>,
5184    ) -> Option<usize> {
5185        let arg = args[i].as_str();
5186        if arg == "--" {
5187            return Some(i + 1);
5188        }
5189        if arg == "-c" && i + 1 < args.len() {
5190            *mode = StreamingHeadMode::Bytes(args[i + 1].parse().ok()?);
5191            return Some(i + 2);
5192        }
5193        if arg == "-n" && i + 1 < args.len() {
5194            *mode = StreamingHeadMode::Lines(args[i + 1].parse().ok()?);
5195            return Some(i + 2);
5196        }
5197        if arg.starts_with('-') && arg.len() > 1 {
5198            *mode = StreamingHeadMode::Lines(arg[1..].parse().ok()?);
5199            return Some(i + 1);
5200        }
5201        files.push(arg);
5202        Some(i + 1)
5203    }
5204
5205    fn parse_streaming_tail_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5206        let mut mode = StreamingTailMode::Lines(10);
5207        let mut files: Vec<&str> = Vec::new();
5208        let mut i = 0usize;
5209        while i < args.len() {
5210            i = Self::apply_streaming_tail_arg(args, i, &mut mode, &mut files)?;
5211        }
5212        files
5213            .is_empty()
5214            .then_some(StreamingPipelineStage::Tail(mode))
5215    }
5216
5217    fn apply_streaming_tail_arg<'a>(
5218        args: &'a [String],
5219        i: usize,
5220        mode: &mut StreamingTailMode,
5221        files: &mut Vec<&'a str>,
5222    ) -> Option<usize> {
5223        let arg = args[i].as_str();
5224        if arg == "-f" {
5225            return None;
5226        }
5227        if arg == "--" {
5228            return Some(i + 1);
5229        }
5230        if arg == "-c" && i + 1 < args.len() {
5231            *mode = StreamingTailMode::Bytes(args[i + 1].parse().ok()?);
5232            return Some(i + 2);
5233        }
5234        if arg == "-n" && i + 1 < args.len() {
5235            *mode = Self::parse_streaming_tail_lines_value(&args[i + 1])?;
5236            return Some(i + 2);
5237        }
5238        if arg.starts_with('-') && arg.len() > 1 {
5239            *mode = StreamingTailMode::Lines(arg[1..].parse().ok()?);
5240            return Some(i + 1);
5241        }
5242        files.push(arg);
5243        Some(i + 1)
5244    }
5245
5246    fn parse_streaming_tail_lines_value(value: &str) -> Option<StreamingTailMode> {
5247        if value.starts_with('+') {
5248            return None;
5249        }
5250        Some(StreamingTailMode::Lines(value.parse().ok()?))
5251    }
5252
5253    fn parse_streaming_bat_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5254        let mut stage = StreamingBatStage {
5255            show_numbers: true,
5256            show_header: true,
5257            line_range: None,
5258            show_all: false,
5259        };
5260        let mut i = 0usize;
5261        while i < args.len() {
5262            let advance = Self::apply_streaming_bat_arg(args, i, &mut stage)?;
5263            i += advance;
5264        }
5265        Some(StreamingPipelineStage::Bat(stage))
5266    }
5267
5268    fn apply_streaming_bat_arg(
5269        args: &[String],
5270        i: usize,
5271        stage: &mut StreamingBatStage,
5272    ) -> Option<usize> {
5273        let arg = args[i].as_str();
5274        match arg {
5275            "-n" | "--number" => {
5276                stage.show_numbers = true;
5277                Some(1)
5278            }
5279            "-p" | "--plain" | "--style=plain" => {
5280                stage.show_numbers = false;
5281                stage.show_header = false;
5282                Some(1)
5283            }
5284            "-A" | "--show-all" => {
5285                stage.show_all = true;
5286                Some(1)
5287            }
5288            "-r" | "--line-range" if i + 1 < args.len() => {
5289                stage.line_range = Self::parse_streaming_bat_range(&args[i + 1]);
5290                Some(2)
5291            }
5292            "-l" | "--language" | "--paging" if i + 1 < args.len() => Some(2),
5293            "--style=numbers" => {
5294                stage.show_numbers = true;
5295                stage.show_header = false;
5296                Some(1)
5297            }
5298            "--style=header" => {
5299                stage.show_numbers = false;
5300                stage.show_header = true;
5301                Some(1)
5302            }
5303            "--" => (i + 1 == args.len()).then_some(1),
5304            _ => Self::apply_streaming_bat_long_or_short(arg, stage),
5305        }
5306    }
5307
5308    fn apply_streaming_bat_long_or_short(
5309        value: &str,
5310        stage: &mut StreamingBatStage,
5311    ) -> Option<usize> {
5312        if value.starts_with("--style=") {
5313            stage.show_numbers = true;
5314            stage.show_header = true;
5315            return Some(1);
5316        }
5317        if let Some(range_spec) = value.strip_prefix("--line-range=") {
5318            stage.line_range = Self::parse_streaming_bat_range(range_spec);
5319            return Some(1);
5320        }
5321        if value.starts_with("--paging=") || value.starts_with("--language=") {
5322            return Some(1);
5323        }
5324        if value.starts_with('-') && value.len() > 1 && !value.starts_with("--") {
5325            Self::apply_streaming_bat_short_cluster(&value[1..], stage)?;
5326            return Some(1);
5327        }
5328        None
5329    }
5330
5331    fn apply_streaming_bat_short_cluster(flags: &str, stage: &mut StreamingBatStage) -> Option<()> {
5332        for ch in flags.chars() {
5333            match ch {
5334                'n' => stage.show_numbers = true,
5335                'p' => {
5336                    stage.show_numbers = false;
5337                    stage.show_header = false;
5338                }
5339                'A' => stage.show_all = true,
5340                _ => return None,
5341            }
5342        }
5343        Some(())
5344    }
5345
5346    fn parse_streaming_bat_range(s: &str) -> Option<(Option<usize>, Option<usize>)> {
5347        if let Some((start, end)) = s.split_once(':') {
5348            let start = if start.is_empty() {
5349                None
5350            } else {
5351                start.parse().ok()
5352            };
5353            let end = if end.is_empty() {
5354                None
5355            } else {
5356                end.parse().ok()
5357            };
5358            Some((start, end))
5359        } else {
5360            let n = s.parse().ok()?;
5361            Some((Some(n), Some(n)))
5362        }
5363    }
5364
5365    fn parse_streaming_sed_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5366        let mut suppress_print = false;
5367        let mut expressions = Vec::new();
5368        let mut i = 0usize;
5369        while i < args.len() {
5370            let step =
5371                Self::apply_streaming_sed_arg(args, i, &mut suppress_print, &mut expressions)?;
5372            match step {
5373                StreamingSedStep::Advance(n) => i += n,
5374                StreamingSedStep::Break => break,
5375            }
5376        }
5377        if expressions.is_empty() {
5378            return None;
5379        }
5380        let script = expressions.join(";");
5381        let instructions = parse_streaming_sed_script(&script);
5382        if instructions.is_empty() {
5383            return None;
5384        }
5385        Some(StreamingPipelineStage::Sed(StreamingSedStage {
5386            suppress_print,
5387            instructions,
5388        }))
5389    }
5390
5391    fn apply_streaming_sed_arg(
5392        args: &[String],
5393        i: usize,
5394        suppress_print: &mut bool,
5395        expressions: &mut Vec<String>,
5396    ) -> Option<StreamingSedStep> {
5397        let arg = args[i].as_str();
5398        if arg == "-n" {
5399            *suppress_print = true;
5400            return Some(StreamingSedStep::Advance(1));
5401        }
5402        if arg == "-e" && i + 1 < args.len() {
5403            expressions.push(args[i + 1].clone());
5404            return Some(StreamingSedStep::Advance(2));
5405        }
5406        if arg == "-E" || arg == "-r" {
5407            return Some(StreamingSedStep::Advance(1));
5408        }
5409        if Self::streaming_sed_arg_rejected(arg) {
5410            return None;
5411        }
5412        if arg == "--" {
5413            return Self::streaming_sed_handle_doubledash(args, i, expressions);
5414        }
5415        if expressions.is_empty() {
5416            expressions.push(args[i].clone());
5417            Some(StreamingSedStep::Advance(1))
5418        } else {
5419            None
5420        }
5421    }
5422
5423    fn streaming_sed_arg_rejected(arg: &str) -> bool {
5424        arg == "-f"
5425            || arg == "-i"
5426            || arg.starts_with("-i")
5427            || (arg.starts_with('-') && arg.len() > 1 && arg != "--")
5428    }
5429
5430    fn streaming_sed_handle_doubledash(
5431        args: &[String],
5432        i: usize,
5433        expressions: &mut Vec<String>,
5434    ) -> Option<StreamingSedStep> {
5435        if i + 1 >= args.len() {
5436            return Some(StreamingSedStep::Break);
5437        }
5438        if !expressions.is_empty() {
5439            return None;
5440        }
5441        expressions.push(args[i + 1].clone());
5442        Some(StreamingSedStep::Advance(2))
5443    }
5444
5445    fn parse_streaming_paste_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5446        let mut delimiter = "\t".to_string();
5447        let mut serial = false;
5448        let mut i = 0usize;
5449        while i < args.len() {
5450            i = Self::apply_streaming_paste_arg(args, i, &mut delimiter, &mut serial)?;
5451        }
5452        Some(StreamingPipelineStage::Paste(StreamingPasteStage {
5453            delimiter,
5454            serial,
5455        }))
5456    }
5457
5458    fn apply_streaming_paste_arg(
5459        args: &[String],
5460        i: usize,
5461        delimiter: &mut String,
5462        serial: &mut bool,
5463    ) -> Option<usize> {
5464        let arg = args[i].as_str();
5465        if arg == "-d" && i + 1 < args.len() {
5466            delimiter.clone_from(&args[i + 1]);
5467            return Some(i + 2);
5468        }
5469        if arg == "-s" {
5470            *serial = true;
5471            return Some(i + 1);
5472        }
5473        if arg == "--" {
5474            return (i + 1 == args.len()).then_some(i + 1);
5475        }
5476        if arg.starts_with('-') && arg.len() > 1 {
5477            let extra = Self::apply_streaming_paste_short_cluster(args, i, delimiter, serial)?;
5478            return Some(i + 1 + extra);
5479        }
5480        None
5481    }
5482
5483    fn apply_streaming_paste_short_cluster(
5484        args: &[String],
5485        i: usize,
5486        delimiter: &mut String,
5487        serial: &mut bool,
5488    ) -> Option<usize> {
5489        let arg = args[i].as_str();
5490        let mut extra = 0usize;
5491        for ch in arg[1..].chars() {
5492            match ch {
5493                's' => *serial = true,
5494                'd' if i + 1 < args.len() => {
5495                    delimiter.clone_from(&args[i + 1]);
5496                    extra = 1;
5497                }
5498                _ => return None,
5499            }
5500        }
5501        Some(extra)
5502    }
5503
5504    fn parse_streaming_tee_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5505        let mut append = false;
5506        let mut paths = Vec::new();
5507        let mut i = 0usize;
5508        while i < args.len() {
5509            let arg = args[i].as_str();
5510            if arg == "-a" {
5511                append = true;
5512                i += 1;
5513            } else if arg == "-i" {
5514                i += 1;
5515            } else if arg == "--" {
5516                paths.extend(args[i + 1..].iter().cloned());
5517                break;
5518            } else if arg.starts_with('-') && arg.len() > 1 {
5519                for ch in arg[1..].chars() {
5520                    match ch {
5521                        'a' => append = true,
5522                        'i' => {}
5523                        _ => return None,
5524                    }
5525                }
5526                i += 1;
5527            } else {
5528                paths.push(args[i].clone());
5529                i += 1;
5530            }
5531        }
5532        Some(StreamingPipelineStage::Tee(StreamingTeeStage {
5533            append,
5534            paths,
5535        }))
5536    }
5537
5538    fn parse_streaming_column_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5539        let mut i = 0usize;
5540        while i < args.len() {
5541            let arg = args[i].as_str();
5542            if arg == "-t" {
5543                return None;
5544            }
5545            if arg == "-s" && i + 1 < args.len() {
5546                return None;
5547            }
5548            if arg.starts_with('-') && arg.len() > 1 {
5549                i += 1;
5550            } else if arg == "--" {
5551                if i + 1 != args.len() {
5552                    return None;
5553                }
5554                i += 1;
5555            } else {
5556                return None;
5557            }
5558        }
5559        Some(StreamingPipelineStage::Column(StreamingColumnStage))
5560    }
5561
5562    fn parse_streaming_rev_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5563        if args.iter().all(|arg| arg == "--") {
5564            Some(StreamingPipelineStage::Rev)
5565        } else {
5566            None
5567        }
5568    }
5569
5570    fn parse_streaming_grep_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5571        let mut flags = StreamingGrepFlags {
5572            ignore_case: false,
5573            invert: false,
5574            count_only: false,
5575            show_line_numbers: false,
5576            files_only: false,
5577            word_match: false,
5578            only_matching: false,
5579            quiet: false,
5580            extended: false,
5581            fixed: false,
5582            after_context: 0,
5583            before_context: 0,
5584            max_count: None,
5585            show_filename: None,
5586        };
5587        let mut patterns = Vec::new();
5588        let mut rest = Vec::new();
5589        let mut i = 0usize;
5590        while i < args.len() {
5591            let arg = args[i].as_str();
5592            if arg == "--" {
5593                rest.extend(args[i + 1..].iter().cloned());
5594                break;
5595            }
5596            if Self::streaming_grep_arg_rejected(arg) {
5597                return None;
5598            }
5599            match Self::parse_streaming_grep_value_flag(args, i, &mut flags, &mut patterns)? {
5600                StreamingGrepStep::Advance(delta) => {
5601                    i += delta;
5602                    continue;
5603                }
5604                StreamingGrepStep::NotMatched => {}
5605            }
5606            if arg.starts_with('-') && arg.len() > 1 {
5607                Self::apply_streaming_grep_short_flags(&arg[1..], &mut flags)?;
5608                i += 1;
5609            } else {
5610                rest.push(args[i].clone());
5611                i += 1;
5612            }
5613        }
5614
5615        let (patterns, file_args) = if patterns.is_empty() {
5616            let first = rest.first()?.clone();
5617            (vec![first], rest[1..].to_vec())
5618        } else {
5619            (patterns, rest)
5620        };
5621        if !file_args.is_empty() {
5622            return None;
5623        }
5624        Some(StreamingPipelineStage::Grep(StreamingGrepStage {
5625            flags,
5626            patterns,
5627        }))
5628    }
5629
5630    fn streaming_grep_arg_rejected(arg: &str) -> bool {
5631        arg.starts_with("--include=")
5632            || arg.starts_with("--exclude=")
5633            || arg == "--color"
5634            || arg.starts_with("--color=")
5635            || arg == "-r"
5636            || arg == "-R"
5637            || arg == "--recursive"
5638    }
5639
5640    fn parse_streaming_grep_value_flag(
5641        args: &[String],
5642        i: usize,
5643        flags: &mut StreamingGrepFlags,
5644        patterns: &mut Vec<String>,
5645    ) -> Option<StreamingGrepStep> {
5646        let arg = args[i].as_str();
5647        let has_next = i + 1 < args.len();
5648        if !has_next {
5649            return Some(StreamingGrepStep::NotMatched);
5650        }
5651        match arg {
5652            "-e" => {
5653                patterns.push(args[i + 1].clone());
5654                Some(StreamingGrepStep::Advance(2))
5655            }
5656            "-f" => None,
5657            "-A" => {
5658                flags.after_context = args[i + 1].parse().ok()?;
5659                Some(StreamingGrepStep::Advance(2))
5660            }
5661            "-B" => {
5662                flags.before_context = args[i + 1].parse().ok()?;
5663                Some(StreamingGrepStep::Advance(2))
5664            }
5665            "-C" => {
5666                let n = args[i + 1].parse().ok()?;
5667                flags.before_context = n;
5668                flags.after_context = n;
5669                Some(StreamingGrepStep::Advance(2))
5670            }
5671            "-m" => {
5672                flags.max_count = args[i + 1].parse().ok();
5673                Some(StreamingGrepStep::Advance(2))
5674            }
5675            _ => Some(StreamingGrepStep::NotMatched),
5676        }
5677    }
5678
5679    fn apply_streaming_grep_short_flags(
5680        short_flags: &str,
5681        flags: &mut StreamingGrepFlags,
5682    ) -> Option<()> {
5683        for ch in short_flags.chars() {
5684            match ch {
5685                'i' => flags.ignore_case = true,
5686                'v' => flags.invert = true,
5687                'c' => flags.count_only = true,
5688                'n' => flags.show_line_numbers = true,
5689                'l' => flags.files_only = true,
5690                'E' | 'P' => flags.extended = true,
5691                'F' => flags.fixed = true,
5692                'w' => flags.word_match = true,
5693                'o' => flags.only_matching = true,
5694                'q' => flags.quiet = true,
5695                'h' => flags.show_filename = Some(false),
5696                'H' => flags.show_filename = Some(true),
5697                'z' => {}
5698                _ => return None,
5699            }
5700        }
5701        Some(())
5702    }
5703
5704    fn parse_streaming_uniq_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5705        let mut flags = StreamingUniqFlags {
5706            count: false,
5707            duplicates_only: false,
5708            unique_only: false,
5709            ignore_case: false,
5710            skip_fields: 0,
5711            skip_chars: 0,
5712            compare_chars: None,
5713        };
5714        let mut i = 0usize;
5715        while i < args.len() {
5716            i = Self::apply_streaming_uniq_arg(args, i, &mut flags)?;
5717        }
5718        Some(StreamingPipelineStage::Uniq(flags))
5719    }
5720
5721    fn apply_streaming_uniq_arg(
5722        args: &[String],
5723        i: usize,
5724        flags: &mut StreamingUniqFlags,
5725    ) -> Option<usize> {
5726        let arg = args[i].as_str();
5727        if arg == "--" {
5728            return Some(i + 1);
5729        }
5730        if i + 1 < args.len() {
5731            match arg {
5732                "-f" => {
5733                    flags.skip_fields = args[i + 1].parse().ok()?;
5734                    return Some(i + 2);
5735                }
5736                "-s" => {
5737                    flags.skip_chars = args[i + 1].parse().ok()?;
5738                    return Some(i + 2);
5739                }
5740                "-w" => {
5741                    flags.compare_chars = args[i + 1].parse().ok();
5742                    return Some(i + 2);
5743                }
5744                _ => {}
5745            }
5746        }
5747        if arg.starts_with('-') && arg.len() > 1 {
5748            Self::apply_streaming_uniq_short_cluster(&arg[1..], flags)?;
5749            return Some(i + 1);
5750        }
5751        None
5752    }
5753
5754    fn apply_streaming_uniq_short_cluster(
5755        short_flags: &str,
5756        flags: &mut StreamingUniqFlags,
5757    ) -> Option<()> {
5758        for ch in short_flags.chars() {
5759            match ch {
5760                'c' => flags.count = true,
5761                'd' => flags.duplicates_only = true,
5762                'u' => flags.unique_only = true,
5763                'i' => flags.ignore_case = true,
5764                'z' => {}
5765                _ => return None,
5766            }
5767        }
5768        Some(())
5769    }
5770
5771    fn parse_streaming_cut_ranges(spec: &str) -> Vec<StreamingCutRange> {
5772        spec.split(',')
5773            .filter_map(|part| {
5774                if let Some((start, end)) = part.split_once('-') {
5775                    Some(StreamingCutRange {
5776                        start: if start.is_empty() {
5777                            None
5778                        } else {
5779                            start.parse().ok()
5780                        },
5781                        end: if end.is_empty() {
5782                            None
5783                        } else {
5784                            end.parse().ok()
5785                        },
5786                    })
5787                } else {
5788                    let n: usize = part.parse().ok()?;
5789                    Some(StreamingCutRange {
5790                        start: Some(n),
5791                        end: Some(n),
5792                    })
5793                }
5794            })
5795            .collect()
5796    }
5797
5798    fn parse_streaming_cut_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5799        let mut state = StreamingCutParseState {
5800            delim: '\t',
5801            mode: None,
5802            complement: false,
5803            only_delimited: false,
5804            output_delim: None,
5805        };
5806        let mut i = 0usize;
5807        while i < args.len() {
5808            i = Self::apply_streaming_cut_arg(args, i, &mut state)?;
5809        }
5810        Some(StreamingPipelineStage::Cut(StreamingCutStage {
5811            mode: state.mode?,
5812            delim: state.delim,
5813            complement: state.complement,
5814            only_delimited: state.only_delimited,
5815            output_delim: state
5816                .output_delim
5817                .unwrap_or_else(|| state.delim.to_string()),
5818        }))
5819    }
5820
5821    fn apply_streaming_cut_arg(
5822        args: &[String],
5823        i: usize,
5824        state: &mut StreamingCutParseState,
5825    ) -> Option<usize> {
5826        let arg = args[i].as_str();
5827        if let Some(advance) = Self::streaming_cut_try_mode_flag(args, i, &mut state.mode) {
5828            return Some(advance);
5829        }
5830        if let Some(advance) = Self::streaming_cut_try_delim_flag(args, i, &mut state.delim) {
5831            return Some(advance);
5832        }
5833        match arg {
5834            "--complement" => {
5835                state.complement = true;
5836                Some(i + 1)
5837            }
5838            "-s" => {
5839                state.only_delimited = true;
5840                Some(i + 1)
5841            }
5842            "-z" | "--" => Some(i + 1),
5843            _ => {
5844                if let Some(out) = arg.strip_prefix("--output-delimiter=") {
5845                    state.output_delim = Some(out.to_string());
5846                    Some(i + 1)
5847                } else {
5848                    None
5849                }
5850            }
5851        }
5852    }
5853
5854    fn streaming_cut_try_mode_flag(
5855        args: &[String],
5856        i: usize,
5857        mode: &mut Option<StreamingCutMode>,
5858    ) -> Option<usize> {
5859        let arg = args[i].as_str();
5860        let (flag, wrap): (&str, fn(Vec<StreamingCutRange>) -> StreamingCutMode) =
5861            if arg == "-f" || arg.starts_with("-f") {
5862                ("-f", StreamingCutMode::Fields)
5863            } else if arg == "-c" || arg.starts_with("-c") {
5864                ("-c", StreamingCutMode::Chars)
5865            } else if arg == "-b" || arg.starts_with("-b") {
5866                ("-b", StreamingCutMode::Bytes)
5867            } else {
5868                return None;
5869            };
5870        if arg == flag && i + 1 < args.len() {
5871            *mode = Some(wrap(Self::parse_streaming_cut_ranges(&args[i + 1])));
5872            return Some(i + 2);
5873        }
5874        if let Some(spec) = arg.strip_prefix(flag) {
5875            if !spec.is_empty() {
5876                *mode = Some(wrap(Self::parse_streaming_cut_ranges(spec)));
5877                return Some(i + 1);
5878            }
5879        }
5880        None
5881    }
5882
5883    fn streaming_cut_try_delim_flag(args: &[String], i: usize, delim: &mut char) -> Option<usize> {
5884        let arg = args[i].as_str();
5885        if arg == "-d" && i + 1 < args.len() {
5886            *delim = args[i + 1].chars().next().unwrap_or('\t');
5887            return Some(i + 2);
5888        }
5889        if arg.starts_with("-d") && arg.len() > 2 {
5890            *delim = arg[2..].chars().next().unwrap_or('\t');
5891            return Some(i + 1);
5892        }
5893        None
5894    }
5895
5896    fn parse_streaming_tr_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5897        let mut delete = false;
5898        let mut squeeze = false;
5899        let mut complement = false;
5900        let mut set_args = Vec::new();
5901        for arg in args {
5902            if arg.starts_with('-') && arg.len() > 1 {
5903                Self::apply_streaming_tr_flags(
5904                    &arg[1..],
5905                    &mut delete,
5906                    &mut squeeze,
5907                    &mut complement,
5908                )?;
5909            } else {
5910                set_args.push(arg.as_str());
5911            }
5912        }
5913        let from_chars = streaming_tr_expand_set(set_args.first()?);
5914        let to_chars = Self::streaming_tr_resolve_to_chars(&set_args, delete, squeeze)?;
5915        Some(StreamingPipelineStage::Tr(StreamingTrStage {
5916            delete,
5917            squeeze,
5918            complement,
5919            from_chars,
5920            to_chars,
5921        }))
5922    }
5923
5924    fn apply_streaming_tr_flags(
5925        flags: &str,
5926        delete: &mut bool,
5927        squeeze: &mut bool,
5928        complement: &mut bool,
5929    ) -> Option<()> {
5930        for ch in flags.chars() {
5931            match ch {
5932                'd' => *delete = true,
5933                's' => *squeeze = true,
5934                'c' | 'C' => *complement = true,
5935                't' => {}
5936                _ => return None,
5937            }
5938        }
5939        Some(())
5940    }
5941
5942    fn streaming_tr_resolve_to_chars(
5943        set_args: &[&str],
5944        delete: bool,
5945        squeeze: bool,
5946    ) -> Option<Vec<char>> {
5947        if delete {
5948            let to = if squeeze && set_args.len() >= 2 {
5949                streaming_tr_expand_set(set_args[1])
5950            } else {
5951                Vec::new()
5952            };
5953            return Some(to);
5954        }
5955        if squeeze && set_args.len() < 2 {
5956            return Some(Vec::new());
5957        }
5958        if set_args.len() < 2 {
5959            return None;
5960        }
5961        Some(streaming_tr_expand_set(set_args[1]))
5962    }
5963
5964    fn parse_streaming_wc_stage(args: &[String]) -> Option<StreamingPipelineStage> {
5965        let mut flags = StreamingWcFlags {
5966            lines: false,
5967            words: false,
5968            bytes: false,
5969            max_line_length: false,
5970        };
5971        let mut parsing_flags = true;
5972        for arg in args {
5973            if !Self::apply_streaming_wc_arg(arg, &mut flags, &mut parsing_flags)? {
5974                return None;
5975            }
5976        }
5977        if !flags.lines && !flags.words && !flags.bytes && !flags.max_line_length {
5978            flags.lines = true;
5979            flags.words = true;
5980            flags.bytes = true;
5981        }
5982        Some(StreamingPipelineStage::Wc(flags))
5983    }
5984
5985    fn apply_streaming_wc_arg(
5986        arg: &str,
5987        flags: &mut StreamingWcFlags,
5988        parsing_flags: &mut bool,
5989    ) -> Option<bool> {
5990        if *parsing_flags && arg == "--" {
5991            *parsing_flags = false;
5992            return Some(true);
5993        }
5994        if *parsing_flags && arg.starts_with('-') && arg.len() > 1 {
5995            Self::apply_streaming_wc_short_cluster(&arg[1..], flags)?;
5996            return Some(true);
5997        }
5998        Some(false)
5999    }
6000
6001    fn apply_streaming_wc_short_cluster(short: &str, flags: &mut StreamingWcFlags) -> Option<()> {
6002        for ch in short.chars() {
6003            match ch {
6004                'l' => flags.lines = true,
6005                'w' => flags.words = true,
6006                'c' | 'm' => flags.bytes = true,
6007                'L' => flags.max_line_length = true,
6008                _ => return None,
6009            }
6010        }
6011        Some(())
6012    }
6013
6014    fn set_pipestatus(&mut self, statuses: &[i32]) {
6015        let status_key = smol_str::SmolStr::from("PIPESTATUS");
6016        self.vm.state.init_indexed_array(status_key.clone());
6017        for (i, s) in statuses.iter().enumerate() {
6018            self.vm.state.set_array_element(
6019                status_key.clone(),
6020                &i.to_string(),
6021                smol_str::SmolStr::from(s.to_string()),
6022            );
6023        }
6024    }
6025
6026    fn open_streaming_file_reader(
6027        &mut self,
6028        path: &str,
6029        cmd_name: &str,
6030    ) -> Result<Box<dyn Read>, ()> {
6031        let resolved = self.resolve_cwd_path(path);
6032        match Self::open_streaming_file_reader_in_fs(&mut self.fs, &resolved) {
6033            Ok(reader) => Ok(reader),
6034            Err(err) => {
6035                let msg =
6036                    format!("wasmsh: {cmd_name}: failed to open stdin source {resolved}: {err}\n");
6037                self.write_stderr(msg.as_bytes());
6038                self.vm.state.last_status = 1;
6039                Err(())
6040            }
6041        }
6042    }
6043
6044    fn open_streaming_file_reader_in_fs(
6045        fs: &mut BackendFs,
6046        resolved: &str,
6047    ) -> Result<Box<dyn Read>, String> {
6048        let handle = fs
6049            .open(resolved, OpenOptions::read())
6050            .map_err(|err| err.to_string())?;
6051        let reader_result = fs.stream_file(handle).map_err(|err| err.to_string());
6052        fs.close(handle);
6053        reader_result
6054    }
6055
6056    fn execute_inner_capture_stdout(&mut self, input: &str) -> Vec<u8> {
6057        let events = self.execute_isolated_input_events(input, None);
6058        let mut stdout = Vec::new();
6059        for event in events {
6060            match event {
6061                WorkerEvent::Stdout(data) => stdout.extend_from_slice(&data),
6062                WorkerEvent::Stderr(data) => self.write_stderr(&data),
6063                WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
6064                    convert_diag_level(level),
6065                    wasmsh_vm::DiagCategory::Runtime,
6066                    msg,
6067                ),
6068                _ => {}
6069            }
6070        }
6071        stdout
6072    }
6073
6074    fn execute_isolated_input_events(
6075        &mut self,
6076        input: &str,
6077        pending_input: Option<InputTarget>,
6078    ) -> Vec<WorkerEvent> {
6079        let saved_state = self.vm.state.clone();
6080        let saved_functions = self.functions.clone();
6081        let saved_aliases = self.aliases.clone();
6082        let saved_exec = self.exec.clone();
6083        let saved_exec_io = self.current_exec_io.take();
6084        let saved_stdout = std::mem::take(&mut self.vm.stdout);
6085        let saved_stderr = std::mem::take(&mut self.vm.stderr);
6086        let saved_diagnostics = std::mem::take(&mut self.vm.diagnostics);
6087        let saved_output_bytes = self.vm.output_bytes;
6088        let saved_proc_subst_out_scopes = std::mem::take(&mut self.proc_subst_out_scopes);
6089        let saved_proc_subst_in_scopes = std::mem::take(&mut self.proc_subst_in_scopes);
6090
6091        self.current_exec_io = pending_input.map(|target| {
6092            let mut exec_io = ExecIo::default();
6093            exec_io.fds_mut().set_input(target);
6094            exec_io
6095        });
6096        let (mut inner_events, captured) = self.with_output_capture(true, true, |runtime| {
6097            runtime.with_nested_shell_scope(|nested| nested.execute_input_inner(input))
6098        });
6099        let inner_resource_exhausted = self.exec.resource_exhausted;
6100        let inner_diagnostics = self
6101            .vm
6102            .diagnostics
6103            .drain(..)
6104            .map(|diag| {
6105                WorkerEvent::Diagnostic(Self::to_protocol_diag_level(diag.level), diag.message)
6106            })
6107            .collect::<Vec<_>>();
6108        self.clear_pending_input();
6109        for scope in self.proc_subst_out_scopes.drain(..) {
6110            for sink in scope {
6111                let _ = self.fs.remove_file(&sink.path);
6112            }
6113        }
6114        for scope in self.proc_subst_in_scopes.drain(..) {
6115            for sink in scope {
6116                let _ = self.fs.remove_file(&sink.path);
6117            }
6118        }
6119
6120        self.vm.state = saved_state;
6121        self.functions = saved_functions;
6122        self.aliases = saved_aliases;
6123        self.exec = saved_exec;
6124        self.exec.resource_exhausted |= inner_resource_exhausted;
6125        self.current_exec_io = saved_exec_io;
6126        self.vm.stdout = saved_stdout;
6127        self.vm.stderr = saved_stderr;
6128        self.vm.diagnostics = saved_diagnostics;
6129        self.vm.output_bytes = saved_output_bytes;
6130        self.vm.budget.visible_output_bytes = saved_output_bytes;
6131        self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
6132        self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
6133
6134        let mut events = Self::seed_isolated_events_from_capture(captured);
6135        Self::merge_isolated_inner_events(&mut events, inner_events.drain(..));
6136        events.extend(inner_diagnostics);
6137        events
6138    }
6139
6140    fn seed_isolated_events_from_capture(capture: CapturedOutput) -> Vec<WorkerEvent> {
6141        let mut events = Vec::new();
6142        if !capture.stdout.is_empty() {
6143            events.push(WorkerEvent::Stdout(capture.stdout));
6144        }
6145        if !capture.stderr.is_empty() {
6146            events.push(WorkerEvent::Stderr(capture.stderr));
6147        }
6148        events
6149    }
6150
6151    fn merge_isolated_inner_events(
6152        events: &mut Vec<WorkerEvent>,
6153        inner_events: impl IntoIterator<Item = WorkerEvent>,
6154    ) {
6155        for event in inner_events {
6156            match &event {
6157                WorkerEvent::Stdout(_)
6158                    if !events.iter().any(|e| matches!(e, WorkerEvent::Stdout(_))) =>
6159                {
6160                    events.push(event);
6161                }
6162                WorkerEvent::Stderr(_)
6163                    if !events.iter().any(|e| matches!(e, WorkerEvent::Stderr(_))) =>
6164                {
6165                    events.push(event);
6166                }
6167                WorkerEvent::Stdout(_) | WorkerEvent::Stderr(_) => {}
6168                _ => events.push(event),
6169            }
6170        }
6171    }
6172
6173    fn execute_isolated_scheduled_pipeline_events_from_reader(
6174        &mut self,
6175        pipeline: &HirPipeline,
6176        reader: Box<dyn Read>,
6177    ) -> Vec<WorkerEvent> {
6178        let saved_state = self.vm.state.clone();
6179        let saved_functions = self.functions.clone();
6180        let saved_aliases = self.aliases.clone();
6181        let saved_exec = self.exec.clone();
6182        let saved_exec_io = self.current_exec_io.take();
6183        let saved_stdout = std::mem::take(&mut self.vm.stdout);
6184        let saved_stderr = std::mem::take(&mut self.vm.stderr);
6185        let saved_diagnostics = std::mem::take(&mut self.vm.diagnostics);
6186        let saved_output_bytes = self.vm.output_bytes;
6187        let saved_proc_subst_out_scopes = std::mem::take(&mut self.proc_subst_out_scopes);
6188        let saved_proc_subst_in_scopes = std::mem::take(&mut self.proc_subst_in_scopes);
6189
6190        self.current_exec_io = None;
6191        self.proc_subst_out_scopes.clear();
6192        self.proc_subst_in_scopes.clear();
6193        self.exec.recursion_depth += 1;
6194        if let Err(reason) = self
6195            .vm
6196            .budget
6197            .enter_recursion(self.vm.limits.recursion_limit)
6198        {
6199            self.exec.recursion_depth -= 1;
6200            self.vm.state = saved_state;
6201            self.functions = saved_functions;
6202            self.aliases = saved_aliases;
6203            self.exec = saved_exec;
6204            self.current_exec_io = saved_exec_io;
6205            self.vm.stdout = saved_stdout;
6206            self.vm.stderr = saved_stderr;
6207            self.vm.diagnostics = saved_diagnostics;
6208            self.vm.output_bytes = saved_output_bytes;
6209            self.vm.budget.visible_output_bytes = saved_output_bytes;
6210            self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
6211            self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
6212            self.mark_budget_exhaustion(reason);
6213            return vec![WorkerEvent::Stderr(
6214                b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
6215            )];
6216        }
6217
6218        let ((), captured) = self.with_output_capture(true, true, |runtime| {
6219            runtime.with_nested_shell_scope(|nested| {
6220                nested.execute_scheduled_pipeline_with_source_reader(
6221                    &pipeline.commands,
6222                    pipeline,
6223                    Some(reader),
6224                );
6225            });
6226        });
6227        self.exec.recursion_depth -= 1;
6228        self.vm.budget.exit_recursion();
6229        let inner_resource_exhausted = self.exec.resource_exhausted;
6230        let inner_diagnostics = self
6231            .vm
6232            .diagnostics
6233            .drain(..)
6234            .map(|diag| {
6235                WorkerEvent::Diagnostic(Self::to_protocol_diag_level(diag.level), diag.message)
6236            })
6237            .collect::<Vec<_>>();
6238        self.clear_pending_input();
6239        let pending_scopes: Vec<Vec<PendingProcessSubstOut>> =
6240            self.proc_subst_out_scopes.drain(..).collect();
6241        for scope in pending_scopes {
6242            for sink in scope {
6243                self.flush_process_subst_out(sink);
6244            }
6245        }
6246        let pending_in_scopes: Vec<Vec<PendingProcessSubstIn>> =
6247            self.proc_subst_in_scopes.drain(..).collect();
6248        for scope in pending_in_scopes {
6249            self.flush_process_subst_in_scope(scope);
6250        }
6251
6252        self.vm.state = saved_state;
6253        self.functions = saved_functions;
6254        self.aliases = saved_aliases;
6255        self.exec = saved_exec;
6256        self.exec.resource_exhausted |= inner_resource_exhausted;
6257        self.current_exec_io = saved_exec_io;
6258        self.vm.stdout = saved_stdout;
6259        self.vm.stderr = saved_stderr;
6260        self.vm.diagnostics = saved_diagnostics;
6261        self.vm.output_bytes = saved_output_bytes;
6262        self.vm.budget.visible_output_bytes = saved_output_bytes;
6263        self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
6264        self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
6265
6266        let mut events = Vec::new();
6267        if !captured.stdout.is_empty() {
6268            events.push(WorkerEvent::Stdout(captured.stdout));
6269        }
6270        if !captured.stderr.is_empty() {
6271            events.push(WorkerEvent::Stderr(captured.stderr));
6272        }
6273        events.extend(inner_diagnostics);
6274        events
6275    }
6276
6277    /// Execute a command substitution and return the trimmed output.
6278    fn execute_subst(&mut self, inner: &str) -> smol_str::SmolStr {
6279        let stdout = self.execute_inner_capture_stdout(inner);
6280        let result = String::from_utf8_lossy(&stdout).to_string();
6281        smol_str::SmolStr::from(result.trim_end_matches('\n'))
6282    }
6283
6284    fn word_parts_require_runtime_expansion(parts: &[WordPart]) -> bool {
6285        parts.iter().any(|part| match part {
6286            WordPart::Literal(_) | WordPart::SingleQuoted(_) => false,
6287            WordPart::DoubleQuoted(inner) => Self::word_parts_require_runtime_expansion(inner),
6288            WordPart::Parameter(_)
6289            | WordPart::Arithmetic(_)
6290            | WordPart::CommandSubstitution(_)
6291            | WordPart::ProcessSubstIn(_)
6292            | WordPart::ProcessSubstOut(_)
6293            | _ => true,
6294        })
6295    }
6296
6297    fn command_requires_runtime_expansion(cmd: &HirCommand) -> bool {
6298        let HirCommand::Exec(exec) = cmd else {
6299            return false;
6300        };
6301        exec.argv
6302            .iter()
6303            .any(|word| Self::word_parts_require_runtime_expansion(&word.parts))
6304    }
6305
6306    fn command_needs_full_single_stage_execution(&self, cmd: &HirCommand) -> bool {
6307        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
6308            return true;
6309        }
6310        let HirCommand::Exec(exec) = cmd else {
6311            return false;
6312        };
6313        exec.argv.iter().any(Self::word_has_brace_or_glob_literal)
6314    }
6315
6316    fn word_has_brace_or_glob_literal(word: &Word) -> bool {
6317        word.parts
6318            .iter()
6319            .any(Self::word_part_has_brace_or_glob_literal)
6320    }
6321
6322    fn word_part_has_brace_or_glob_literal(part: &WordPart) -> bool {
6323        match part {
6324            WordPart::Literal(text) | WordPart::SingleQuoted(text) | WordPart::Parameter(text) => {
6325                Self::text_has_brace_or_glob_literal(text)
6326            }
6327            WordPart::DoubleQuoted(parts) => {
6328                parts.iter().any(Self::word_part_has_brace_or_glob_literal)
6329            }
6330            WordPart::Arithmetic(_) => false,
6331            WordPart::CommandSubstitution(_)
6332            | WordPart::ProcessSubstIn(_)
6333            | WordPart::ProcessSubstOut(_)
6334            | _ => true,
6335        }
6336    }
6337
6338    fn text_has_brace_or_glob_literal(text: &str) -> bool {
6339        text.contains('{')
6340            || text.contains('}')
6341            || text.contains('*')
6342            || text.contains('?')
6343            || text.contains('[')
6344    }
6345
6346    fn parse_single_pipeline_input(input: &str) -> Option<HirPipeline> {
6347        let ast = wasmsh_parse::parse(input).ok()?;
6348        let hir = wasmsh_hir::lower(&ast);
6349        let cc = hir.items.first()?;
6350        if hir.items.len() != 1 || cc.list.len() != 1 {
6351            return None;
6352        }
6353        let and_or = cc.list.first()?;
6354        if !and_or.rest.is_empty() {
6355            return None;
6356        }
6357        Some(and_or.first.clone())
6358    }
6359
6360    /// Counter for generating unique temp file paths for process substitution.
6361    fn next_proc_subst_id() -> u64 {
6362        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
6363        COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
6364    }
6365
6366    fn next_pending_input_id() -> u64 {
6367        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
6368        COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
6369    }
6370
6371    fn set_pending_input_bytes(&mut self, data: Vec<u8>) {
6372        self.current_exec_io
6373            .get_or_insert_with(ExecIo::default)
6374            .fds_mut()
6375            .set_input(InputTarget::Bytes(data));
6376    }
6377
6378    fn set_pending_input_file(&mut self, path: String, remove_after_read: bool) {
6379        self.current_exec_io
6380            .get_or_insert_with(ExecIo::default)
6381            .fds_mut()
6382            .set_input(InputTarget::File {
6383                path,
6384                remove_after_read,
6385            });
6386    }
6387
6388    fn clear_pending_input(&mut self) {
6389        let Some(exec_io) = self.current_exec_io.as_mut() else {
6390            return;
6391        };
6392        if let InputTarget::File {
6393            path,
6394            remove_after_read: true,
6395        } = exec_io.take_stdin()
6396        {
6397            let _ = self.fs.remove_file(&path);
6398        }
6399    }
6400
6401    fn take_pending_input_reader(&mut self, cmd_name: &str) -> Result<Option<Box<dyn Read>>, ()> {
6402        let Some(exec_io) = self.current_exec_io.as_mut() else {
6403            return Ok(None);
6404        };
6405        match exec_io.take_stdin() {
6406            InputTarget::Inherit | InputTarget::Closed => Ok(None),
6407            InputTarget::Bytes(data) => Ok(Some(Box::new(Cursor::new(data)))),
6408            InputTarget::File {
6409                path,
6410                remove_after_read,
6411            } => {
6412                let reader_result = self.open_streaming_file_reader(&path, cmd_name);
6413                if remove_after_read {
6414                    let _ = self.fs.remove_file(&path);
6415                }
6416                reader_result.map(Some)
6417            }
6418            InputTarget::Pipe(pipe) => Ok(Some(Box::new(PipeReader::new(pipe)))),
6419        }
6420    }
6421
6422    fn take_builtin_stdin(
6423        &mut self,
6424        cmd_name: &str,
6425    ) -> Result<Option<wasmsh_builtins::BuiltinStdin<'static>>, ()> {
6426        let reader = self.take_pending_input_reader(cmd_name)?;
6427        Ok(reader.map(wasmsh_builtins::BuiltinStdin::from_reader))
6428    }
6429
6430    fn take_util_stdin(
6431        &mut self,
6432        cmd_name: &str,
6433    ) -> Result<Option<wasmsh_utils::UtilStdin<'static>>, ()> {
6434        let reader = self.take_pending_input_reader(cmd_name)?;
6435        Ok(reader.map(wasmsh_utils::UtilStdin::from_reader))
6436    }
6437
6438    fn take_external_stdin(
6439        &mut self,
6440        cmd_name: &str,
6441    ) -> Result<Option<ExternalCommandStdin<'static>>, ()> {
6442        let reader = self.take_pending_input_reader(cmd_name)?;
6443        Ok(reader.map(ExternalCommandStdin::from_reader))
6444    }
6445
6446    fn can_use_isolated_process_subst_runtime(&self) -> bool {
6447        self.external_handler.is_none() && self.network.is_none()
6448    }
6449
6450    fn clone_for_isolated_process_subst(&self) -> Option<Self> {
6451        if !self.can_use_isolated_process_subst_runtime() {
6452            return None;
6453        }
6454        let mut exec = ExecState::new();
6455        exec.recursion_depth = self.exec.recursion_depth;
6456        Some(Self {
6457            config: self.config.clone(),
6458            vm: Vm::with_limits(self.vm.state.clone(), self.vm.limits.clone()),
6459            fs: self.fs.clone(),
6460            utils: UtilRegistry::new(),
6461            builtins: wasmsh_builtins::BuiltinRegistry::new(),
6462            initialized: self.initialized,
6463            current_exec_io: None,
6464            proc_subst_out_scopes: Vec::new(),
6465            proc_subst_in_scopes: Vec::new(),
6466            functions: self.functions.clone(),
6467            exec,
6468            aliases: self.aliases.clone(),
6469            external_handler: None,
6470            network: None,
6471            active_run: None,
6472            pending_signals: VecDeque::new(),
6473        })
6474    }
6475
6476    fn build_live_process_subst_pipeline(
6477        &mut self,
6478        pipeline: &HirPipeline,
6479        source_pipe: Option<Rc<RefCell<PipeBuffer>>>,
6480    ) -> Option<(
6481        Vec<StreamingPipeProcess<'static>>,
6482        Vec<Rc<RefCell<Vec<u8>>>>,
6483        Vec<bool>,
6484        Rc<RefCell<PipeBuffer>>,
6485        Vec<Rc<RefCell<i32>>>,
6486    )> {
6487        let stages: Vec<StreamingPipelineStage> = pipeline
6488            .commands
6489            .iter()
6490            .enumerate()
6491            .map(|(idx, cmd)| self.compile_pipeline_stage(cmd, idx == 0 && source_pipe.is_none()))
6492            .collect();
6493        let stage_statuses: Vec<Rc<RefCell<i32>>> = stages
6494            .iter()
6495            .map(|stage| {
6496                Rc::new(RefCell::new(i32::from(matches!(
6497                    stage,
6498                    StreamingPipelineStage::Grep(_)
6499                ))))
6500            })
6501            .collect();
6502        let stage_stderr: Vec<Rc<RefCell<Vec<u8>>>> = stages
6503            .iter()
6504            .map(|_| Rc::new(RefCell::new(Vec::new())))
6505            .collect();
6506        let stage_pipe_stderr = vec![false; stages.len()];
6507        let output_pipes: Vec<Rc<RefCell<PipeBuffer>>> = (0..stages.len())
6508            .map(|_| Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY))))
6509            .collect();
6510        let mut processes = Vec::new();
6511
6512        let ctx = StreamingStageCtx {
6513            stages: &stages,
6514            stage_pipe_stderr: &stage_pipe_stderr,
6515            stage_statuses: &stage_statuses,
6516            stage_stderr: &stage_stderr,
6517            output_pipes: &output_pipes,
6518        };
6519
6520        self.setup_process_subst_stages(source_pipe, &ctx, &mut processes)?;
6521
6522        let final_pipe = output_pipes.last().cloned()?;
6523        Some((
6524            processes,
6525            stage_stderr,
6526            stage_pipe_stderr,
6527            final_pipe,
6528            stage_statuses,
6529        ))
6530    }
6531
6532    /// Wire the first stage (with or without an upstream pipe) and then every
6533    /// subsequent stage of a process-substitution pipeline. Extracted from
6534    /// `build_live_process_subst_pipeline` to keep its cognitive complexity
6535    /// under the project threshold.
6536    fn setup_process_subst_stages(
6537        &mut self,
6538        source_pipe: Option<Rc<RefCell<PipeBuffer>>>,
6539        ctx: &StreamingStageCtx<'_>,
6540        processes: &mut Vec<StreamingPipeProcess<'static>>,
6541    ) -> Option<()> {
6542        if let Some(source_pipe) = source_pipe {
6543            self.setup_process_subst_first_stage_from_pipe(source_pipe, ctx, processes)?;
6544        } else {
6545            self.setup_process_subst_first_stage_standalone(ctx, processes)?;
6546        }
6547        for idx in 1..ctx.stages.len() {
6548            self.setup_process_subst_later_stage(idx, ctx, processes)?;
6549        }
6550        Some(())
6551    }
6552
6553    /// First stage of a process-substitution pipeline when there is an upstream
6554    /// pipe feeding it. Returns `None` if wrapping is required but fails, which
6555    /// aborts pipeline construction.
6556    fn setup_process_subst_first_stage_from_pipe(
6557        &mut self,
6558        source_pipe: Rc<RefCell<PipeBuffer>>,
6559        ctx: &StreamingStageCtx<'_>,
6560        processes: &mut Vec<StreamingPipeProcess<'static>>,
6561    ) -> Option<()> {
6562        match &ctx.stages[0] {
6563            StreamingPipelineStage::Tee(stage) => {
6564                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
6565                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
6566                    reader,
6567                    ctx.output_pipes[0].clone(),
6568                    &mut self.fs,
6569                    self.vm.state.cwd.as_str(),
6570                    stage,
6571                    ctx.stage_stderr[0].clone(),
6572                    ctx.stage_statuses[0].clone(),
6573                    false,
6574                )));
6575            }
6576            StreamingPipelineStage::BufferedCommand(argv) => {
6577                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
6578                    Some(source_pipe),
6579                    ctx.output_pipes[0].clone(),
6580                    argv.clone(),
6581                    false,
6582                    ctx.stage_stderr[0].clone(),
6583                    ctx.stage_statuses[0].clone(),
6584                )));
6585            }
6586            _ => {
6587                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
6588                let stage_reader = Self::wrap_non_tee_streaming_stage(
6589                    reader,
6590                    &ctx.stages[0],
6591                    0,
6592                    ctx.stage_statuses,
6593                )?;
6594                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6595                    stage_reader,
6596                    ctx.output_pipes[0].clone(),
6597                    ctx.stage_stderr[0].clone(),
6598                    ctx.stage_statuses[0].clone(),
6599                    "process-subst",
6600                    false,
6601                )));
6602            }
6603        }
6604        Some(())
6605    }
6606
6607    /// First stage of a process-substitution pipeline with no upstream pipe —
6608    /// the stage itself supplies the initial data. Returns `None` for stages
6609    /// that cannot produce output without an input pipe.
6610    fn setup_process_subst_first_stage_standalone(
6611        &mut self,
6612        ctx: &StreamingStageCtx<'_>,
6613        processes: &mut Vec<StreamingPipeProcess<'static>>,
6614    ) -> Option<()> {
6615        let reader: Box<dyn Read> = match &ctx.stages[0] {
6616            StreamingPipelineStage::Literal(data) => Box::new(Cursor::new(data.clone())),
6617            StreamingPipelineStage::File(path) => {
6618                let resolved = self.resolve_cwd_path(path);
6619                self.open_streaming_file_reader(&resolved, "cat").ok()?
6620            }
6621            StreamingPipelineStage::Yes { line } => {
6622                Box::new(YesStreamReader::new(line.clone(), STREAMING_YES_MAX_LINES))
6623            }
6624            StreamingPipelineStage::BufferedCommand(argv) => {
6625                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
6626                    None,
6627                    ctx.output_pipes[0].clone(),
6628                    argv.clone(),
6629                    false,
6630                    ctx.stage_stderr[0].clone(),
6631                    ctx.stage_statuses[0].clone(),
6632                )));
6633                return Some(());
6634            }
6635            _ => return None,
6636        };
6637        processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6638            reader,
6639            ctx.output_pipes[0].clone(),
6640            ctx.stage_stderr[0].clone(),
6641            ctx.stage_statuses[0].clone(),
6642            "process-subst",
6643            false,
6644        )));
6645        Some(())
6646    }
6647
6648    /// Subsequent stage (idx > 0) of a process-substitution pipeline. Each
6649    /// stage reads from the previous stage's output pipe.
6650    fn setup_process_subst_later_stage(
6651        &mut self,
6652        idx: usize,
6653        ctx: &StreamingStageCtx<'_>,
6654        processes: &mut Vec<StreamingPipeProcess<'static>>,
6655    ) -> Option<()> {
6656        match &ctx.stages[idx] {
6657            StreamingPipelineStage::Head(mode) => {
6658                processes.push(StreamingPipeProcess::Head(HeadPipeProcess::new(
6659                    ctx.output_pipes[idx - 1].clone(),
6660                    ctx.output_pipes[idx].clone(),
6661                    *mode,
6662                )));
6663            }
6664            StreamingPipelineStage::Tee(stage) => {
6665                let reader =
6666                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
6667                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
6668                    reader,
6669                    ctx.output_pipes[idx].clone(),
6670                    &mut self.fs,
6671                    self.vm.state.cwd.as_str(),
6672                    stage,
6673                    ctx.stage_stderr[idx].clone(),
6674                    ctx.stage_statuses[idx].clone(),
6675                    false,
6676                )));
6677            }
6678            StreamingPipelineStage::BufferedCommand(argv) => {
6679                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
6680                    Some(ctx.output_pipes[idx - 1].clone()),
6681                    ctx.output_pipes[idx].clone(),
6682                    argv.clone(),
6683                    false,
6684                    ctx.stage_stderr[idx].clone(),
6685                    ctx.stage_statuses[idx].clone(),
6686                )));
6687            }
6688            _ => {
6689                let reader =
6690                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
6691                let stage_reader = Self::wrap_non_tee_streaming_stage(
6692                    reader,
6693                    &ctx.stages[idx],
6694                    idx,
6695                    ctx.stage_statuses,
6696                )?;
6697                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6698                    stage_reader,
6699                    ctx.output_pipes[idx].clone(),
6700                    ctx.stage_stderr[idx].clone(),
6701                    ctx.stage_statuses[idx].clone(),
6702                    "process-subst",
6703                    false,
6704                )));
6705            }
6706        }
6707        Some(())
6708    }
6709
6710    fn try_build_live_process_subst_in_reader(
6711        &mut self,
6712        inner: &str,
6713    ) -> Option<(
6714        Box<dyn Read>,
6715        Rc<RefCell<Vec<u8>>>,
6716        Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>,
6717    )> {
6718        let pipeline = Self::parse_single_pipeline_input(inner)?;
6719        let requires_runtime = pipeline.commands.iter().enumerate().any(|(idx, cmd)| {
6720            matches!(
6721                self.compile_pipeline_stage(cmd, idx == 0),
6722                StreamingPipelineStage::BufferedCommand(_)
6723            )
6724        });
6725        let mut isolated_runtime = if requires_runtime {
6726            self.clone_for_isolated_process_subst().map(Box::new)
6727        } else {
6728            None
6729        };
6730        let (processes, stage_stderr, stage_pipe_stderr, final_pipe, _) =
6731            if let Some(runtime) = isolated_runtime.as_mut() {
6732                runtime.build_live_process_subst_pipeline(&pipeline, None)?
6733            } else {
6734                if requires_runtime {
6735                    return None;
6736                }
6737                self.build_live_process_subst_pipeline(&pipeline, None)?
6738            };
6739
6740        let flushed_stderr = Rc::new(RefCell::new(Vec::new()));
6741        let flushed_diagnostics = Rc::new(RefCell::new(Vec::new()));
6742        let reader = LiveProcessSubstInReader {
6743            isolated_runtime,
6744            processes,
6745            finished: vec![false; stage_stderr.len()],
6746            final_pipe,
6747            stage_stderr,
6748            stage_pipe_stderr,
6749            flushed_stderr: flushed_stderr.clone(),
6750            flushed_diagnostics: flushed_diagnostics.clone(),
6751            done: false,
6752        };
6753        Some((Box::new(reader), flushed_stderr, flushed_diagnostics))
6754    }
6755
6756    /// Execute `<(cmd)` by registering a command-scoped readable path.
6757    fn execute_process_subst_in(&mut self, inner: &str) -> smol_str::SmolStr {
6758        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
6759        if self.proc_subst_in_scopes.is_empty() {
6760            self.proc_subst_in_scopes.push(Vec::new());
6761        }
6762
6763        if let Some((reader, stderr, diagnostics)) =
6764            self.try_build_live_process_subst_in_reader(inner)
6765        {
6766            if self.fs.install_stream_reader(&path, reader).is_ok() {
6767                self.proc_subst_in_scopes
6768                    .last_mut()
6769                    .expect("process substitution input scope stack is empty")
6770                    .push(PendingProcessSubstIn {
6771                        path: path.clone(),
6772                        stderr: Some(stderr),
6773                        diagnostics: Some(diagnostics),
6774                    });
6775                return smol_str::SmolStr::from(path);
6776            }
6777        }
6778
6779        let output = self.execute_inner_capture_stdout(inner);
6780        if let Ok(h) = self.fs.open(&path, OpenOptions::write()) {
6781            let _ = self.fs.write_file(h, &output);
6782            self.fs.close(h);
6783        }
6784        self.proc_subst_in_scopes
6785            .last_mut()
6786            .expect("process substitution input scope stack is empty")
6787            .push(PendingProcessSubstIn {
6788                path: path.clone(),
6789                stderr: None,
6790                diagnostics: None,
6791            });
6792        smol_str::SmolStr::from(path)
6793    }
6794
6795    fn try_build_live_process_subst_runner(
6796        &mut self,
6797        inner: &str,
6798    ) -> Option<LiveProcessSubstRunner> {
6799        let pipeline = Self::parse_single_pipeline_input(inner)?;
6800        let source_pipe = Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY)));
6801        let mut isolated_runtime = self.clone_for_isolated_process_subst();
6802        let (processes, stage_stderr, stage_pipe_stderr, final_pipe, _) =
6803            if let Some(runtime) = isolated_runtime.as_mut() {
6804                runtime.build_live_process_subst_pipeline(&pipeline, Some(source_pipe.clone()))?
6805            } else {
6806                self.build_live_process_subst_pipeline(&pipeline, Some(source_pipe.clone()))?
6807            };
6808
6809        Some(LiveProcessSubstRunner {
6810            isolated_runtime: isolated_runtime.map(Box::new),
6811            source_pipe,
6812            processes,
6813            finished: vec![false; stage_stderr.len()],
6814            final_pipe,
6815            stage_stderr,
6816            stage_pipe_stderr,
6817            captured_stdout: Vec::new(),
6818            captured_stderr: Vec::new(),
6819            captured_diagnostics: Vec::new(),
6820            done: false,
6821            synced_steps: self.vm.steps,
6822        })
6823    }
6824
6825    fn register_process_subst_out(&mut self, inner: &str) -> String {
6826        if self.proc_subst_out_scopes.is_empty() {
6827            self.proc_subst_out_scopes.push(Vec::new());
6828        }
6829        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
6830        let mode = if let Some(runner) = self.try_build_live_process_subst_runner(inner) {
6831            PendingProcessSubstOutMode::Live { runner }
6832        } else {
6833            PendingProcessSubstOutMode::Buffered { data: Vec::new() }
6834        };
6835        self.proc_subst_out_scopes
6836            .last_mut()
6837            .expect("process substitution scope stack is empty")
6838            .push(PendingProcessSubstOut {
6839                path: path.clone(),
6840                inner: inner.to_string(),
6841                mode,
6842            });
6843        path
6844    }
6845
6846    fn flush_process_subst_out_scope(&mut self, scope: Vec<PendingProcessSubstOut>) {
6847        for sink in scope {
6848            self.flush_process_subst_out(sink);
6849        }
6850    }
6851
6852    fn flush_process_subst_in_scope(&mut self, scope: Vec<PendingProcessSubstIn>) {
6853        for sink in scope {
6854            if let Some(stderr) = sink.stderr {
6855                let data = stderr.borrow();
6856                if !data.is_empty() {
6857                    self.write_stderr(&data);
6858                }
6859            }
6860            if let Some(diagnostics) = sink.diagnostics {
6861                let mut diagnostics = diagnostics.borrow_mut();
6862                for event in diagnostics.drain(..) {
6863                    self.vm
6864                        .emit_diagnostic(event.level, event.category, event.message);
6865                }
6866            }
6867            let _ = self.fs.remove_file(&sink.path);
6868        }
6869    }
6870
6871    fn flush_process_subst_out(&mut self, sink: PendingProcessSubstOut) {
6872        let saved_status = self.vm.state.last_status;
6873        match sink.mode {
6874            PendingProcessSubstOutMode::Buffered { data } => {
6875                self.flush_buffered_process_subst_out(&sink.inner, data);
6876            }
6877            PendingProcessSubstOutMode::Live { runner } => {
6878                self.flush_live_process_subst_out(runner);
6879            }
6880        }
6881        self.vm.state.last_status = saved_status;
6882    }
6883
6884    fn flush_buffered_process_subst_out(&mut self, inner: &str, data: Vec<u8>) {
6885        let events = if let Some(pipeline) = Self::parse_single_pipeline_input(inner) {
6886            self.execute_isolated_scheduled_pipeline_events_from_reader(
6887                &pipeline,
6888                Box::new(Cursor::new(data.clone())),
6889            )
6890        } else {
6891            self.execute_isolated_input_events(inner, Some(InputTarget::Bytes(data)))
6892        };
6893        for event in events {
6894            self.apply_isolated_flush_event(event);
6895        }
6896    }
6897
6898    fn apply_isolated_flush_event(&mut self, event: WorkerEvent) {
6899        match event {
6900            WorkerEvent::Stdout(data) => self.write_stdout(&data),
6901            WorkerEvent::Stderr(data) => self.write_stderr(&data),
6902            WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
6903                convert_diag_level(level),
6904                wasmsh_vm::DiagCategory::Runtime,
6905                msg,
6906            ),
6907            _ => {}
6908        }
6909    }
6910
6911    fn flush_live_process_subst_out(&mut self, mut runner: LiveProcessSubstRunner) {
6912        if runner.isolated_runtime.is_some() {
6913            runner.finish_with_parent(self);
6914        } else {
6915            runner.finish();
6916        }
6917        if !runner.captured_stdout.is_empty() {
6918            self.write_stdout(&runner.captured_stdout);
6919        }
6920        if !runner.captured_stderr.is_empty() {
6921            self.write_stderr(&runner.captured_stderr);
6922        }
6923        for diag in runner.captured_diagnostics {
6924            self.vm
6925                .emit_diagnostic(diag.level, diag.category, diag.message);
6926        }
6927    }
6928
6929    /// Execute `>(cmd)` by creating a writable temp path and scheduling the
6930    /// consumer command to run once the enclosing command finishes writing to it.
6931    fn execute_process_subst_out(&mut self, inner: &str) -> smol_str::SmolStr {
6932        smol_str::SmolStr::from(self.register_process_subst_out(inner))
6933    }
6934
6935    /// Resolve command substitutions in a list of words by executing them.
6936    fn resolve_command_subst(&mut self, words: &[Word]) -> Vec<Word> {
6937        words
6938            .iter()
6939            .map(|w| {
6940                let parts: Vec<WordPart> = w
6941                    .parts
6942                    .iter()
6943                    .map(|p| match p {
6944                        WordPart::CommandSubstitution(inner) => {
6945                            WordPart::Literal(self.execute_subst(inner))
6946                        }
6947                        WordPart::ProcessSubstIn(inner) => {
6948                            WordPart::Literal(self.execute_process_subst_in(inner))
6949                        }
6950                        WordPart::ProcessSubstOut(inner) => {
6951                            WordPart::Literal(self.execute_process_subst_out(inner))
6952                        }
6953                        WordPart::DoubleQuoted(inner_parts) => {
6954                            let resolved: Vec<WordPart> = inner_parts
6955                                .iter()
6956                                .map(|ip| match ip {
6957                                    WordPart::CommandSubstitution(inner) => {
6958                                        WordPart::Literal(self.execute_subst(inner))
6959                                    }
6960                                    WordPart::ProcessSubstIn(inner) => {
6961                                        WordPart::Literal(self.execute_process_subst_in(inner))
6962                                    }
6963                                    WordPart::ProcessSubstOut(inner) => {
6964                                        WordPart::Literal(self.execute_process_subst_out(inner))
6965                                    }
6966                                    other => other.clone(),
6967                                })
6968                                .collect();
6969                            WordPart::DoubleQuoted(resolved)
6970                        }
6971                        other => other.clone(),
6972                    })
6973                    .collect();
6974                Word {
6975                    parts,
6976                    span: w.span,
6977                }
6978            })
6979            .collect()
6980    }
6981
6982    fn execute_command(&mut self, cmd: &HirCommand) {
6983        self.run_debug_trap_if_needed();
6984        self.proc_subst_out_scopes.push(Vec::new());
6985        self.proc_subst_in_scopes.push(Vec::new());
6986        self.execute_command_body(cmd);
6987        let in_scope = self
6988            .proc_subst_in_scopes
6989            .pop()
6990            .expect("process substitution input scope stack underflow");
6991        let scope = self
6992            .proc_subst_out_scopes
6993            .pop()
6994            .expect("process substitution scope stack underflow");
6995        self.flush_process_subst_out_scope(scope);
6996        self.flush_process_subst_in_scope(in_scope);
6997    }
6998
6999    fn execute_command_body(&mut self, cmd: &HirCommand) {
7000        match cmd {
7001            HirCommand::Exec(exec) => self.execute_exec(exec),
7002            HirCommand::Assign(assign) => {
7003                for a in &assign.assignments {
7004                    self.execute_assignment(&a.name, a.value.as_ref());
7005                }
7006                let stdout_before = self.current_stdout_len();
7007                self.apply_redirections(&assign.redirections, stdout_before);
7008                self.vm.state.last_status = 0;
7009            }
7010            HirCommand::If(if_cmd) => self.execute_if(if_cmd),
7011            HirCommand::While(loop_cmd) => self.execute_while_loop(loop_cmd),
7012            HirCommand::Until(loop_cmd) => self.execute_until_loop(loop_cmd),
7013            HirCommand::For(for_cmd) => self.execute_for_loop(for_cmd),
7014            HirCommand::Group(block) => self.execute_body(&block.body),
7015            HirCommand::Subshell(block) => {
7016                self.vm.state.env.push_scope();
7017                self.execute_body(&block.body);
7018                self.vm.state.env.pop_scope();
7019            }
7020            HirCommand::Case(case_cmd) => self.execute_case(case_cmd),
7021            HirCommand::FunctionDef(fd) => {
7022                self.functions
7023                    .insert(fd.name.to_string(), (*fd.body).clone());
7024                self.vm.state.last_status = 0;
7025            }
7026            HirCommand::RedirectOnly(ro) => {
7027                let stdout_before = self.current_stdout_len();
7028                self.apply_redirections(&ro.redirections, stdout_before);
7029                self.vm.state.last_status = 0;
7030            }
7031            HirCommand::DoubleBracket(db) => {
7032                let result = self.eval_double_bracket(&db.words);
7033                self.vm.state.last_status = i32::from(!result);
7034            }
7035            HirCommand::ArithCommand(ac) => {
7036                let result = wasmsh_expand::eval_arithmetic(&ac.expr, &mut self.vm.state);
7037                self.vm.state.last_status = i32::from(result == 0);
7038            }
7039            HirCommand::ArithFor(af) => self.execute_arith_for(af),
7040            HirCommand::Select(sel) => self.execute_select(sel),
7041            _ => {}
7042        }
7043    }
7044
7045    /// Execute a simple command (`HirCommand::Exec`).
7046    fn execute_exec(&mut self, exec: &wasmsh_hir::HirExec) {
7047        let resolved = self.resolve_command_subst(&exec.argv);
7048        if self.exec.expansion_failed {
7049            return;
7050        }
7051        let expanded = expand_words_argv(&resolved, &mut self.vm.state);
7052
7053        if self.check_nounset_error() {
7054            return;
7055        }
7056        if expanded.is_empty() {
7057            return;
7058        }
7059
7060        // Brace and glob expansion must be suppressed for quoted words (POSIX + bash).
7061        let tagged: Vec<(String, bool)> = expanded
7062            .into_iter()
7063            .flat_map(|ew| {
7064                if ew.was_quoted {
7065                    vec![(ew.text, true)]
7066                } else {
7067                    wasmsh_expand::expand_braces(&ew.text)
7068                        .into_iter()
7069                        .map(|s| (s, false))
7070                        .collect()
7071                }
7072            })
7073            .collect();
7074        let argv = self.expand_globs_tagged(tagged);
7075
7076        for assignment in &exec.env {
7077            self.execute_assignment(&assignment.name, assignment.value.as_ref());
7078        }
7079
7080        if self.try_alias_expansion(&argv) {
7081            return;
7082        }
7083
7084        let Ok(exec_io) = self.prepare_exec_io(&exec.redirections) else {
7085            return;
7086        };
7087        self.with_exec_io_scope(exec_io, |runtime| {
7088            runtime.trace_command(&argv);
7089            runtime.execute_argv_command(&argv);
7090        });
7091    }
7092
7093    /// Drain a pending nounset error from parameter expansion and report it
7094    /// through the fallback interpreter's stderr sink.
7095    fn check_nounset_error(&mut self) -> bool {
7096        let Some(var_name) = self.vm.state.take_nounset_error() else {
7097            return false;
7098        };
7099        let msg = format!("wasmsh: {var_name}: unbound variable\n");
7100        self.write_stderr(msg.as_bytes());
7101        self.vm.state.last_status = 1;
7102        true
7103    }
7104
7105    /// Collect stdin from here-doc bodies or input redirections. Returns true if
7106    /// an error occurred and execution should stop.
7107    fn collect_stdin_from_redirections(&mut self, redirections: &[HirRedirection]) -> bool {
7108        for redir in redirections {
7109            if self.collect_stdin_from_redir(redir) {
7110                return true;
7111            }
7112        }
7113        false
7114    }
7115
7116    fn collect_stdin_from_redir(&mut self, redir: &HirRedirection) -> bool {
7117        match redir.op {
7118            RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
7119                self.collect_stdin_heredoc(redir);
7120                false
7121            }
7122            RedirectionOp::HereString => {
7123                self.collect_stdin_herestring(redir);
7124                false
7125            }
7126            RedirectionOp::Input => self.collect_stdin_input(redir),
7127            _ => false,
7128        }
7129    }
7130
7131    fn collect_stdin_heredoc(&mut self, redir: &HirRedirection) {
7132        if let Some(body) = &redir.here_doc_body {
7133            let expanded = wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
7134            self.set_pending_input_bytes(expanded.into_bytes());
7135        }
7136    }
7137
7138    fn collect_stdin_herestring(&mut self, redir: &HirRedirection) {
7139        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
7140        let resolved_target = resolved.first().unwrap_or(&redir.target);
7141        let content = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
7142        let mut data = content.into_bytes();
7143        data.push(b'\n');
7144        self.set_pending_input_bytes(data);
7145    }
7146
7147    fn collect_stdin_input(&mut self, redir: &HirRedirection) -> bool {
7148        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
7149        let resolved_target = resolved.first().unwrap_or(&redir.target);
7150        let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
7151        let path = self.resolve_cwd_path(&target);
7152        match self.fs.stat(&path) {
7153            Ok(metadata) if !metadata.is_dir => {
7154                self.set_pending_input_file(path, false);
7155                false
7156            }
7157            Ok(_) => self.fail_stdin_input(&target, "Is a directory"),
7158            Err(_) => self.fail_stdin_input(&target, "No such file or directory"),
7159        }
7160    }
7161
7162    fn fail_stdin_input(&mut self, target: &str, reason: &str) -> bool {
7163        let msg = format!("wasmsh: {target}: {reason}\n");
7164        self.write_stderr(msg.as_bytes());
7165        self.vm.state.last_status = 1;
7166        true
7167    }
7168
7169    fn read_pending_input_bytes(&mut self, cmd_name: &str) -> Result<Option<Vec<u8>>, ()> {
7170        let Some(mut reader) = self.take_pending_input_reader(cmd_name)? else {
7171            return Ok(None);
7172        };
7173        let mut data = Vec::new();
7174        match reader.read_to_end(&mut data) {
7175            Ok(_) => Ok(Some(data)),
7176            Err(err) => {
7177                let msg = format!("wasmsh: {cmd_name}: stdin read error: {err}\n");
7178                self.write_stderr(msg.as_bytes());
7179                self.vm.state.last_status = 1;
7180                Err(())
7181            }
7182        }
7183    }
7184
7185    /// Try alias expansion for the command. Returns true if an alias was expanded.
7186    fn try_alias_expansion(&mut self, argv: &[String]) -> bool {
7187        if !self.get_shopt_value("expand_aliases") {
7188            return false;
7189        }
7190        if let Some(alias_val) = self.aliases.get(&argv[0]).cloned() {
7191            let rest = if argv.len() > 1 {
7192                format!(" {}", argv[1..].join(" "))
7193            } else {
7194                String::new()
7195            };
7196            let expanded = format!("{alias_val}{rest}");
7197            let sub_events = self.execute_input_inner(&expanded);
7198            self.merge_sub_events(sub_events);
7199            return true;
7200        }
7201        false
7202    }
7203
7204    /// Print xtrace output if enabled.
7205    fn trace_command(&mut self, argv: &[String]) {
7206        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
7207            let ps4 = self
7208                .vm
7209                .state
7210                .get_var("PS4")
7211                .unwrap_or_else(|| smol_str::SmolStr::from("+ "));
7212            let trace_line = format!("{}{}\n", ps4, argv.join(" "));
7213            self.write_stderr(trace_line.as_bytes());
7214        }
7215    }
7216
7217    fn resolve_runtime_command(cmd_name: &str) -> Option<RuntimeCommandKind> {
7218        match cmd_name {
7219            CMD_LOCAL => Some(RuntimeCommandKind::Local),
7220            CMD_BREAK => Some(RuntimeCommandKind::Break),
7221            CMD_CONTINUE => Some(RuntimeCommandKind::Continue),
7222            CMD_EXIT => Some(RuntimeCommandKind::Exit),
7223            CMD_EVAL => Some(RuntimeCommandKind::Eval),
7224            CMD_SOURCE | CMD_DOT => Some(RuntimeCommandKind::Source),
7225            CMD_DECLARE | CMD_TYPESET => Some(RuntimeCommandKind::Declare),
7226            CMD_LET => Some(RuntimeCommandKind::Let),
7227            CMD_SHOPT => Some(RuntimeCommandKind::Shopt),
7228            CMD_ALIAS => Some(RuntimeCommandKind::Alias),
7229            CMD_UNALIAS => Some(RuntimeCommandKind::Unalias),
7230            CMD_BUILTIN => Some(RuntimeCommandKind::BuiltinKeyword),
7231            CMD_MAPFILE | CMD_READARRAY => Some(RuntimeCommandKind::Mapfile),
7232            CMD_TYPE => Some(RuntimeCommandKind::Type),
7233            CMD_COMMAND => Some(RuntimeCommandKind::CommandKeyword),
7234            CMD_EXEC => Some(RuntimeCommandKind::ExecKeyword),
7235            CMD_HASH => Some(RuntimeCommandKind::Hash),
7236            CMD_TIMES => Some(RuntimeCommandKind::Times),
7237            CMD_DIRS => Some(RuntimeCommandKind::Dirs),
7238            CMD_PUSHD => Some(RuntimeCommandKind::Pushd),
7239            CMD_POPD => Some(RuntimeCommandKind::Popd),
7240            CMD_UMASK => Some(RuntimeCommandKind::Umask),
7241            CMD_WAIT => Some(RuntimeCommandKind::Wait),
7242            CMD_ULIMIT => Some(RuntimeCommandKind::Ulimit),
7243            _ => None,
7244        }
7245    }
7246
7247    fn resolve_command(&self, cmd_name: &str, argv: &[String]) -> ResolvedCommand {
7248        if let Some(kind) = Self::resolve_runtime_command(cmd_name) {
7249            return ResolvedCommand::Runtime(kind);
7250        }
7251        if cmd_name == "bash" || cmd_name == "sh" {
7252            return ResolvedCommand::ShellScript;
7253        }
7254        if let Some(body) = self.functions.get(cmd_name).cloned() {
7255            return ResolvedCommand::Function(body);
7256        }
7257        if let Some(builtin_fn) = self.builtins.get(cmd_name) {
7258            return ResolvedCommand::Builtin(builtin_fn);
7259        }
7260        if let Some(util_fn) = self.utils.get(cmd_name) {
7261            return ResolvedCommand::Utility(Self::utility_kind(cmd_name, argv), util_fn);
7262        }
7263        ResolvedCommand::External
7264    }
7265
7266    fn resolve_command_without_functions(
7267        &self,
7268        cmd_name: &str,
7269        argv: &[String],
7270    ) -> ResolvedCommand {
7271        if let Some(kind) = Self::resolve_runtime_command(cmd_name) {
7272            return ResolvedCommand::Runtime(kind);
7273        }
7274        if cmd_name == "bash" || cmd_name == "sh" {
7275            return ResolvedCommand::ShellScript;
7276        }
7277        if let Some(builtin_fn) = self.builtins.get(cmd_name) {
7278            return ResolvedCommand::Builtin(builtin_fn);
7279        }
7280        if let Some(util_fn) = self.utils.get(cmd_name) {
7281            return ResolvedCommand::Utility(Self::utility_kind(cmd_name, argv), util_fn);
7282        }
7283        ResolvedCommand::External
7284    }
7285
7286    fn utility_kind(cmd_name: &str, argv: &[String]) -> UtilityCommandKind {
7287        if cmd_name == "find" && argv.iter().any(|arg| arg == "-exec") {
7288            UtilityCommandKind::FindWithExec
7289        } else if cmd_name == "xargs" {
7290            UtilityCommandKind::Xargs
7291        } else {
7292            UtilityCommandKind::Plain
7293        }
7294    }
7295
7296    fn find_command_path(&self, name: &str) -> Option<String> {
7297        if name.contains('/') {
7298            let path = self.resolve_cwd_path(name);
7299            self.fs.stat(&path).ok().map(|_| path)
7300        } else {
7301            self.search_path_for_file(name)
7302        }
7303    }
7304
7305    fn command_lookups(
7306        &self,
7307        name: &str,
7308        skip_functions: bool,
7309        force_path: bool,
7310    ) -> Vec<CommandLookup> {
7311        let mut lookups = Vec::new();
7312
7313        if !force_path {
7314            if let Some(value) = self.aliases.get(name) {
7315                lookups.push(CommandLookup {
7316                    kind: CommandLookupKind::Alias,
7317                    name: name.to_string(),
7318                    detail: value.clone(),
7319                });
7320            }
7321            if !skip_functions && self.functions.contains_key(name) {
7322                lookups.push(CommandLookup {
7323                    kind: CommandLookupKind::Function,
7324                    name: name.to_string(),
7325                    detail: name.to_string(),
7326                });
7327            }
7328            if self.builtins.is_builtin(name) {
7329                lookups.push(CommandLookup {
7330                    kind: CommandLookupKind::Builtin,
7331                    name: name.to_string(),
7332                    detail: name.to_string(),
7333                });
7334            }
7335        }
7336
7337        if let Some(path) = self.find_command_path(name) {
7338            lookups.push(CommandLookup {
7339                kind: CommandLookupKind::File,
7340                name: name.to_string(),
7341                detail: path,
7342            });
7343        }
7344
7345        lookups
7346    }
7347
7348    fn execute_argv_command(&mut self, argv: &[String]) {
7349        if self.check_resource_limits() || argv.is_empty() {
7350            return;
7351        }
7352        if let Some(last) = argv.last() {
7353            self.vm.state.set_last_argument(last.as_str());
7354        }
7355        let mut resolved = self.resolve_command(&argv[0], argv);
7356        // If the command would be dispatched externally and the path
7357        // contains a `/`, check whether the file has a shell shebang
7358        // so we can execute it natively instead of forwarding to the
7359        // external handler (which may not exist).
7360        if matches!(resolved, ResolvedCommand::External) && argv[0].contains('/') {
7361            if let Some(interp) = self.detect_shell_shebang(&argv[0]) {
7362                if interp == "bash"
7363                    || interp == "sh"
7364                    || interp == "/bin/bash"
7365                    || interp == "/bin/sh"
7366                    || interp.ends_with("/bash")
7367                    || interp.ends_with("/sh")
7368                {
7369                    resolved = ResolvedCommand::ShebangScript;
7370                }
7371            }
7372        }
7373        self.execute_resolved_command(resolved, argv);
7374    }
7375
7376    fn execute_resolved_command(&mut self, resolved: ResolvedCommand, argv: &[String]) {
7377        match resolved {
7378            ResolvedCommand::Runtime(kind) => self.execute_runtime_command(kind, argv),
7379            ResolvedCommand::ShellScript => self.call_shell_script(argv),
7380            ResolvedCommand::ShebangScript => self.call_shebang_script(argv),
7381            ResolvedCommand::Function(body) => self.call_shell_function(&argv[0], argv, &body),
7382            ResolvedCommand::Builtin(builtin_fn) => self.call_builtin(&argv[0], builtin_fn, argv),
7383            ResolvedCommand::Utility(kind, util_fn) => match kind {
7384                UtilityCommandKind::Plain => self.call_utility(&argv[0], util_fn, argv),
7385                UtilityCommandKind::FindWithExec => self.call_find_with_exec(util_fn, argv),
7386                UtilityCommandKind::Xargs => self.call_xargs_with_exec(util_fn, argv),
7387            },
7388            ResolvedCommand::External => self.call_external(argv),
7389        }
7390    }
7391
7392    fn execute_runtime_command(&mut self, kind: RuntimeCommandKind, argv: &[String]) {
7393        match kind {
7394            RuntimeCommandKind::Local => self.execute_local(argv),
7395            RuntimeCommandKind::Break => {
7396                self.exec.break_depth = argv.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
7397                self.vm.state.last_status = 0;
7398            }
7399            RuntimeCommandKind::Continue => {
7400                self.exec.loop_continue = true;
7401                self.vm.state.last_status = 0;
7402            }
7403            RuntimeCommandKind::Exit => {
7404                let code = argv
7405                    .get(1)
7406                    .and_then(|s| s.parse().ok())
7407                    .unwrap_or(self.vm.state.last_status);
7408                self.exec.exit_requested = Some(code);
7409                self.vm.state.last_status = code;
7410            }
7411            RuntimeCommandKind::Eval => {
7412                let code = argv[1..].join(" ");
7413                let sub_events = self.execute_input_inner(&code);
7414                self.merge_sub_events_with_diagnostics(sub_events);
7415            }
7416            RuntimeCommandKind::Source => self.execute_source(argv),
7417            RuntimeCommandKind::Declare => self.execute_declare(argv),
7418            RuntimeCommandKind::Let => self.execute_let(argv),
7419            RuntimeCommandKind::Shopt => self.execute_shopt(argv),
7420            RuntimeCommandKind::Alias => self.execute_alias(argv),
7421            RuntimeCommandKind::Unalias => self.execute_unalias(argv),
7422            RuntimeCommandKind::BuiltinKeyword => self.execute_builtin_keyword(argv),
7423            RuntimeCommandKind::Mapfile => self.execute_mapfile(argv),
7424            RuntimeCommandKind::Type => self.execute_type(argv),
7425            RuntimeCommandKind::CommandKeyword => self.execute_command_keyword(argv),
7426            RuntimeCommandKind::ExecKeyword => self.execute_exec_keyword(argv),
7427            RuntimeCommandKind::Hash => self.execute_hash(argv),
7428            RuntimeCommandKind::Times => self.execute_times(),
7429            RuntimeCommandKind::Dirs => self.execute_dirs(),
7430            RuntimeCommandKind::Pushd => self.execute_pushd(argv),
7431            RuntimeCommandKind::Popd => self.execute_popd(),
7432            RuntimeCommandKind::Umask => self.execute_umask(argv),
7433            RuntimeCommandKind::Wait => self.execute_wait(argv),
7434            RuntimeCommandKind::Ulimit => self.execute_ulimit(argv),
7435        }
7436    }
7437
7438    /// Execute `local` — save old variable values and set new ones.
7439    fn execute_local(&mut self, argv: &[String]) {
7440        for arg in &argv[1..] {
7441            let (name, value) = if let Some(eq) = arg.find('=') {
7442                (&arg[..eq], Some(&arg[eq + 1..]))
7443            } else {
7444                (arg.as_str(), None)
7445            };
7446            let old = self.vm.state.get_var(name);
7447            self.exec
7448                .local_save_stack
7449                .push((smol_str::SmolStr::from(name), old));
7450            let val = value.map_or(smol_str::SmolStr::default(), smol_str::SmolStr::from);
7451            self.vm.state.set_var(smol_str::SmolStr::from(name), val);
7452        }
7453        self.vm.state.last_status = 0;
7454    }
7455
7456    /// Execute `source`/`.` — read and execute a file.
7457    fn execute_source(&mut self, argv: &[String]) {
7458        let Some(path) = argv.get(1) else { return };
7459        let resolved = if path.contains('/') {
7460            Some(self.resolve_cwd_path(path))
7461        } else {
7462            let direct = self.resolve_cwd_path(path);
7463            if self.fs.stat(&direct).is_ok() {
7464                Some(direct)
7465            } else if self.get_shopt_value("sourcepath") {
7466                self.search_path_for_file(path)
7467            } else {
7468                None
7469            }
7470        };
7471        let Some(full) = resolved else {
7472            let msg = format!("source: {path}: not found\n");
7473            self.write_stderr(msg.as_bytes());
7474            self.vm.state.last_status = 1;
7475            return;
7476        };
7477        let Ok(h) = self.fs.open(&full, OpenOptions::read()) else {
7478            let msg = format!("source: {path}: not found\n");
7479            self.write_stderr(msg.as_bytes());
7480            self.vm.state.last_status = 1;
7481            return;
7482        };
7483        match self.fs.read_file(h) {
7484            Ok(data) => {
7485                self.fs.close(h);
7486                self.vm
7487                    .state
7488                    .source_stack
7489                    .push(smol_str::SmolStr::from(full.as_str()));
7490                let code = String::from_utf8_lossy(&data).to_string();
7491                self.with_nested_shell_scope(|runtime| {
7492                    let sub_events = runtime.execute_input_inner(&code);
7493                    runtime.merge_sub_events_with_diagnostics(sub_events);
7494                    runtime.run_return_trap_if_needed();
7495                });
7496                self.vm.state.source_stack.pop();
7497            }
7498            Err(e) => {
7499                self.fs.close(h);
7500                let msg = format!("source: {path}: read error: {e}\n");
7501                self.write_stderr(msg.as_bytes());
7502                self.vm.state.last_status = 1;
7503            }
7504        }
7505    }
7506
7507    /// Merge sub-events (stdout/stderr only) into the current VM buffers.
7508    fn merge_sub_events(&mut self, events: Vec<WorkerEvent>) {
7509        for e in events {
7510            match e {
7511                WorkerEvent::Stdout(d) => self.write_stdout(&d),
7512                WorkerEvent::Stderr(d) => self.write_stderr(&d),
7513                _ => {}
7514            }
7515        }
7516    }
7517
7518    /// Merge sub-events including diagnostics into the current VM buffers.
7519    fn merge_sub_events_with_diagnostics(&mut self, events: Vec<WorkerEvent>) {
7520        for e in events {
7521            match e {
7522                WorkerEvent::Stdout(d) => self.write_stdout(&d),
7523                WorkerEvent::Stderr(d) => self.write_stderr(&d),
7524                WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
7525                    convert_diag_level(level),
7526                    wasmsh_vm::DiagCategory::Runtime,
7527                    msg,
7528                ),
7529                _ => {}
7530            }
7531        }
7532    }
7533
7534    /// Handle `bash`/`sh` commands by reading the script and executing it.
7535    fn call_shell_script(&mut self, argv: &[String]) {
7536        if argv.len() < 2 {
7537            // Interactive shell not supported — just return
7538            return;
7539        }
7540
7541        // Check for -c flag (inline script)
7542        // bash -c 'script' [name [args...]]
7543        // $0 = name (argv[3]), $1.. = args (argv[4..])
7544        if argv[1] == "-c" {
7545            if let Some(script) = argv.get(2) {
7546                let old_positional = std::mem::take(&mut self.vm.state.positional);
7547                let old_script_name = self.vm.state.script_name.take();
7548                if let Some(name) = argv.get(3) {
7549                    self.vm.state.script_name = Some(smol_str::SmolStr::from(name.as_str()));
7550                }
7551                self.vm.state.positional = argv
7552                    .get(4..)
7553                    .unwrap_or_default()
7554                    .iter()
7555                    .map(|s| smol_str::SmolStr::from(s.as_str()))
7556                    .collect();
7557                self.with_nested_shell_scope(|runtime| {
7558                    let sub_events = runtime.execute_input_inner(script);
7559                    runtime.merge_sub_events_with_diagnostics(sub_events);
7560                });
7561                self.vm.state.positional = old_positional;
7562                self.vm.state.script_name = old_script_name;
7563            }
7564            return;
7565        }
7566
7567        // Read script file from VFS
7568        let path = if argv[1].starts_with('/') {
7569            argv[1].clone()
7570        } else {
7571            format!("{}/{}", self.vm.state.cwd, argv[1])
7572        };
7573        let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
7574            let msg = format!("{}: {}: No such file or directory\n", argv[0], argv[1]);
7575            self.write_stderr(msg.as_bytes());
7576            self.vm.state.last_status = 127;
7577            return;
7578        };
7579        let data = self.fs.read_file(h).unwrap_or_default();
7580        self.fs.close(h);
7581        let content = String::from_utf8_lossy(&data).to_string();
7582
7583        // Set $0 to the script path, positional parameters from argv[2..]
7584        let old_positional = std::mem::take(&mut self.vm.state.positional);
7585        let old_script_name = self.vm.state.script_name.take();
7586        self.vm.state.script_name = Some(smol_str::SmolStr::from(argv[1].as_str()));
7587        self.vm.state.positional = argv[2..]
7588            .iter()
7589            .map(|s| smol_str::SmolStr::from(s.as_str()))
7590            .collect();
7591
7592        self.vm
7593            .state
7594            .source_stack
7595            .push(smol_str::SmolStr::from(path.as_str()));
7596        let sub_events =
7597            self.with_nested_shell_scope(|runtime| runtime.execute_input_inner(&content));
7598        self.vm.state.source_stack.pop();
7599        self.merge_sub_events_with_diagnostics(sub_events);
7600
7601        self.vm.state.positional = old_positional;
7602        self.vm.state.script_name = old_script_name;
7603    }
7604
7605    /// Detect a shell shebang at the start of a file.
7606    /// Returns the interpreter command (e.g. "bash", "/bin/sh") if found.
7607    fn detect_shell_shebang(&mut self, cmd_name: &str) -> Option<String> {
7608        let path = if cmd_name.starts_with('/') {
7609            cmd_name.to_string()
7610        } else {
7611            format!("{}/{cmd_name}", self.vm.state.cwd)
7612        };
7613        let h = self.fs.open(&path, OpenOptions::read()).ok()?;
7614        let data = self.fs.read_file(h).unwrap_or_default();
7615        self.fs.close(h);
7616        if data.len() < 3 || data[0] != b'#' || data[1] != b'!' {
7617            return None;
7618        }
7619        let end = data.iter().position(|&b| b == b'\n').unwrap_or(data.len());
7620        let line = String::from_utf8_lossy(&data[2..end]).trim().to_string();
7621        // Handle "#!/usr/bin/env bash" → "bash"
7622        if let Some(rest) = line.strip_prefix("/usr/bin/env ") {
7623            Some(rest.trim().to_string())
7624        } else {
7625            // e.g. "/bin/bash" → extract basename for matching
7626            Some(line.clone())
7627        }
7628    }
7629
7630    /// Execute a script file that was invoked directly by path (e.g. `/workspace/script.sh`).
7631    /// The shebang has already been validated as a shell interpreter.
7632    fn call_shebang_script(&mut self, argv: &[String]) {
7633        let cmd_name = &argv[0];
7634        let path = if cmd_name.starts_with('/') {
7635            cmd_name.clone()
7636        } else {
7637            format!("{}/{cmd_name}", self.vm.state.cwd)
7638        };
7639        let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
7640            let msg = format!("wasmsh: {cmd_name}: No such file or directory\n");
7641            self.write_stderr(msg.as_bytes());
7642            self.vm.state.last_status = 127;
7643            return;
7644        };
7645        let data = self.fs.read_file(h).unwrap_or_default();
7646        self.fs.close(h);
7647        let content = String::from_utf8_lossy(&data).to_string();
7648
7649        // Set $0 to the script path, positional parameters from argv[1..]
7650        let old_positional = std::mem::take(&mut self.vm.state.positional);
7651        let old_script_name = self.vm.state.script_name.take();
7652        self.vm.state.script_name = Some(smol_str::SmolStr::from(cmd_name.as_str()));
7653        self.vm.state.positional = argv[1..]
7654            .iter()
7655            .map(|s| smol_str::SmolStr::from(s.as_str()))
7656            .collect();
7657
7658        self.vm
7659            .state
7660            .source_stack
7661            .push(smol_str::SmolStr::from(path.as_str()));
7662        let sub_events =
7663            self.with_nested_shell_scope(|runtime| runtime.execute_input_inner(&content));
7664        self.vm.state.source_stack.pop();
7665        self.merge_sub_events_with_diagnostics(sub_events);
7666
7667        self.vm.state.positional = old_positional;
7668        self.vm.state.script_name = old_script_name;
7669    }
7670
7671    fn call_external(&mut self, argv: &[String]) {
7672        let cmd_name = &argv[0];
7673        let Ok(stdin) = self.take_external_stdin(cmd_name) else {
7674            return;
7675        };
7676        if let Some(ref mut handler) = self.external_handler {
7677            if let Some(result) = handler(cmd_name, argv, stdin) {
7678                self.write_streams(&result.stdout, &result.stderr);
7679                self.vm.state.last_status = result.status;
7680            } else {
7681                let msg = format!("wasmsh: {cmd_name}: command not found\n");
7682                self.write_stderr(msg.as_bytes());
7683                self.vm.state.last_status = 127;
7684            }
7685        } else {
7686            let msg = format!("wasmsh: {cmd_name}: command not found\n");
7687            self.write_stderr(msg.as_bytes());
7688            self.vm.state.last_status = 127;
7689        }
7690    }
7691
7692    /// Invoke a shell function.
7693    fn call_shell_function(&mut self, cmd_name: &str, argv: &[String], body: &HirCommand) {
7694        self.exec.recursion_depth += 1;
7695        if let Err(reason) = self
7696            .vm
7697            .budget
7698            .enter_recursion(self.vm.limits.recursion_limit)
7699        {
7700            self.exec.recursion_depth -= 1;
7701            self.mark_budget_exhaustion(reason);
7702            self.write_stderr(b"wasmsh: maximum recursion depth exceeded\n");
7703            self.vm.state.last_status = 1;
7704            return;
7705        }
7706        let old_positional = std::mem::take(&mut self.vm.state.positional);
7707        self.vm.state.positional = argv[1..]
7708            .iter()
7709            .map(|s| smol_str::SmolStr::from(s.as_str()))
7710            .collect();
7711        self.vm
7712            .state
7713            .func_stack
7714            .push(smol_str::SmolStr::from(cmd_name));
7715        let locals_before = self.exec.local_save_stack.len();
7716        self.with_nested_shell_scope(|runtime| {
7717            runtime.execute_command(body);
7718            runtime.run_return_trap_if_needed();
7719        });
7720        let new_locals: Vec<_> = self.exec.local_save_stack.drain(locals_before..).collect();
7721        for (name, old_val) in new_locals.into_iter().rev() {
7722            if let Some(val) = old_val {
7723                self.vm.state.set_var(name, val);
7724            } else {
7725                self.vm.state.unset_var(&name).ok();
7726            }
7727        }
7728        self.vm.state.func_stack.pop();
7729        self.vm.state.positional = old_positional;
7730        self.vm.budget.exit_recursion();
7731        self.exec.recursion_depth -= 1;
7732    }
7733
7734    /// Invoke a builtin command.
7735    fn call_builtin(
7736        &mut self,
7737        cmd_name: &str,
7738        builtin_fn: wasmsh_builtins::BuiltinFn,
7739        argv: &[String],
7740    ) {
7741        let Ok(stdin) = self.take_builtin_stdin(cmd_name) else {
7742            return;
7743        };
7744        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
7745        let status = {
7746            let mut router = RuntimeOutputRouter {
7747                exec: &mut self.exec,
7748                exec_io: self.current_exec_io.as_mut(),
7749                proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
7750                vm_stdout: &mut self.vm.stdout,
7751                vm_stderr: &mut self.vm.stderr,
7752                vm_output_bytes: &mut self.vm.output_bytes,
7753                vm_output_limit: self.vm.limits.output_byte_limit,
7754                vm_diagnostics: &mut self.vm.diagnostics,
7755            };
7756            let mut sink = RuntimeBuiltinSink {
7757                router: &mut router,
7758            };
7759            let mut ctx = wasmsh_builtins::BuiltinContext {
7760                state: &mut self.vm.state,
7761                output: &mut sink,
7762                fs: Some(&self.fs),
7763                stdin,
7764            };
7765            builtin_fn(&mut ctx, &argv_refs)
7766        };
7767        self.vm.state.last_status = status;
7768    }
7769
7770    /// Extract `-exec CMD [args...] {} \;` from find argv.
7771    /// Returns `(exec_template, cleaned_argv)` or `None` if no `-exec` present.
7772    fn extract_find_exec(argv: &[String]) -> Option<(Vec<String>, Vec<String>)> {
7773        let exec_pos = argv.iter().position(|a| a == "-exec")?;
7774        // Find the terminator: \; or ;
7775        let term_pos = argv[exec_pos + 1..]
7776            .iter()
7777            .position(|a| a == "\\;" || a == ";")
7778            .map(|p| p + exec_pos + 1)?;
7779        let template: Vec<String> = argv[exec_pos + 1..term_pos].to_vec();
7780        if template.is_empty() {
7781            return None;
7782        }
7783        let mut cleaned: Vec<String> = argv[..exec_pos].to_vec();
7784        cleaned.extend_from_slice(&argv[term_pos + 1..]);
7785        Some((template, cleaned))
7786    }
7787
7788    /// Shell-quote a path for safe interpolation into a command string.
7789    fn shell_quote(s: &str) -> String {
7790        if s.chars()
7791            .all(|c| c.is_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'))
7792        {
7793            s.to_string()
7794        } else {
7795            format!("'{}'", s.replace('\'', "'\\''"))
7796        }
7797    }
7798
7799    /// Handle `find ... -exec CMD {} \;` by running find for paths, then executing
7800    /// the command for each matched path via the shell.
7801    fn call_find_with_exec(&mut self, find_fn: wasmsh_utils::UtilFn, argv: &[String]) {
7802        let Some((template, cleaned_argv)) = Self::extract_find_exec(argv) else {
7803            // Malformed -exec (missing \;), fall through to normal find
7804            self.call_utility("find", find_fn, argv);
7805            return;
7806        };
7807
7808        // Phase 1: run find with cleaned argv, capturing stdout
7809        let ((), captured) = self.with_output_capture(true, false, |runtime| {
7810            runtime.call_utility("find", find_fn, &cleaned_argv);
7811        });
7812        let find_output = captured.stdout;
7813
7814        // Phase 2: parse matched paths
7815        let paths_str = String::from_utf8_lossy(&find_output);
7816        let paths: Vec<&str> = paths_str.lines().filter(|l| !l.is_empty()).collect();
7817
7818        // Phase 3: execute the command for each path
7819        let mut last_status = 0i32;
7820        for path in paths {
7821            let cmd_line: String = template
7822                .iter()
7823                .map(|t| {
7824                    if t == "{}" {
7825                        Self::shell_quote(path)
7826                    } else {
7827                        t.clone()
7828                    }
7829                })
7830                .collect::<Vec<_>>()
7831                .join(" ");
7832            let sub_events = self.execute_input_inner(&cmd_line);
7833            self.merge_sub_events(sub_events);
7834            if self.vm.state.last_status != 0 {
7835                last_status = self.vm.state.last_status;
7836            }
7837        }
7838        self.vm.state.last_status = last_status;
7839    }
7840
7841    /// Handle `xargs` with actual command execution for non-echo commands.
7842    /// The existing xargs utility already formats correct command lines for
7843    /// non-echo; we capture those and execute them via the shell.
7844    fn call_xargs_with_exec(&mut self, xargs_fn: wasmsh_utils::UtilFn, argv: &[String]) {
7845        // Determine if xargs has a non-echo command by scanning past flags
7846        let mut has_non_echo = false;
7847        let mut i = 1;
7848        while i < argv.len() {
7849            let arg = &argv[i];
7850            if matches!(arg.as_str(), "-I" | "-n" | "-d" | "-P" | "-L") && i + 1 < argv.len() {
7851                i += 2;
7852            } else if matches!(arg.as_str(), "-0" | "--null" | "-t" | "-p") || arg.starts_with('-')
7853            {
7854                i += 1;
7855            } else {
7856                // First non-flag arg is the command
7857                if arg != "echo" {
7858                    has_non_echo = true;
7859                }
7860                break;
7861            }
7862        }
7863
7864        if !has_non_echo {
7865            self.call_utility("xargs", xargs_fn, argv);
7866            return;
7867        }
7868
7869        // Run xargs utility — it outputs formatted command lines for non-echo
7870        let ((), captured) = self.with_output_capture(true, false, |runtime| {
7871            runtime.call_utility("xargs", xargs_fn, argv);
7872        });
7873        let xargs_output = captured.stdout;
7874
7875        // Execute each output line as a command
7876        let output_str = String::from_utf8_lossy(&xargs_output);
7877        let mut last_status = 0i32;
7878        for line in output_str.lines().filter(|l| !l.is_empty()) {
7879            let sub_events = self.execute_input_inner(line);
7880            self.merge_sub_events(sub_events);
7881            if self.vm.state.last_status != 0 {
7882                last_status = self.vm.state.last_status;
7883            }
7884        }
7885        self.vm.state.last_status = last_status;
7886    }
7887
7888    /// Invoke a utility command.
7889    fn call_utility(&mut self, cmd_name: &str, util_fn: wasmsh_utils::UtilFn, argv: &[String]) {
7890        let Ok(stdin) = self.take_util_stdin(cmd_name) else {
7891            return;
7892        };
7893        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
7894        let cwd = self.vm.state.cwd.clone();
7895        let status = {
7896            let mut router = RuntimeOutputRouter {
7897                exec: &mut self.exec,
7898                exec_io: self.current_exec_io.as_mut(),
7899                proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
7900                vm_stdout: &mut self.vm.stdout,
7901                vm_stderr: &mut self.vm.stderr,
7902                vm_output_bytes: &mut self.vm.output_bytes,
7903                vm_output_limit: self.vm.limits.output_byte_limit,
7904                vm_diagnostics: &mut self.vm.diagnostics,
7905            };
7906            let mut output = RuntimeUtilSink {
7907                router: &mut router,
7908            };
7909            let mut ctx = UtilContext {
7910                fs: &mut self.fs,
7911                output: &mut output,
7912                cwd: &cwd,
7913                stdin,
7914                state: Some(&self.vm.state),
7915                network: self.network.as_deref(),
7916            };
7917            util_fn(&mut ctx, &argv_refs)
7918        };
7919        self.vm.state.last_status = status;
7920    }
7921
7922    /// Execute an `if` command.
7923    fn execute_if(&mut self, if_cmd: &wasmsh_hir::HirIf) {
7924        let saved_suppress = self.exec.errexit_suppressed;
7925        self.exec.errexit_suppressed = true;
7926        self.execute_body(&if_cmd.condition);
7927        self.exec.errexit_suppressed = saved_suppress;
7928        if self.vm.state.last_status == 0 {
7929            self.execute_body(&if_cmd.then_body);
7930            return;
7931        }
7932        for elif in &if_cmd.elifs {
7933            let saved = self.exec.errexit_suppressed;
7934            self.exec.errexit_suppressed = true;
7935            self.execute_body(&elif.condition);
7936            self.exec.errexit_suppressed = saved;
7937            if self.vm.state.last_status == 0 {
7938                self.execute_body(&elif.then_body);
7939                return;
7940            }
7941        }
7942        if let Some(else_body) = &if_cmd.else_body {
7943            self.execute_body(else_body);
7944        }
7945    }
7946
7947    /// Execute a `while` loop.
7948    fn execute_while_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
7949        loop {
7950            if self.check_resource_limits() {
7951                break;
7952            }
7953            let saved = self.exec.errexit_suppressed;
7954            self.exec.errexit_suppressed = true;
7955            self.execute_body(&loop_cmd.condition);
7956            self.exec.errexit_suppressed = saved;
7957            if self.vm.state.last_status != 0 {
7958                break;
7959            }
7960            self.execute_body(&loop_cmd.body);
7961            if self.handle_loop_control() {
7962                break;
7963            }
7964        }
7965    }
7966
7967    /// Execute an `until` loop.
7968    fn execute_until_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
7969        loop {
7970            if self.check_resource_limits() {
7971                break;
7972            }
7973            let saved = self.exec.errexit_suppressed;
7974            self.exec.errexit_suppressed = true;
7975            self.execute_body(&loop_cmd.condition);
7976            self.exec.errexit_suppressed = saved;
7977            if self.vm.state.last_status == 0 {
7978                break;
7979            }
7980            self.execute_body(&loop_cmd.body);
7981            if self.handle_loop_control() {
7982                break;
7983            }
7984        }
7985    }
7986
7987    /// Handle loop control flow (break/continue/exit). Returns true if the loop should break.
7988    fn handle_loop_control(&mut self) -> bool {
7989        if self.exec.break_depth > 0 {
7990            self.exec.break_depth -= 1;
7991            return true;
7992        }
7993        if self.exec.loop_continue {
7994            self.exec.loop_continue = false;
7995        }
7996        self.exec.exit_requested.is_some()
7997    }
7998
7999    /// Execute a `for` loop.
8000    fn execute_for_loop(&mut self, for_cmd: &wasmsh_hir::HirFor) {
8001        let words = self.expand_for_words(for_cmd.words.as_deref());
8002        for word in words {
8003            if self.check_resource_limits() {
8004                break;
8005            }
8006            self.vm.state.set_var(for_cmd.var_name.clone(), word.into());
8007            self.execute_body(&for_cmd.body);
8008            if self.exec.break_depth > 0 {
8009                self.exec.break_depth -= 1;
8010                break;
8011            }
8012            if self.exec.loop_continue {
8013                self.exec.loop_continue = false;
8014                continue;
8015            }
8016            if self.exec.exit_requested.is_some() {
8017                break;
8018            }
8019        }
8020    }
8021
8022    /// Expand word list for `for` and `select` commands.
8023    fn expand_for_words(&mut self, words: Option<&[Word]>) -> Vec<String> {
8024        if let Some(ws) = words {
8025            let resolved = self.resolve_command_subst(ws);
8026            let mut result = Vec::new();
8027            for w in &resolved {
8028                let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
8029                result.extend(expanded.fields);
8030            }
8031            let result: Vec<String> = result
8032                .into_iter()
8033                .flat_map(|arg| wasmsh_expand::expand_braces(&arg))
8034                .collect();
8035            self.expand_globs(result)
8036        } else {
8037            self.vm
8038                .state
8039                .positional
8040                .iter()
8041                .map(ToString::to_string)
8042                .collect()
8043        }
8044    }
8045
8046    /// Execute a `case` command.
8047    fn execute_case(&mut self, case_cmd: &wasmsh_hir::HirCase) {
8048        let nocasematch = self.vm.state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
8049        let value = wasmsh_expand::expand_word(&case_cmd.word, &mut self.vm.state);
8050        let mut i = 0;
8051        let mut fallthrough = false;
8052        while i < case_cmd.items.len() {
8053            let item = &case_cmd.items[i];
8054            let pattern_matched = if fallthrough {
8055                true
8056            } else {
8057                item.patterns.iter().any(|pattern| {
8058                    let pat = wasmsh_expand::expand_word(pattern, &mut self.vm.state);
8059                    if nocasematch {
8060                        glob_match_inner(
8061                            pat.to_lowercase().as_bytes(),
8062                            value.to_lowercase().as_bytes(),
8063                        )
8064                    } else {
8065                        glob_match_inner(pat.as_bytes(), value.as_bytes())
8066                    }
8067                })
8068            };
8069            if pattern_matched {
8070                self.execute_body(&item.body);
8071                match item.terminator {
8072                    CaseTerminator::Break => break,
8073                    CaseTerminator::Fallthrough => {
8074                        fallthrough = true;
8075                        i += 1;
8076                    }
8077                    CaseTerminator::ContinueTesting => {
8078                        fallthrough = false;
8079                        i += 1;
8080                    }
8081                }
8082            } else {
8083                fallthrough = false;
8084                i += 1;
8085            }
8086        }
8087    }
8088
8089    /// Execute a C-style `for (( init; cond; step ))` loop.
8090    fn execute_arith_for(&mut self, af: &wasmsh_hir::HirArithFor) {
8091        if !af.init.is_empty() {
8092            wasmsh_expand::eval_arithmetic(&af.init, &mut self.vm.state);
8093        }
8094        loop {
8095            if self.check_resource_limits() {
8096                break;
8097            }
8098            if !af.cond.is_empty() {
8099                let cond_val = wasmsh_expand::eval_arithmetic(&af.cond, &mut self.vm.state);
8100                if cond_val == 0 {
8101                    break;
8102                }
8103            }
8104            self.execute_body(&af.body);
8105            if self.handle_loop_control() {
8106                break;
8107            }
8108            if !af.step.is_empty() {
8109                wasmsh_expand::eval_arithmetic(&af.step, &mut self.vm.state);
8110            }
8111        }
8112    }
8113
8114    /// Execute a `select` command.
8115    fn execute_select(&mut self, sel: &wasmsh_hir::HirSelect) {
8116        if self.collect_stdin_from_redirections(&sel.redirections) {
8117            return;
8118        }
8119        let words = self.expand_for_words(sel.words.as_deref());
8120        if words.is_empty() {
8121            return;
8122        }
8123        self.print_select_menu(&words);
8124        let Ok(input) = self.read_pending_input_bytes("select") else {
8125            return;
8126        };
8127        let input = String::from_utf8_lossy(&input.unwrap_or_default()).into_owned();
8128
8129        for line in input.lines() {
8130            let reply = line.trim();
8131            self.bind_select_iteration_vars(sel, reply, &words);
8132            self.execute_body(&sel.body);
8133            if !self.consume_select_loop_control() {
8134                break;
8135            }
8136            if reply.is_empty() {
8137                self.print_select_menu(&words);
8138            }
8139        }
8140    }
8141
8142    /// Set `REPLY` and the user-named variable for one iteration of `select`.
8143    fn bind_select_iteration_vars(
8144        &mut self,
8145        sel: &wasmsh_hir::HirSelect,
8146        reply: &str,
8147        words: &[String],
8148    ) {
8149        self.vm
8150            .state
8151            .set_var(smol_str::SmolStr::from("REPLY"), reply.into());
8152        let selected = Self::pick_select_word(reply, words).unwrap_or_default();
8153        self.vm.state.set_var(sel.var_name.clone(), selected.into());
8154    }
8155
8156    /// Resolve the user's reply to the word at that 1-based menu index, if any.
8157    fn pick_select_word(reply: &str, words: &[String]) -> Option<String> {
8158        reply
8159            .parse::<usize>()
8160            .ok()
8161            .filter(|&n| n >= 1 && n <= words.len())
8162            .map(|n| words[n - 1].clone())
8163    }
8164
8165    /// Apply the post-body break/continue/exit checks. Returns `true` to keep
8166    /// looping, `false` to break out of the `select` loop.
8167    fn consume_select_loop_control(&mut self) -> bool {
8168        if self.exec.break_depth > 0 {
8169            self.exec.break_depth -= 1;
8170            return false;
8171        }
8172        if self.exec.loop_continue {
8173            self.exec.loop_continue = false;
8174        }
8175        if self.exec.exit_requested.is_some() {
8176            return false;
8177        }
8178        true
8179    }
8180
8181    fn print_select_menu(&mut self, words: &[String]) {
8182        for (idx, word) in words.iter().enumerate() {
8183            let line = format!("{}) {word}\n", idx + 1);
8184            self.write_stderr(line.as_bytes());
8185        }
8186    }
8187
8188    // ---- [[ ]] extended test evaluation ----
8189
8190    /// Expand a word inside `[[ ]]` — no word splitting or glob expansion.
8191    fn dbl_bracket_expand(&mut self, word: &Word) -> String {
8192        let resolved = self.resolve_command_subst(std::slice::from_ref(word));
8193        wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
8194    }
8195
8196    /// Evaluate a `[[ expression ]]` command. Returns true for exit-status 0.
8197    fn eval_double_bracket(&mut self, words: &[Word]) -> bool {
8198        // Expand all words (no splitting/globbing) into string tokens for the evaluator
8199        let tokens: Vec<String> = words.iter().map(|w| self.dbl_bracket_expand(w)).collect();
8200        let mut pos = 0;
8201        dbl_bracket_eval_or(&tokens, &mut pos, &self.fs, &mut self.vm.state)
8202    }
8203
8204    fn resolve_cwd_path(&self, path: &str) -> String {
8205        if path.starts_with('/') {
8206            wasmsh_fs::normalize_path(path)
8207        } else {
8208            wasmsh_fs::normalize_path(&format!("{}/{}", self.vm.state.cwd, path))
8209        }
8210    }
8211
8212    /// Execute `alias [name[='value'] ...]`.
8213    fn execute_alias(&mut self, argv: &[String]) {
8214        let args = &argv[1..];
8215        if args.is_empty() {
8216            // List all aliases
8217            let alias_lines: Vec<String> = self
8218                .aliases
8219                .iter()
8220                .map(|(name, value)| format!("alias {name}='{value}'\n"))
8221                .collect();
8222            for line in alias_lines {
8223                self.write_stdout(line.as_bytes());
8224            }
8225            self.vm.state.last_status = 0;
8226            return;
8227        }
8228        for arg in args {
8229            if let Some(eq_pos) = arg.find('=') {
8230                let name = &arg[..eq_pos];
8231                let value = &arg[eq_pos + 1..];
8232                self.aliases.insert(name.to_string(), value.to_string());
8233            } else {
8234                // Show specific alias
8235                if let Some(value) = self.aliases.get(arg.as_str()) {
8236                    let line = format!("alias {arg}='{value}'\n");
8237                    self.write_stdout(line.as_bytes());
8238                } else {
8239                    let msg = format!("alias: {arg}: not found\n");
8240                    self.write_stderr(msg.as_bytes());
8241                    self.vm.state.last_status = 1;
8242                    return;
8243                }
8244            }
8245        }
8246        self.vm.state.last_status = 0;
8247    }
8248
8249    /// Execute `unalias [-a] name ...`.
8250    fn execute_unalias(&mut self, argv: &[String]) {
8251        let args = &argv[1..];
8252        if args.is_empty() {
8253            self.write_stderr(b"unalias: usage: unalias [-a] name ...\n");
8254            self.vm.state.last_status = 1;
8255            return;
8256        }
8257        for arg in args {
8258            if arg == "-a" {
8259                self.aliases.clear();
8260            } else if self.aliases.shift_remove(arg.as_str()).is_none() {
8261                let msg = format!("unalias: {arg}: not found\n");
8262                self.write_stderr(msg.as_bytes());
8263                self.vm.state.last_status = 1;
8264                return;
8265            }
8266        }
8267        self.vm.state.last_status = 0;
8268    }
8269
8270    /// Execute `type name ...` — report how each name would be interpreted.
8271    /// Checks aliases, functions, builtins, and utilities in that order.
8272    fn execute_type(&mut self, argv: &[String]) {
8273        let (flags, names) = Self::parse_type_args(&argv[1..]);
8274        let mut status = 0;
8275        for name in names {
8276            if !self.render_type_name(name, &flags) {
8277                status = 1;
8278            }
8279        }
8280        self.vm.state.last_status = status;
8281    }
8282
8283    fn parse_type_args<'a>(args: &'a [String]) -> (TypeFlags, Vec<&'a str>) {
8284        let mut flags = TypeFlags::default();
8285        let mut names = Vec::new();
8286        for arg in args {
8287            if arg.starts_with('-') && arg.len() > 1 {
8288                Self::apply_type_short_flags(&arg[1..], &mut flags);
8289            } else {
8290                names.push(arg.as_str());
8291            }
8292        }
8293        (flags, names)
8294    }
8295
8296    fn apply_type_short_flags(short: &str, flags: &mut TypeFlags) {
8297        for ch in short.chars() {
8298            match ch {
8299                'a' => flags.all = true,
8300                'f' => flags.skip_functions = true,
8301                'p' => flags.path_only = true,
8302                'P' => {
8303                    flags.path_only = true;
8304                    flags.force_path = true;
8305                }
8306                't' => flags.type_only = true,
8307                _ => {}
8308            }
8309        }
8310    }
8311
8312    fn render_type_name(&mut self, name: &str, flags: &TypeFlags) -> bool {
8313        let mut lookups = self.command_lookups(name, flags.skip_functions, flags.force_path);
8314        if flags.path_only {
8315            lookups.retain(|lookup| matches!(lookup.kind, CommandLookupKind::File));
8316        }
8317        if lookups.is_empty() {
8318            let msg = format!("wasmsh: type: {name}: not found\n");
8319            self.write_stderr(msg.as_bytes());
8320            return false;
8321        }
8322        let limit = if flags.all { usize::MAX } else { 1 };
8323        for lookup in lookups.into_iter().take(limit) {
8324            let line = format_type_lookup(&lookup, flags.type_only, flags.path_only);
8325            self.write_stdout(format!("{line}\n").as_bytes());
8326        }
8327        true
8328    }
8329
8330    /// Execute `builtin name [args...]` — skip alias and function lookup,
8331    /// invoke the named builtin directly.
8332    fn execute_builtin_keyword(&mut self, argv: &[String]) {
8333        if argv.len() < 2 {
8334            self.vm.state.last_status = 0;
8335            return;
8336        }
8337        let builtin_argv: Vec<String> = argv[1..].to_vec();
8338        let cmd_name = &builtin_argv[0];
8339        if let Some(builtin_fn) = self.builtins.get(cmd_name) {
8340            self.execute_resolved_command(ResolvedCommand::Builtin(builtin_fn), &builtin_argv);
8341        } else {
8342            let msg = format!("builtin: {cmd_name}: not a shell builtin\n");
8343            self.write_stderr(msg.as_bytes());
8344            self.vm.state.last_status = 1;
8345        }
8346    }
8347
8348    fn execute_command_keyword(&mut self, argv: &[String]) {
8349        let mut use_default_path = false;
8350        let mut verbose = false;
8351        let mut describe = false;
8352        let mut index = 1usize;
8353
8354        while let Some(arg) = argv.get(index) {
8355            match arg.as_str() {
8356                "-p" => use_default_path = true,
8357                "-v" => verbose = true,
8358                "-V" => describe = true,
8359                _ if arg.starts_with('-') && arg.len() > 1 => {}
8360                _ => break,
8361            }
8362            index += 1;
8363        }
8364
8365        let args = &argv[index..];
8366        if verbose || describe {
8367            let mut status = 0;
8368            for name in args {
8369                let lookups = self.command_lookups(name, true, use_default_path);
8370                let Some(lookup) = lookups.first() else {
8371                    status = 1;
8372                    continue;
8373                };
8374                let line = if verbose {
8375                    format_command_verbose(lookup)
8376                } else {
8377                    format_type_lookup(lookup, false, false)
8378                };
8379                self.write_stdout(format!("{line}\n").as_bytes());
8380            }
8381            self.vm.state.last_status = status;
8382            return;
8383        }
8384
8385        if args.is_empty() {
8386            self.vm.state.last_status = 0;
8387            return;
8388        }
8389
8390        let resolved = self.resolve_command_without_functions(&args[0], args);
8391        self.execute_resolved_command(resolved, args);
8392    }
8393
8394    fn execute_exec_keyword(&mut self, argv: &[String]) {
8395        if argv.len() <= 1 {
8396            self.vm.state.last_status = 0;
8397            return;
8398        }
8399        let args = &argv[1..];
8400        let resolved = self.resolve_command_without_functions(&args[0], args);
8401        self.execute_resolved_command(resolved, args);
8402    }
8403
8404    fn execute_hash(&mut self, argv: &[String]) {
8405        let mut print_paths = false;
8406        let mut status = 0;
8407
8408        for arg in &argv[1..] {
8409            match arg.as_str() {
8410                "-r" => {}
8411                "-t" => print_paths = true,
8412                name => {
8413                    let lookups = self.command_lookups(name, true, true);
8414                    let Some(lookup) = lookups
8415                        .iter()
8416                        .find(|lookup| matches!(lookup.kind, CommandLookupKind::File))
8417                    else {
8418                        status = 1;
8419                        continue;
8420                    };
8421                    if print_paths {
8422                        self.write_stdout(format!("{}\n", lookup.detail).as_bytes());
8423                    }
8424                }
8425            }
8426        }
8427
8428        self.vm.state.last_status = status;
8429    }
8430
8431    fn execute_times(&mut self) {
8432        self.write_stdout(b"0m0.000s 0m0.000s\n0m0.000s 0m0.000s\n");
8433        self.vm.state.last_status = 0;
8434    }
8435
8436    fn emit_pipeline_timing(&mut self, posix_format: bool, elapsed_seconds: f64) {
8437        let output = if posix_format {
8438            format!("real {elapsed_seconds:.3}\nuser 0.000\nsys 0.000\n")
8439        } else {
8440            let minutes = (elapsed_seconds / 60.0).floor() as u64;
8441            let seconds = elapsed_seconds - (minutes as f64 * 60.0);
8442            format!("real\t{minutes}m{seconds:.3}s\nuser\t0m0.000s\nsys\t0m0.000s\n")
8443        };
8444        self.write_stderr(output.as_bytes());
8445    }
8446
8447    fn execute_dirs(&mut self) {
8448        let mut dirs = vec![self.vm.state.cwd.clone()];
8449        dirs.extend(self.vm.state.dir_stack.iter().map(ToString::to_string));
8450        self.write_stdout(format!("{}\n", dirs.join(" ")).as_bytes());
8451        self.vm.state.last_status = 0;
8452    }
8453
8454    fn execute_pushd(&mut self, argv: &[String]) {
8455        let target = if let Some(path) = argv.get(1) {
8456            path.clone()
8457        } else if let Some(path) = self.vm.state.dir_stack.first() {
8458            path.to_string()
8459        } else {
8460            self.write_stderr(b"pushd: no other directory\n");
8461            self.vm.state.last_status = 1;
8462            return;
8463        };
8464
8465        let old_cwd = self.vm.state.cwd.clone();
8466        if !self.change_directory(&target) {
8467            return;
8468        }
8469        self.vm
8470            .state
8471            .dir_stack
8472            .insert(0, smol_str::SmolStr::from(old_cwd.as_str()));
8473        self.execute_dirs();
8474    }
8475
8476    fn execute_popd(&mut self) {
8477        let Some(target) = self.vm.state.dir_stack.first().cloned() else {
8478            self.write_stderr(b"popd: directory stack empty\n");
8479            self.vm.state.last_status = 1;
8480            return;
8481        };
8482        self.vm.state.dir_stack.remove(0);
8483        if !self.change_directory(&target) {
8484            return;
8485        }
8486        self.execute_dirs();
8487    }
8488
8489    fn execute_umask(&mut self, argv: &[String]) {
8490        if argv.len() <= 1 {
8491            self.write_stdout(format!("{:03o}\n", self.vm.state.umask).as_bytes());
8492            self.vm.state.last_status = 0;
8493            return;
8494        }
8495
8496        let value = argv[1].trim_start_matches('0');
8497        let value = if value.is_empty() { "0" } else { value };
8498        if let Ok(value) = u32::from_str_radix(value, 8) {
8499            self.vm.state.umask = value;
8500            self.vm.state.last_status = 0;
8501        } else {
8502            self.write_stderr(b"umask: invalid mode\n");
8503            self.vm.state.last_status = 1;
8504        }
8505    }
8506
8507    fn execute_wait(&mut self, argv: &[String]) {
8508        if argv.len() <= 1 {
8509            self.vm.state.last_status = 0;
8510            return;
8511        }
8512
8513        let mut status = 0;
8514        for arg in &argv[1..] {
8515            let Ok(pid) = arg.parse::<u32>() else {
8516                self.write_stderr(format!("wait: {arg}: not a pid or valid job spec\n").as_bytes());
8517                status = 1;
8518                continue;
8519            };
8520            if self.vm.state.last_background_pid != Some(pid) {
8521                self.write_stderr(
8522                    format!("wait: pid {pid} is not a child of this shell\n").as_bytes(),
8523                );
8524                status = 127;
8525            }
8526        }
8527        self.vm.state.last_status = status;
8528    }
8529
8530    fn execute_ulimit(&mut self, argv: &[String]) {
8531        if argv.len() <= 1 || argv.get(1).is_some_and(|arg| arg == "-a") {
8532            self.write_stdout(b"unlimited\n");
8533        }
8534        self.vm.state.last_status = 0;
8535    }
8536
8537    /// Execute `mapfile`/`readarray` — read stdin lines into an indexed array.
8538    /// Supports the common Bash flags needed by scripts in the sandbox model.
8539    fn execute_mapfile(&mut self, argv: &[String]) {
8540        let Ok(opts) = Self::parse_mapfile_args(&argv[1..]) else {
8541            self.vm.state.last_status = 1;
8542            return;
8543        };
8544        if opts.fd != 0 {
8545            self.write_stderr(b"wasmsh: mapfile: only file descriptor 0 is supported\n");
8546            self.vm.state.last_status = 1;
8547            return;
8548        }
8549
8550        let name_key = smol_str::SmolStr::from(opts.array_name.as_str());
8551        if opts.origin == 0
8552            || !matches!(
8553                self.vm
8554                    .state
8555                    .env
8556                    .get(name_key.as_str())
8557                    .map(|var| &var.value),
8558                Some(wasmsh_state::VarValue::IndexedArray(_))
8559            )
8560        {
8561            self.vm.state.init_indexed_array(name_key.clone());
8562        }
8563
8564        let Ok(bytes) = self.read_pending_input_bytes("mapfile") else {
8565            return;
8566        };
8567        self.populate_mapfile_array(&name_key, &bytes.unwrap_or_default(), &opts);
8568        self.vm.state.last_status = 0;
8569    }
8570
8571    fn parse_mapfile_args(args: &[String]) -> Result<MapfileOptions, ()> {
8572        let mut opts = MapfileOptions {
8573            strip_delimiter: false,
8574            delimiter: b'\n',
8575            count: None,
8576            origin: 0,
8577            skip: 0,
8578            fd: 0,
8579            array_name: "MAPFILE".to_string(),
8580        };
8581        let mut i = 0usize;
8582        while i < args.len() {
8583            match args[i].as_str() {
8584                "-t" => opts.strip_delimiter = true,
8585                "-d" => {
8586                    i += 1;
8587                    let Some(value) = args.get(i) else {
8588                        return Err(());
8589                    };
8590                    opts.delimiter = value.as_bytes().first().copied().unwrap_or(0);
8591                }
8592                "-n" => {
8593                    i += 1;
8594                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
8595                        return Err(());
8596                    };
8597                    opts.count = Some(value);
8598                }
8599                "-O" => {
8600                    i += 1;
8601                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
8602                        return Err(());
8603                    };
8604                    opts.origin = value;
8605                }
8606                "-s" => {
8607                    i += 1;
8608                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
8609                        return Err(());
8610                    };
8611                    opts.skip = value;
8612                }
8613                "-u" => {
8614                    i += 1;
8615                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<u32>().ok()) else {
8616                        return Err(());
8617                    };
8618                    opts.fd = value;
8619                }
8620                "-C" | "-c" => {
8621                    i += 1;
8622                    if args.get(i).is_none() {
8623                        return Err(());
8624                    }
8625                }
8626                value if value.starts_with('-') && value.len() > 1 => {}
8627                value => opts.array_name = value.to_string(),
8628            }
8629            i += 1;
8630        }
8631        Ok(opts)
8632    }
8633
8634    fn populate_mapfile_array(
8635        &mut self,
8636        name_key: &smol_str::SmolStr,
8637        text: &[u8],
8638        opts: &MapfileOptions,
8639    ) {
8640        let mut records = Vec::new();
8641        let mut current = Vec::new();
8642        for &byte in text {
8643            if byte == opts.delimiter {
8644                if !opts.strip_delimiter {
8645                    current.push(byte);
8646                }
8647                records.push(std::mem::take(&mut current));
8648            } else {
8649                current.push(byte);
8650            }
8651        }
8652        if !current.is_empty() {
8653            records.push(current);
8654        }
8655
8656        for (offset, record) in records
8657            .into_iter()
8658            .skip(opts.skip)
8659            .take(opts.count.unwrap_or(usize::MAX))
8660            .enumerate()
8661        {
8662            let value = String::from_utf8_lossy(&record).to_string();
8663            self.vm.state.set_array_element(
8664                name_key.clone(),
8665                &(opts.origin + offset).to_string(),
8666                smol_str::SmolStr::from(value.as_str()),
8667            );
8668        }
8669    }
8670
8671    fn change_directory(&mut self, target: &str) -> bool {
8672        let path = self.resolve_cwd_path(target);
8673        match self.fs.stat(&path) {
8674            Ok(meta) if meta.is_dir => {
8675                let old_pwd = self.vm.state.cwd.clone();
8676                self.vm.state.cwd.clone_from(&path);
8677                self.vm
8678                    .state
8679                    .set_var("OLDPWD".into(), smol_str::SmolStr::from(old_pwd.as_str()));
8680                self.vm
8681                    .state
8682                    .set_var("PWD".into(), smol_str::SmolStr::from(path.as_str()));
8683                self.vm.state.last_status = 0;
8684                true
8685            }
8686            Ok(_) => {
8687                self.write_stderr(format!("wasmsh: {target}: Not a directory\n").as_bytes());
8688                self.vm.state.last_status = 1;
8689                false
8690            }
8691            Err(_) => {
8692                self.write_stderr(
8693                    format!("wasmsh: {target}: No such file or directory\n").as_bytes(),
8694                );
8695                self.vm.state.last_status = 1;
8696                false
8697            }
8698        }
8699    }
8700
8701    /// Search `$PATH` directories in the VFS for a file. Returns the first match.
8702    fn search_path_for_file(&self, filename: &str) -> Option<String> {
8703        let path_var = self.vm.state.get_var("PATH")?;
8704        for dir in path_var.split(':') {
8705            if dir.is_empty() {
8706                continue;
8707            }
8708            let candidate = format!("{dir}/{filename}");
8709            let full = self.resolve_cwd_path(&candidate);
8710            if self.fs.stat(&full).is_ok() {
8711                return Some(full);
8712            }
8713        }
8714        None
8715    }
8716
8717    fn should_errexit(&self, and_or: &HirAndOr) -> bool {
8718        !self.exec.errexit_suppressed
8719            && and_or.rest.is_empty()
8720            && !and_or.first.negated
8721            && self.vm.state.get_var("SHOPT_e").as_deref() == Some("1")
8722            && self.vm.state.last_status != 0
8723            && self.exec.exit_requested.is_none()
8724    }
8725
8726    /// Execute `let expr1 expr2 ...` — evaluate each as arithmetic.
8727    /// Exit status: 0 if the last expression is non-zero, 1 if zero.
8728    fn execute_let(&mut self, argv: &[String]) {
8729        if argv.len() < 2 {
8730            self.vm
8731                .stderr
8732                .extend_from_slice(b"let: expression expected\n");
8733            self.vm.state.last_status = 1;
8734            return;
8735        }
8736        let mut last_val: i64 = 0;
8737        for expr in &argv[1..] {
8738            last_val = wasmsh_expand::eval_arithmetic(expr, &mut self.vm.state);
8739        }
8740        self.vm.state.last_status = i32::from(last_val == 0);
8741    }
8742
8743    /// Known `shopt` option names.
8744    const SHOPT_OPTIONS: &'static [&'static str] = &[
8745        "extglob",
8746        "nullglob",
8747        "dotglob",
8748        "globstar",
8749        "nocasematch",
8750        "nocaseglob",
8751        "failglob",
8752        "lastpipe",
8753        "expand_aliases",
8754        "sourcepath",
8755    ];
8756
8757    /// Execute `shopt [-s|-u] [optname ...]`.
8758    fn execute_shopt(&mut self, argv: &[String]) {
8759        let (set_mode, names) = Self::parse_shopt_args(&argv[1..]);
8760        if let Some(enable) = set_mode {
8761            self.shopt_set_options(&names, enable);
8762        } else {
8763            self.shopt_print_options(&names);
8764        }
8765    }
8766
8767    fn parse_shopt_args(args: &[String]) -> (Option<bool>, Vec<&str>) {
8768        let mut set_mode = None;
8769        let mut names = Vec::new();
8770
8771        for arg in args {
8772            match arg.as_str() {
8773                "-s" => set_mode = Some(true),
8774                "-u" => set_mode = Some(false),
8775                _ => names.push(arg.as_str()),
8776            }
8777        }
8778
8779        (set_mode, names)
8780    }
8781
8782    /// Set shopt options (`-s` or `-u`).
8783    fn shopt_set_options(&mut self, names: &[&str], enable: bool) {
8784        if names.is_empty() {
8785            self.vm
8786                .stderr
8787                .extend_from_slice(b"shopt: option name required\n");
8788            self.vm.state.last_status = 1;
8789            return;
8790        }
8791        let val = if enable { "1" } else { "0" };
8792        for name in names {
8793            if self.reject_invalid_shopt_name(name) {
8794                return;
8795            }
8796            self.set_shopt_value(name, val);
8797        }
8798        self.vm.state.last_status = 0;
8799    }
8800
8801    /// Print shopt option statuses. If `names` is empty, print all.
8802    fn shopt_print_options(&mut self, names: &[&str]) {
8803        let options_to_print: Vec<&str> = if names.is_empty() {
8804            Self::SHOPT_OPTIONS.to_vec()
8805        } else {
8806            names.to_vec()
8807        };
8808        for name in &options_to_print {
8809            if self.reject_invalid_shopt_name(name) {
8810                return;
8811            }
8812            let enabled = self.get_shopt_value(name);
8813            let status_str = if enabled { "on" } else { "off" };
8814            let line = format!("{name}\t{status_str}\n");
8815            self.write_stdout(line.as_bytes());
8816        }
8817        self.vm.state.last_status = 0;
8818    }
8819
8820    fn reject_invalid_shopt_name(&mut self, name: &str) -> bool {
8821        if Self::SHOPT_OPTIONS.contains(&name) {
8822            return false;
8823        }
8824
8825        let msg = format!("shopt: {name}: invalid shell option name\n");
8826        self.write_stderr(msg.as_bytes());
8827        self.vm.state.last_status = 1;
8828        true
8829    }
8830
8831    fn shopt_var_name(name: &str) -> String {
8832        format!("SHOPT_{name}")
8833    }
8834
8835    fn set_shopt_value(&mut self, name: &str, value: &str) {
8836        let var = Self::shopt_var_name(name);
8837        self.vm.state.set_var(
8838            smol_str::SmolStr::from(var.as_str()),
8839            smol_str::SmolStr::from(value),
8840        );
8841    }
8842
8843    fn get_shopt_value(&self, name: &str) -> bool {
8844        let var = Self::shopt_var_name(name);
8845        self.vm.state.get_var(&var).as_deref() == Some("1")
8846    }
8847
8848    fn is_set_option_enabled(&self, flag: char) -> bool {
8849        let var = format!("SHOPT_{flag}");
8850        self.vm.state.get_var(&var).as_deref() == Some("1")
8851    }
8852
8853    fn maybe_write_verbose_input(&mut self, input: &str, cc: &HirCompleteCommand) {
8854        if !self.is_set_option_enabled('v') {
8855            return;
8856        }
8857        let start = cc.span.start as usize;
8858        let end = cc.span.end as usize;
8859        let Some(snippet) = input.get(start..end) else {
8860            return;
8861        };
8862        if snippet.is_empty() {
8863            return;
8864        }
8865        self.write_stderr(snippet.as_bytes());
8866        if !snippet.ends_with('\n') {
8867            self.write_stderr(b"\n");
8868        }
8869    }
8870
8871    /// Execute `declare`/`typeset` with flag parsing.
8872    /// Supports: -i, -a, -A, -x, -r, -l, -u, -p, -n, name=value.
8873    fn execute_declare(&mut self, argv: &[String]) {
8874        let (flags, names) = parse_declare_flags(argv);
8875
8876        if flags.is_print || flags.is_functions || flags.is_function_names {
8877            self.declare_print(argv, &names);
8878            return;
8879        }
8880
8881        for &idx in &names {
8882            self.declare_one_name(argv, idx, &flags);
8883        }
8884        self.vm.state.last_status = 0;
8885    }
8886
8887    /// Handle `declare -p` printing.
8888    fn declare_print(&mut self, argv: &[String], names: &[usize]) {
8889        let (flags, _) = parse_declare_flags(argv);
8890        if flags.is_functions || flags.is_function_names {
8891            self.declare_print_functions(argv, names, flags.is_function_names);
8892            return;
8893        }
8894        self.declare_print_vars(argv, names);
8895    }
8896
8897    fn declare_print_functions(&mut self, argv: &[String], names: &[usize], names_only: bool) {
8898        let function_names: Vec<String> = if names.is_empty() {
8899            self.functions.keys().cloned().collect()
8900        } else {
8901            names.iter().map(|&idx| argv[idx].clone()).collect()
8902        };
8903        for name in function_names {
8904            if !self.functions.contains_key(name.as_str()) {
8905                continue;
8906            }
8907            let line = if names_only {
8908                format!("declare -f {name}\n")
8909            } else {
8910                format!("{name} () {{ :; }}\n")
8911            };
8912            self.write_stdout(line.as_bytes());
8913        }
8914        self.vm.state.last_status = 0;
8915    }
8916
8917    fn declare_print_vars(&mut self, argv: &[String], names: &[usize]) {
8918        if names.is_empty() {
8919            let vars: Vec<(String, String)> = self
8920                .vm
8921                .state
8922                .env
8923                .scopes
8924                .iter()
8925                .flat_map(|scope| {
8926                    scope
8927                        .iter()
8928                        .map(|(n, v)| (n.to_string(), v.value.as_scalar().to_string()))
8929                })
8930                .collect();
8931            for (name, val) in &vars {
8932                let line = format!("declare -- {name}=\"{val}\"\n");
8933                self.write_stdout(line.as_bytes());
8934            }
8935        } else {
8936            for &idx in names {
8937                let name_arg = &argv[idx];
8938                let name = name_arg
8939                    .find('=')
8940                    .map_or(name_arg.as_str(), |eq| &name_arg[..eq]);
8941                if let Some(var) = self.vm.state.env.get(name) {
8942                    let val = var.value.as_scalar();
8943                    let line = format!("declare -- {name}=\"{val}\"\n");
8944                    self.write_stdout(line.as_bytes());
8945                }
8946            }
8947        }
8948        self.vm.state.last_status = 0;
8949    }
8950
8951    /// Process a single name in a `declare`/`typeset` command.
8952    fn declare_one_name(&mut self, argv: &[String], idx: usize, flags: &DeclareFlags) {
8953        let name_arg = &argv[idx];
8954        let (name, value) = if let Some(eq) = name_arg.find('=') {
8955            (&name_arg[..eq], Some(&name_arg[eq + 1..]))
8956        } else {
8957            (name_arg.as_str(), None)
8958        };
8959
8960        if flags.is_assoc {
8961            self.vm
8962                .state
8963                .init_assoc_array(smol_str::SmolStr::from(name));
8964        } else if flags.is_indexed {
8965            self.vm
8966                .state
8967                .init_indexed_array(smol_str::SmolStr::from(name));
8968        }
8969
8970        if let Some(val) = value {
8971            self.declare_assign_value(name, val, flags);
8972        } else if !flags.is_assoc && !flags.is_indexed && self.vm.state.get_var(name).is_none() {
8973            self.vm
8974                .state
8975                .set_var(smol_str::SmolStr::from(name), smol_str::SmolStr::default());
8976        }
8977
8978        self.declare_apply_attributes(name, flags);
8979
8980        if flags.is_nameref {
8981            self.declare_apply_nameref(name);
8982        }
8983    }
8984
8985    /// Assign a value in `declare`, handling compound arrays and scalar transforms.
8986    fn declare_assign_value(&mut self, name: &str, val: &str, flags: &DeclareFlags) {
8987        let trimmed = val.trim();
8988        if trimmed.starts_with('(') && trimmed.ends_with(')') {
8989            self.declare_assign_compound(name, &trimmed[1..trimmed.len() - 1], flags);
8990            return;
8991        }
8992        let final_val = Self::transform_declare_scalar(trimmed, flags, &mut self.vm.state);
8993        self.vm.state.set_var(
8994            smol_str::SmolStr::from(name),
8995            smol_str::SmolStr::from(final_val.as_str()),
8996        );
8997    }
8998
8999    fn declare_assign_compound(&mut self, name: &str, inner: &str, flags: &DeclareFlags) {
9000        let name_key = smol_str::SmolStr::from(name);
9001        if flags.is_assoc || inner.contains("]=") {
9002            self.declare_assign_assoc_compound(&name_key, inner);
9003        } else {
9004            self.declare_assign_indexed_compound(&name_key, inner);
9005        }
9006    }
9007
9008    fn declare_assign_assoc_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
9009        self.vm.state.init_assoc_array(name_key.clone());
9010        for pair in Self::parse_assoc_pairs(inner) {
9011            self.vm.state.set_array_element(
9012                name_key.clone(),
9013                &pair.0,
9014                smol_str::SmolStr::from(pair.1.as_str()),
9015            );
9016        }
9017    }
9018
9019    fn declare_assign_indexed_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
9020        let elements = Self::parse_array_elements(inner);
9021        self.vm.state.init_indexed_array(name_key.clone());
9022        for (i, elem) in elements.iter().enumerate() {
9023            self.vm
9024                .state
9025                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
9026        }
9027    }
9028
9029    fn transform_declare_scalar(val: &str, flags: &DeclareFlags, state: &mut ShellState) -> String {
9030        if flags.is_integer {
9031            wasmsh_expand::eval_arithmetic(val, state).to_string()
9032        } else if flags.is_lower {
9033            val.to_lowercase()
9034        } else if flags.is_upper {
9035            val.to_uppercase()
9036        } else {
9037            val.to_string()
9038        }
9039    }
9040
9041    /// Apply export, readonly, integer attributes after declare assignment.
9042    fn declare_apply_attributes(&mut self, name: &str, flags: &DeclareFlags) {
9043        if let Some(var) = self.vm.state.env.get_mut(name) {
9044            if flags.is_export {
9045                var.exported = true;
9046            }
9047            if flags.is_readonly {
9048                var.readonly = true;
9049            }
9050            if flags.is_integer {
9051                var.integer = true;
9052            }
9053        }
9054    }
9055
9056    /// Apply nameref attribute for `declare -n`.
9057    fn declare_apply_nameref(&mut self, name: &str) {
9058        let target_value = if let Some(eq_pos) = name.find('=') {
9059            smol_str::SmolStr::from(&name[eq_pos + 1..])
9060        } else if let Some(var) = self.vm.state.env.get(name) {
9061            var.value.as_scalar()
9062        } else {
9063            smol_str::SmolStr::default()
9064        };
9065        let actual_name = name.find('=').map_or(name, |eq| &name[..eq]);
9066        self.vm.state.env.set(
9067            smol_str::SmolStr::from(actual_name),
9068            wasmsh_state::ShellVar {
9069                value: wasmsh_state::VarValue::Scalar(target_value),
9070                exported: false,
9071                readonly: false,
9072                integer: false,
9073                nameref: true,
9074            },
9075        );
9076    }
9077
9078    fn should_stop_execution(&self) -> bool {
9079        self.exec.break_depth > 0
9080            || self.exec.loop_continue
9081            || self.exec.exit_requested.is_some()
9082            || self.exec.resource_exhausted
9083    }
9084
9085    /// Check resource limits (step budget, output limit, cancellation).
9086    /// Returns true if execution should stop. Emits a diagnostic on first violation.
9087    fn check_resource_limits(&mut self) -> bool {
9088        if self.exec.resource_exhausted {
9089            return true;
9090        }
9091        if self.vm.begin_step().is_err() {
9092            self.exec.resource_exhausted = true;
9093            self.exec.stop_reason = self.vm.stop_reason().cloned();
9094            return true;
9095        }
9096        false
9097    }
9098
9099    fn execute_body(&mut self, body: &[HirCompleteCommand]) {
9100        for cc in body {
9101            if self.should_stop_execution() || self.check_resource_limits() {
9102                break;
9103            }
9104            if self.is_set_option_enabled('n') {
9105                continue;
9106            }
9107            self.execute_complete_command(cc);
9108        }
9109    }
9110
9111    fn execute_complete_command(&mut self, cc: &HirCompleteCommand) {
9112        for and_or in &cc.list {
9113            if self.should_stop_execution() || self.is_set_option_enabled('n') {
9114                break;
9115            }
9116            self.execute_and_or(and_or);
9117            if self.exec.exit_requested.is_some() {
9118                break;
9119            }
9120            self.handle_post_and_or(and_or);
9121        }
9122    }
9123
9124    /// Expand a word value via command substitution and word expansion.
9125    fn expand_assignment_value(&mut self, value: Option<&Word>) -> String {
9126        if let Some(w) = value {
9127            let resolved = self.resolve_command_subst(std::slice::from_ref(w));
9128            wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
9129        } else {
9130            String::new()
9131        }
9132    }
9133
9134    /// Execute a variable assignment, handling array syntax:
9135    /// - `name=(val1 val2 ...)` -- indexed array compound assignment
9136    /// - `name[idx]=val` -- single element assignment
9137    /// - `name+=(val1 val2 ...)` -- array append
9138    /// - Plain `name=val` -- scalar assignment
9139    fn execute_assignment(&mut self, raw_name: &smol_str::SmolStr, value: Option<&Word>) {
9140        let (name_str, is_append) = Self::split_assignment_name(raw_name.as_str());
9141        if self.try_assign_array_element(name_str, value) {
9142            return;
9143        }
9144
9145        let val_str = self.expand_assignment_value(value);
9146        let trimmed = val_str.trim();
9147        if trimmed.starts_with('(') && trimmed.ends_with(')') {
9148            self.assign_compound_array(name_str, trimmed, is_append);
9149            return;
9150        }
9151
9152        let final_val = self.resolve_scalar_assignment_value(name_str, &val_str, is_append);
9153        self.vm
9154            .state
9155            .set_var(smol_str::SmolStr::from(name_str), final_val.into());
9156    }
9157
9158    fn split_assignment_name(name: &str) -> (&str, bool) {
9159        if let Some(stripped) = name.strip_suffix('+') {
9160            (stripped, true)
9161        } else {
9162            (name, false)
9163        }
9164    }
9165
9166    fn parse_array_element_assignment(name: &str) -> Option<(&str, &str)> {
9167        let bracket_pos = name.find('[')?;
9168        name.ends_with(']')
9169            .then_some((&name[..bracket_pos], &name[bracket_pos + 1..name.len() - 1]))
9170    }
9171
9172    fn try_assign_array_element(&mut self, name: &str, value: Option<&Word>) -> bool {
9173        let Some((base, index)) = Self::parse_array_element_assignment(name) else {
9174            return false;
9175        };
9176        let val = self.expand_assignment_value(value);
9177        self.vm
9178            .state
9179            .set_array_element(smol_str::SmolStr::from(base), index, val.into());
9180        true
9181    }
9182
9183    fn resolve_scalar_assignment_value(
9184        &mut self,
9185        name: &str,
9186        value: &str,
9187        is_append: bool,
9188    ) -> String {
9189        if self.vm.state.env.get(name).is_some_and(|v| v.integer) {
9190            return self.eval_integer_assignment(name, value, is_append);
9191        }
9192        if is_append {
9193            return format!(
9194                "{}{}",
9195                self.vm.state.get_var(name).unwrap_or_default(),
9196                value
9197            );
9198        }
9199        value.to_string()
9200    }
9201
9202    fn eval_integer_assignment(&mut self, name: &str, value: &str, is_append: bool) -> String {
9203        let arith_input = if is_append {
9204            format!(
9205                "{}+{}",
9206                self.vm.state.get_var(name).unwrap_or_default(),
9207                value
9208            )
9209        } else {
9210            value.to_string()
9211        };
9212        wasmsh_expand::eval_arithmetic(&arith_input, &mut self.vm.state).to_string()
9213    }
9214
9215    /// Assign a compound array value `(...)` to a variable.
9216    fn assign_compound_array(&mut self, name_str: &str, val_str: &str, is_append: bool) {
9217        let inner = &val_str[1..val_str.len() - 1];
9218        let elements = Self::parse_array_elements(inner);
9219        let name_key = smol_str::SmolStr::from(name_str);
9220
9221        if is_append {
9222            self.vm.state.append_array(name_str, elements);
9223            return;
9224        }
9225
9226        if Self::is_assoc_array_assignment(inner, &elements) {
9227            self.assign_assoc_array(&name_key, inner);
9228            return;
9229        }
9230        self.assign_indexed_array(&name_key, &elements);
9231    }
9232
9233    fn is_assoc_array_assignment(inner: &str, elements: &[smol_str::SmolStr]) -> bool {
9234        !elements.is_empty() && inner.contains('[') && inner.contains("]=")
9235    }
9236
9237    fn assign_assoc_array(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
9238        self.vm.state.init_assoc_array(name_key.clone());
9239        for (key, value) in Self::parse_assoc_pairs(inner) {
9240            self.vm.state.set_array_element(
9241                name_key.clone(),
9242                &key,
9243                smol_str::SmolStr::from(value.as_str()),
9244            );
9245        }
9246    }
9247
9248    fn assign_indexed_array(
9249        &mut self,
9250        name_key: &smol_str::SmolStr,
9251        elements: &[smol_str::SmolStr],
9252    ) {
9253        self.vm.state.init_indexed_array(name_key.clone());
9254        for (i, elem) in elements.iter().enumerate() {
9255            self.vm
9256                .state
9257                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
9258        }
9259    }
9260
9261    fn push_array_element(elements: &mut Vec<smol_str::SmolStr>, current: &mut String) {
9262        if current.is_empty() {
9263            return;
9264        }
9265        elements.push(smol_str::SmolStr::from(current.as_str()));
9266        current.clear();
9267    }
9268
9269    /// Parse space-separated array elements from the inner content of `(...)`.
9270    /// Respects quoting (single and double quotes).
9271    fn parse_array_elements(inner: &str) -> Vec<smol_str::SmolStr> {
9272        let mut elements = Vec::new();
9273        let mut current = String::new();
9274        let mut state = ArrayParseState::default();
9275
9276        for ch in inner.chars() {
9277            match state.process_char(ch) {
9278                ArrayCharAction::Append(c) => current.push(c),
9279                ArrayCharAction::Skip => {}
9280                ArrayCharAction::SplitField => {
9281                    Self::push_array_element(&mut elements, &mut current);
9282                }
9283            }
9284        }
9285        Self::push_array_element(&mut elements, &mut current);
9286        elements
9287    }
9288
9289    /// Parse `[key]=value` pairs from associative array compound assignment.
9290    fn parse_assoc_pairs(inner: &str) -> Vec<(String, String)> {
9291        let mut pairs = Vec::new();
9292        let mut pos = 0;
9293        let bytes = inner.as_bytes();
9294
9295        while pos < bytes.len() {
9296            Self::skip_ascii_whitespace(bytes, &mut pos);
9297            if pos >= bytes.len() {
9298                break;
9299            }
9300            if let Some(key) = Self::parse_assoc_key(inner, &mut pos) {
9301                pairs.push((key, Self::parse_assoc_value(inner, &mut pos)));
9302                continue;
9303            }
9304            Self::skip_non_whitespace(bytes, &mut pos);
9305        }
9306        pairs
9307    }
9308
9309    fn skip_ascii_whitespace(bytes: &[u8], pos: &mut usize) {
9310        while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() {
9311            *pos += 1;
9312        }
9313    }
9314
9315    fn skip_non_whitespace(bytes: &[u8], pos: &mut usize) {
9316        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
9317            *pos += 1;
9318        }
9319    }
9320
9321    fn parse_assoc_key(inner: &str, pos: &mut usize) -> Option<String> {
9322        let bytes = inner.as_bytes();
9323        if *pos >= bytes.len() || bytes[*pos] != b'[' {
9324            return None;
9325        }
9326
9327        *pos += 1;
9328        let key_start = *pos;
9329        while *pos < bytes.len() && bytes[*pos] != b']' {
9330            *pos += 1;
9331        }
9332        let key = inner[key_start..*pos].to_string();
9333        if *pos < bytes.len() {
9334            *pos += 1;
9335        }
9336        if *pos < bytes.len() && bytes[*pos] == b'=' {
9337            *pos += 1;
9338        }
9339        Some(key)
9340    }
9341
9342    /// Parse a single value in an associative array assignment (may be quoted).
9343    fn parse_assoc_value(inner: &str, pos: &mut usize) -> String {
9344        let bytes = inner.as_bytes();
9345        match bytes.get(*pos).copied() {
9346            Some(b'"') => Self::parse_double_quoted_assoc_value(bytes, pos),
9347            Some(b'\'') => Self::parse_single_quoted_assoc_value(bytes, pos),
9348            _ => Self::parse_unquoted_assoc_value(bytes, pos),
9349        }
9350    }
9351
9352    fn parse_double_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
9353        let mut value = String::new();
9354        *pos += 1;
9355        while *pos < bytes.len() && bytes[*pos] != b'"' {
9356            if bytes[*pos] == b'\\' && *pos + 1 < bytes.len() {
9357                *pos += 1;
9358            }
9359            value.push(bytes[*pos] as char);
9360            *pos += 1;
9361        }
9362        if *pos < bytes.len() {
9363            *pos += 1;
9364        }
9365        value
9366    }
9367
9368    fn parse_single_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
9369        let mut value = String::new();
9370        *pos += 1;
9371        while *pos < bytes.len() && bytes[*pos] != b'\'' {
9372            value.push(bytes[*pos] as char);
9373            *pos += 1;
9374        }
9375        if *pos < bytes.len() {
9376            *pos += 1;
9377        }
9378        value
9379    }
9380
9381    fn parse_unquoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
9382        let mut value = String::new();
9383        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
9384            value.push(bytes[*pos] as char);
9385            *pos += 1;
9386        }
9387        value
9388    }
9389
9390    /// Maximum number of arguments after glob expansion.
9391    const MAX_GLOB_RESULTS: usize = 10_000;
9392
9393    /// Expand glob patterns in argv against the VFS.
9394    /// Supports: basic glob (`*`, `?`, `[...]`), globstar (`**`), nullglob,
9395    /// dotglob, and extglob patterns.
9396    /// When `set -f` (noglob) is active, glob expansion is skipped entirely.
9397    /// Expand globs in argv, skipping entries tagged as quoted.
9398    fn expand_globs_tagged(&mut self, argv: Vec<(String, bool)>) -> Vec<String> {
9399        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
9400            return argv.into_iter().map(|(s, _)| s).collect();
9401        }
9402        let nullglob = self.get_shopt_value("nullglob");
9403        let dotglob = self.get_shopt_value("dotglob");
9404        let globstar = self.get_shopt_value("globstar");
9405        let extglob = self.get_shopt_value("extglob");
9406
9407        let mut result = Vec::new();
9408        for (arg, quoted) in argv {
9409            if quoted {
9410                result.push(arg);
9411            } else {
9412                result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
9413            }
9414        }
9415        result.truncate(Self::MAX_GLOB_RESULTS);
9416        result
9417    }
9418
9419    fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
9420        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
9421            return argv;
9422        }
9423        let nullglob = self.get_shopt_value("nullglob");
9424        let dotglob = self.get_shopt_value("dotglob");
9425        let globstar = self.get_shopt_value("globstar");
9426        let extglob = self.get_shopt_value("extglob");
9427
9428        let mut result = Vec::new();
9429        for arg in argv {
9430            result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
9431        }
9432        result.truncate(Self::MAX_GLOB_RESULTS);
9433        result
9434    }
9435
9436    #[allow(clippy::fn_params_excessive_bools)]
9437    fn expand_glob_arg(
9438        &self,
9439        arg: String,
9440        nullglob: bool,
9441        dotglob: bool,
9442        globstar: bool,
9443        extglob: bool,
9444    ) -> Vec<String> {
9445        if !Self::is_glob_pattern(&arg, extglob) {
9446            return vec![arg];
9447        }
9448        if globstar && arg.contains("**") {
9449            return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
9450        }
9451        self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
9452    }
9453
9454    fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
9455        let has_bracket_class = arg.contains('[') && arg.contains(']');
9456        arg.contains('*')
9457            || arg.contains('?')
9458            || has_bracket_class
9459            || (extglob && has_extglob_pattern(arg))
9460    }
9461
9462    fn expand_globstar_arg(
9463        &self,
9464        arg: String,
9465        nullglob: bool,
9466        dotglob: bool,
9467        extglob: bool,
9468    ) -> Vec<String> {
9469        let mut matches = self.expand_globstar(&arg, dotglob, extglob);
9470        matches.sort();
9471        self.finalize_glob_matches(arg, matches, nullglob)
9472    }
9473
9474    fn expand_standard_glob_arg(
9475        &self,
9476        arg: String,
9477        nullglob: bool,
9478        dotglob: bool,
9479        extglob: bool,
9480    ) -> Vec<String> {
9481        let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
9482            return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
9483        };
9484        let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
9485        self.finalize_glob_matches(arg, matches, nullglob)
9486    }
9487
9488    fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
9489        let Some(slash_pos) = arg.rfind('/') else {
9490            return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
9491        };
9492
9493        let dir_part = &arg[..=slash_pos];
9494        if Self::path_segment_has_glob(dir_part) {
9495            return None;
9496        }
9497
9498        Some((
9499            self.resolve_cwd_path(dir_part),
9500            arg[slash_pos + 1..].to_string(),
9501            Some(dir_part.to_string()),
9502        ))
9503    }
9504
9505    fn path_segment_has_glob(path: &str) -> bool {
9506        path.contains('*') || path.contains('?') || path.contains('[')
9507    }
9508
9509    fn read_glob_matches(
9510        &self,
9511        dir: &str,
9512        pattern: &str,
9513        prefix: Option<&str>,
9514        dotglob: bool,
9515        extglob: bool,
9516    ) -> Vec<String> {
9517        let Ok(entries) = self.fs.read_dir(dir) else {
9518            return Vec::new();
9519        };
9520
9521        let mut matches: Vec<String> = entries
9522            .iter()
9523            .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
9524            .map(|e| match prefix {
9525                Some(prefix) => format!("{prefix}{}", e.name),
9526                None => e.name.clone(),
9527            })
9528            .collect();
9529        matches.sort();
9530        matches
9531    }
9532
9533    #[allow(clippy::unused_self)]
9534    fn finalize_glob_matches(
9535        &self,
9536        arg: String,
9537        matches: Vec<String>,
9538        nullglob: bool,
9539    ) -> Vec<String> {
9540        if !matches.is_empty() {
9541            return matches;
9542        }
9543        if nullglob {
9544            Vec::new()
9545        } else {
9546            vec![arg]
9547        }
9548    }
9549
9550    /// Expand a globstar (**) pattern against the VFS with recursive directory traversal.
9551    fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
9552        // Split pattern into segments by /
9553        let segments: Vec<&str> = pattern.split('/').collect();
9554        let base_dir = self.vm.state.cwd.clone();
9555        let mut matches = Vec::new();
9556        self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
9557        matches
9558    }
9559
9560    /// Recursive walk for globstar expansion.
9561    fn globstar_walk(
9562        &self,
9563        dir: &str,
9564        segments: &[&str],
9565        seg_idx: usize,
9566        prefix: &str,
9567        dotglob: bool,
9568        extglob: bool,
9569        matches: &mut Vec<String>,
9570    ) {
9571        if seg_idx >= segments.len() {
9572            return;
9573        }
9574
9575        let seg = segments[seg_idx];
9576        if seg == "**" {
9577            self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
9578            return;
9579        }
9580        self.globstar_walk_segment(
9581            dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
9582        );
9583    }
9584
9585    fn globstar_walk_wildcard(
9586        &self,
9587        dir: &str,
9588        segments: &[&str],
9589        seg_idx: usize,
9590        prefix: &str,
9591        dotglob: bool,
9592        extglob: bool,
9593        matches: &mut Vec<String>,
9594    ) {
9595        if seg_idx + 1 < segments.len() {
9596            self.globstar_walk(
9597                dir,
9598                segments,
9599                seg_idx + 1,
9600                prefix,
9601                dotglob,
9602                extglob,
9603                matches,
9604            );
9605        }
9606
9607        let Ok(entries) = self.fs.read_dir(dir) else {
9608            return;
9609        };
9610        for entry in &entries {
9611            if !dotglob && entry.name.starts_with('.') {
9612                continue;
9613            }
9614            let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
9615            if self.fs.stat(&child_path).is_ok_and(|m| m.is_dir) {
9616                self.globstar_walk(
9617                    &child_path,
9618                    segments,
9619                    seg_idx,
9620                    &child_prefix,
9621                    dotglob,
9622                    extglob,
9623                    matches,
9624                );
9625            }
9626        }
9627    }
9628
9629    #[allow(clippy::too_many_arguments)]
9630    fn globstar_walk_segment(
9631        &self,
9632        dir: &str,
9633        seg: &str,
9634        segments: &[&str],
9635        seg_idx: usize,
9636        prefix: &str,
9637        dotglob: bool,
9638        extglob: bool,
9639        matches: &mut Vec<String>,
9640    ) {
9641        let Ok(entries) = self.fs.read_dir(dir) else {
9642            return;
9643        };
9644        let is_last = seg_idx == segments.len() - 1;
9645
9646        for entry in &entries {
9647            if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
9648                continue;
9649            }
9650            self.globstar_handle_matched_entry(
9651                dir,
9652                segments,
9653                seg_idx,
9654                prefix,
9655                dotglob,
9656                extglob,
9657                matches,
9658                &entry.name,
9659                is_last,
9660            );
9661        }
9662    }
9663
9664    #[allow(clippy::too_many_arguments)]
9665    fn globstar_handle_matched_entry(
9666        &self,
9667        dir: &str,
9668        segments: &[&str],
9669        seg_idx: usize,
9670        prefix: &str,
9671        dotglob: bool,
9672        extglob: bool,
9673        matches: &mut Vec<String>,
9674        name: &str,
9675        is_last: bool,
9676    ) {
9677        let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
9678        if is_last {
9679            matches.push(child_prefix);
9680            return;
9681        }
9682        let is_dir = self.fs.stat(&child_path).is_ok_and(|m| m.is_dir);
9683        if is_dir {
9684            self.globstar_walk(
9685                &child_path,
9686                segments,
9687                seg_idx + 1,
9688                &child_prefix,
9689                dotglob,
9690                extglob,
9691                matches,
9692            );
9693        }
9694    }
9695
9696    fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
9697        let child_path = if dir == "/" {
9698            format!("/{name}")
9699        } else {
9700            format!("{dir}/{name}")
9701        };
9702        let child_prefix = if prefix.is_empty() {
9703            name.to_string()
9704        } else {
9705            format!("{prefix}/{name}")
9706        };
9707        (child_path, child_prefix)
9708    }
9709
9710    /// Write data to a file path, reporting errors to stderr.
9711    fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
9712        match self.fs.open(path, opts) {
9713            Ok(h) => {
9714                if let Err(e) = self.fs.write_file(h, data) {
9715                    self.write_stderr(format!("wasmsh: write error: {e}\n").as_bytes());
9716                }
9717                self.fs.close(h);
9718            }
9719            Err(e) => {
9720                self.write_stderr(format!("wasmsh: {target}: {e}\n").as_bytes());
9721            }
9722        }
9723    }
9724
9725    fn current_stdout_len(&self) -> usize {
9726        for capture in self.exec.output_captures.iter().rev() {
9727            if capture.capture_stdout {
9728                return capture.stdout.len();
9729            }
9730        }
9731        self.vm.stdout.len()
9732    }
9733
9734    /// Capture stdout data from the given position, truncating the active stdout buffer.
9735    fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
9736        for capture in self.exec.output_captures.iter_mut().rev() {
9737            if capture.capture_stdout {
9738                let data = capture.stdout[from..].to_vec();
9739                capture.stdout.truncate(from);
9740                return data;
9741            }
9742        }
9743
9744        let data = self.vm.stdout[from..].to_vec();
9745        self.vm.stdout.truncate(from);
9746        data
9747    }
9748
9749    /// Drain the active stderr buffer.
9750    fn take_stderr(&mut self) -> Vec<u8> {
9751        for capture in self.exec.output_captures.iter_mut().rev() {
9752            if capture.capture_stderr {
9753                return std::mem::take(&mut capture.stderr);
9754            }
9755        }
9756        std::mem::take(&mut self.vm.stderr)
9757    }
9758
9759    fn process_subst_out_sink_mut(&mut self, path: &str) -> Option<&mut PendingProcessSubstOut> {
9760        for scope in self.proc_subst_out_scopes.iter_mut().rev() {
9761            if let Some(index) = scope.iter().position(|sink| sink.path == path) {
9762                return scope.get_mut(index);
9763            }
9764        }
9765        None
9766    }
9767
9768    fn write_process_subst_out_with_parent(
9769        &mut self,
9770        path: &str,
9771        data: &[u8],
9772        clear: bool,
9773    ) -> bool {
9774        for scope_index in (0..self.proc_subst_out_scopes.len()).rev() {
9775            let maybe_index = self.proc_subst_out_scopes[scope_index]
9776                .iter()
9777                .position(|sink| sink.path == path);
9778            if let Some(index) = maybe_index {
9779                let mut sink = self.proc_subst_out_scopes[scope_index].remove(index);
9780                if clear {
9781                    sink.clear();
9782                }
9783                sink.write_with_parent(self, data);
9784                self.proc_subst_out_scopes[scope_index].insert(index, sink);
9785                return true;
9786            }
9787        }
9788        false
9789    }
9790
9791    fn prepare_exec_io(&mut self, redirections: &[HirRedirection]) -> Result<Option<ExecIo>, ()> {
9792        let mut exec_io = self.current_exec_io.clone().unwrap_or_default();
9793        let mut handled_any = false;
9794        for redir in redirections {
9795            if self.apply_hir_redir(redir, &mut exec_io)? {
9796                handled_any = true;
9797            }
9798        }
9799        Ok(handled_any.then_some(exec_io))
9800    }
9801
9802    fn apply_hir_redir(
9803        &mut self,
9804        redir: &HirRedirection,
9805        exec_io: &mut ExecIo,
9806    ) -> Result<bool, ()> {
9807        match redir.op {
9808            RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
9809                self.apply_heredoc_redir(redir, exec_io);
9810                Ok(true)
9811            }
9812            RedirectionOp::HereString => {
9813                self.apply_herestring_redir(redir, exec_io);
9814                Ok(true)
9815            }
9816            RedirectionOp::Input => self.apply_input_redir(redir, exec_io).map(|()| true),
9817            RedirectionOp::Output
9818            | RedirectionOp::Append
9819            | RedirectionOp::Clobber
9820            | RedirectionOp::AppendBoth => self.apply_write_redir(redir, exec_io).map(|()| true),
9821            RedirectionOp::DupOutput => {
9822                self.apply_dup_output_redir(redir, exec_io);
9823                Ok(true)
9824            }
9825            RedirectionOp::DupInput => {
9826                self.apply_dup_input_redir(redir, exec_io);
9827                Ok(true)
9828            }
9829            _ => Ok(false),
9830        }
9831    }
9832
9833    fn resolve_redir_target(&mut self, redir: &HirRedirection) -> String {
9834        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
9835        let resolved_target = resolved.first().unwrap_or(&redir.target);
9836        wasmsh_expand::expand_word(resolved_target, &mut self.vm.state)
9837    }
9838
9839    fn apply_heredoc_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
9840        if let Some(body) = &redir.here_doc_body {
9841            let expanded = wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
9842            exec_io
9843                .fds_mut()
9844                .set_input(InputTarget::Bytes(expanded.into_bytes()));
9845        }
9846    }
9847
9848    fn apply_herestring_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
9849        let content = self.resolve_redir_target(redir);
9850        let mut data = content.into_bytes();
9851        data.push(b'\n');
9852        exec_io.fds_mut().set_input(InputTarget::Bytes(data));
9853    }
9854
9855    fn apply_input_redir(
9856        &mut self,
9857        redir: &HirRedirection,
9858        exec_io: &mut ExecIo,
9859    ) -> Result<(), ()> {
9860        let target = self.resolve_redir_target(redir);
9861        let path = self.resolve_cwd_path(&target);
9862        match self.fs.stat(&path) {
9863            Ok(metadata) if !metadata.is_dir => {
9864                exec_io.fds_mut().set_input(InputTarget::File {
9865                    path,
9866                    remove_after_read: false,
9867                });
9868                Ok(())
9869            }
9870            Ok(_) => self.fail_input_redir(&target, "Is a directory"),
9871            Err(_) => self.fail_input_redir(&target, "No such file or directory"),
9872        }
9873    }
9874
9875    fn fail_input_redir(&mut self, target: &str, reason: &str) -> Result<(), ()> {
9876        let msg = format!("wasmsh: {target}: {reason}\n");
9877        self.write_stderr(msg.as_bytes());
9878        self.vm.state.last_status = 1;
9879        Err(())
9880    }
9881
9882    fn apply_write_redir(
9883        &mut self,
9884        redir: &HirRedirection,
9885        exec_io: &mut ExecIo,
9886    ) -> Result<(), ()> {
9887        let target = self.resolve_redir_target(redir);
9888        let path = self.resolve_cwd_path(&target);
9889        let append = matches!(redir.op, RedirectionOp::Append | RedirectionOp::AppendBoth);
9890        let clear_before = matches!(redir.op, RedirectionOp::Output | RedirectionOp::Clobber);
9891
9892        if matches!(redir.op, RedirectionOp::Output) && self.noclobber_rejects(&path, &target) {
9893            return Err(());
9894        }
9895
9896        let destination = self.open_write_destination(path, &target, append, clear_before)?;
9897        Self::attach_write_destination(redir, exec_io, destination);
9898        Ok(())
9899    }
9900
9901    fn open_write_destination(
9902        &mut self,
9903        path: String,
9904        target: &str,
9905        append: bool,
9906        clear_before: bool,
9907    ) -> Result<OutputTarget, ()> {
9908        if self.process_subst_out_sink_mut(&path).is_some() {
9909            if clear_before {
9910                if let Some(sink) = self.process_subst_out_sink_mut(&path) {
9911                    sink.clear();
9912                }
9913            }
9914            return Ok(OutputTarget::ProcessSubst { path });
9915        }
9916        match self.fs.open_write_sink(&path, append) {
9917            Ok(sink) => Ok(OutputTarget::File {
9918                path,
9919                append,
9920                sink: Rc::new(RefCell::new(sink)),
9921            }),
9922            Err(err) => {
9923                let msg = format!("wasmsh: {target}: {err}\n");
9924                self.write_stderr(msg.as_bytes());
9925                self.vm.state.last_status = 1;
9926                Err(())
9927            }
9928        }
9929    }
9930
9931    fn attach_write_destination(
9932        redir: &HirRedirection,
9933        exec_io: &mut ExecIo,
9934        destination: OutputTarget,
9935    ) {
9936        let default_fd = if matches!(redir.op, RedirectionOp::AppendBoth) {
9937            FD_BOTH
9938        } else {
9939            1
9940        };
9941        match redir.fd.unwrap_or(default_fd) {
9942            FD_BOTH => {
9943                exec_io.fds_mut().open_output(1, destination.clone());
9944                exec_io.fds_mut().open_output(2, destination);
9945            }
9946            2 => exec_io.fds_mut().open_output(2, destination),
9947            _ => exec_io.fds_mut().open_output(1, destination),
9948        }
9949    }
9950
9951    fn apply_dup_output_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
9952        let target = self.resolve_redir_target(redir);
9953        let source_fd = redir.fd.unwrap_or(1);
9954        if target == "-" {
9955            exec_io.fds_mut().close(source_fd);
9956        } else if let Ok(target_fd) = target.parse() {
9957            exec_io.fds_mut().dup_output(source_fd, target_fd);
9958        }
9959    }
9960
9961    fn apply_dup_input_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
9962        let target = self.resolve_redir_target(redir);
9963        let source_fd = redir.fd.unwrap_or(0);
9964        if target == "-" {
9965            exec_io.fds_mut().close(source_fd);
9966        } else if let Ok(target_fd) = target.parse() {
9967            exec_io.fds_mut().dup_input(source_fd, target_fd);
9968        }
9969    }
9970
9971    /// Apply redirections: for `>` and `>>`, write captured stdout/stderr to file.
9972    /// For `<`, read file content (handled pre-execution).
9973    /// Supports fd-specific redirections (2>, 2>>) and &> (both stdout and stderr).
9974    fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
9975        for redir in redirections {
9976            if !self.apply_single_redirection(redir, stdout_before) {
9977                return;
9978            }
9979        }
9980    }
9981
9982    fn apply_single_redirection(&mut self, redir: &HirRedirection, stdout_before: usize) -> bool {
9983        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
9984        let resolved_target = resolved.first().unwrap_or(&redir.target);
9985        let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
9986        let path = self.resolve_cwd_path(&target);
9987        let fd = redir.fd.unwrap_or(1);
9988        match redir.op {
9989            RedirectionOp::Output => {
9990                if self.noclobber_rejects(&path, &target) {
9991                    return false;
9992                }
9993                self.apply_output_redir(&path, &target, fd, stdout_before);
9994            }
9995            RedirectionOp::Clobber => {
9996                self.apply_output_redir(&path, &target, fd, stdout_before);
9997            }
9998            RedirectionOp::Append => {
9999                self.apply_append_redir(&path, &target, fd, stdout_before);
10000            }
10001            RedirectionOp::AppendBoth => {
10002                self.apply_append_redir(&path, &target, FD_BOTH, stdout_before);
10003            }
10004            RedirectionOp::DupOutput => {
10005                self.apply_dup_output_redir_inline(redir, &target, stdout_before);
10006            }
10007            #[allow(unreachable_patterns)]
10008            _ => {}
10009        }
10010        true
10011    }
10012
10013    fn apply_dup_output_redir_inline(
10014        &mut self,
10015        redir: &HirRedirection,
10016        target: &str,
10017        stdout_before: usize,
10018    ) {
10019        let source_fd = redir.fd.unwrap_or(1);
10020        if target == "-" {
10021            if source_fd == 2 {
10022                self.take_stderr();
10023            } else {
10024                self.capture_stdout(stdout_before);
10025            }
10026            return;
10027        }
10028        let target_fd = target.parse::<u32>().ok();
10029        if target_fd == Some(1) && source_fd == 2 {
10030            let stderr_data = self.take_stderr();
10031            self.write_stdout(&stderr_data);
10032        } else if target_fd == Some(2) && source_fd == 1 {
10033            let stdout_data = self.capture_stdout(stdout_before);
10034            self.write_stderr(&stdout_data);
10035        }
10036    }
10037
10038    /// Apply `>` output redirection for a specific fd.
10039    fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
10040        let data = if fd == FD_BOTH {
10041            let mut combined = self.capture_stdout(stdout_before);
10042            combined.extend_from_slice(&self.take_stderr());
10043            combined
10044        } else if fd == 2 {
10045            self.take_stderr()
10046        } else {
10047            self.capture_stdout(stdout_before)
10048        };
10049        if self.write_process_subst_out_with_parent(path, &data, true) {
10050            return;
10051        }
10052        self.write_to_file(path, target, &data, OpenOptions::write());
10053    }
10054
10055    /// Apply `>>` append redirection for a specific fd.
10056    fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
10057        let data = if fd == FD_BOTH {
10058            let mut combined = self.capture_stdout(stdout_before);
10059            combined.extend_from_slice(&self.take_stderr());
10060            combined
10061        } else if fd == 2 {
10062            self.take_stderr()
10063        } else {
10064            self.capture_stdout(stdout_before)
10065        };
10066        if self.write_process_subst_out_with_parent(path, &data, false) {
10067            return;
10068        }
10069        self.write_to_file(path, target, &data, OpenOptions::append());
10070    }
10071
10072    fn noclobber_rejects(&mut self, path: &str, target: &str) -> bool {
10073        if self.vm.state.get_var("SHOPT_C").as_deref() != Some("1") {
10074            return false;
10075        }
10076        if self.fs.stat(path).is_err() {
10077            return false;
10078        }
10079        self.write_stderr(format!("wasmsh: {target}: cannot overwrite existing file\n").as_bytes());
10080        self.vm.state.last_status = 1;
10081        true
10082    }
10083}
10084
10085#[cfg(not(target_arch = "wasm32"))]
10086type PipelineStartedAt = std::time::Instant;
10087#[cfg(target_arch = "wasm32")]
10088type PipelineStartedAt = ();
10089
10090#[cfg(not(target_arch = "wasm32"))]
10091fn pipeline_started_at() -> PipelineStartedAt {
10092    std::time::Instant::now()
10093}
10094
10095#[cfg(target_arch = "wasm32")]
10096fn pipeline_started_at() -> PipelineStartedAt {}
10097
10098#[cfg(not(target_arch = "wasm32"))]
10099fn started_elapsed_seconds(started: PipelineStartedAt) -> f64 {
10100    started.elapsed().as_secs_f64()
10101}
10102
10103#[cfg(target_arch = "wasm32")]
10104fn started_elapsed_seconds(_: PipelineStartedAt) -> f64 {
10105    0.0
10106}
10107
10108/// Convert a protocol diagnostic level to a VM diagnostic level.
10109fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
10110    match level {
10111        DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
10112        DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
10113        DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
10114        _ => wasmsh_vm::DiagLevel::Info,
10115    }
10116}
10117
10118impl Default for WorkerRuntime {
10119    fn default() -> Self {
10120        Self::new()
10121    }
10122}
10123
10124#[cfg(test)]
10125mod tests {
10126    use super::*;
10127
10128    fn first_and_or(source: &str) -> HirAndOr {
10129        let ast = wasmsh_parse::parse(source).unwrap();
10130        let hir = wasmsh_hir::lower(&ast);
10131        hir.items[0].list[0].clone()
10132    }
10133
10134    fn get_stdout(events: &[WorkerEvent]) -> String {
10135        let mut out = Vec::new();
10136        for event in events {
10137            if let WorkerEvent::Stdout(data) = event {
10138                out.extend_from_slice(data);
10139            }
10140        }
10141        String::from_utf8(out).unwrap_or_default()
10142    }
10143
10144    fn get_stderr(events: &[WorkerEvent]) -> String {
10145        let mut out = Vec::new();
10146        for event in events {
10147            if let WorkerEvent::Stderr(data) = event {
10148                out.extend_from_slice(data);
10149            }
10150        }
10151        String::from_utf8(out).unwrap_or_default()
10152    }
10153
10154    fn get_exit(events: &[WorkerEvent]) -> i32 {
10155        events
10156            .iter()
10157            .find_map(|event| match event {
10158                WorkerEvent::Exit(status) => Some(*status),
10159                _ => None,
10160            })
10161            .unwrap_or(-1)
10162    }
10163
10164    fn has_output_limit_diagnostic(events: &[WorkerEvent]) -> bool {
10165        events.iter().any(|event| {
10166            matches!(
10167                event,
10168                WorkerEvent::Diagnostic(_, message) if message.contains("output limit exceeded")
10169            )
10170        })
10171    }
10172
10173    #[test]
10174    fn output_limit_exposes_structured_exhaustion_reason() {
10175        let mut runtime = WorkerRuntime::new();
10176        runtime.handle_command(HostCommand::Init {
10177            step_budget: 0,
10178            allowed_hosts: vec![],
10179        });
10180        runtime.set_output_byte_limit(3);
10181
10182        let events = runtime.handle_command(HostCommand::Run {
10183            input: "echo hello".into(),
10184        });
10185
10186        assert_eq!(get_exit(&events), 128);
10187        assert!(has_output_limit_diagnostic(&events));
10188        assert_eq!(
10189            runtime.exec.stop_reason,
10190            Some(StopReason::Exhausted(ExhaustionReason {
10191                category: BudgetCategory::VisibleOutputBytes,
10192                used: 6,
10193                limit: 3,
10194            }))
10195        );
10196    }
10197
10198    #[test]
10199    fn recursion_limit_exposes_structured_exhaustion_reason() {
10200        let mut runtime = WorkerRuntime::new();
10201        runtime.handle_command(HostCommand::Init {
10202            step_budget: 0,
10203            allowed_hosts: vec![],
10204        });
10205        runtime.set_recursion_limit(2);
10206
10207        let events = runtime.handle_command(HostCommand::Run {
10208            input: "f(){ f; }\nf".into(),
10209        });
10210
10211        assert_eq!(get_exit(&events), 128);
10212        assert!(get_stderr(&events).contains("maximum recursion depth exceeded"));
10213        assert_eq!(
10214            runtime.exec.stop_reason,
10215            Some(StopReason::Exhausted(ExhaustionReason {
10216                category: BudgetCategory::RecursionDepth,
10217                used: 3,
10218                limit: 2,
10219            }))
10220        );
10221    }
10222
10223    #[test]
10224    fn pipe_limit_exposes_structured_exhaustion_reason() {
10225        let mut runtime = WorkerRuntime::new();
10226        runtime.handle_command(HostCommand::Init {
10227            step_budget: 0,
10228            allowed_hosts: vec![],
10229        });
10230        runtime.set_pipe_byte_limit(1);
10231
10232        let events = runtime.handle_command(HostCommand::Run {
10233            input: "printf 'ab' | cat".into(),
10234        });
10235
10236        assert_eq!(get_exit(&events), 128);
10237        assert!(events.iter().any(|event| {
10238            matches!(
10239                event,
10240                WorkerEvent::Diagnostic(_, message) if message.contains("pipe buffer limit exceeded")
10241            )
10242        }));
10243        assert!(matches!(
10244            runtime.exec.stop_reason,
10245            Some(StopReason::Exhausted(ExhaustionReason {
10246                category: BudgetCategory::PipeBytes,
10247                ..
10248            }))
10249        ));
10250    }
10251
10252    #[test]
10253    fn vm_subset_boundary_accepts_simple_builtin_and_or() {
10254        let runtime = WorkerRuntime::new();
10255        let program = runtime
10256            .lower_vm_subset_and_or(&first_and_or("true && echo ok"))
10257            .expect("simple builtin and/or should lower");
10258        assert!(!program.instructions.is_empty());
10259    }
10260
10261    #[test]
10262    fn vm_subset_boundary_rejects_multi_stage_pipeline() {
10263        let runtime = WorkerRuntime::new();
10264        let reason = runtime
10265            .lower_vm_subset_and_or(&first_and_or("echo hello | cat"))
10266            .unwrap_err();
10267        assert_eq!(
10268            reason,
10269            VmSubsetFallbackReason::Lowering(LoweringError::Unsupported(
10270                "pipeline shape is outside the VM subset"
10271            ))
10272        );
10273    }
10274
10275    #[test]
10276    fn vm_subset_boundary_rejects_alias_expansion() {
10277        let mut runtime = WorkerRuntime::new();
10278        runtime
10279            .vm
10280            .state
10281            .set_var("SHOPT_expand_aliases".into(), "1".into());
10282        runtime.aliases.insert("echo".into(), "printf".into());
10283        let reason = runtime
10284            .lower_vm_subset_and_or(&first_and_or("echo hello"))
10285            .unwrap_err();
10286        assert_eq!(reason, VmSubsetFallbackReason::AliasExpansion);
10287    }
10288
10289    #[test]
10290    fn streaming_yes_head_respects_visible_output_limit() {
10291        let mut runtime = WorkerRuntime::new();
10292        runtime.handle_command(HostCommand::Init {
10293            step_budget: 0,
10294            allowed_hosts: vec![],
10295        });
10296        runtime.vm.limits.output_byte_limit = 10;
10297
10298        let events = runtime.handle_command(HostCommand::Run {
10299            input: "yes | head -n 5".into(),
10300        });
10301
10302        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
10303        assert!(!has_output_limit_diagnostic(&events));
10304    }
10305
10306    #[test]
10307    fn streaming_yes_cat_head_respects_visible_output_limit() {
10308        let mut runtime = WorkerRuntime::new();
10309        runtime.handle_command(HostCommand::Init {
10310            step_budget: 0,
10311            allowed_hosts: vec![],
10312        });
10313        runtime.vm.limits.output_byte_limit = 10;
10314
10315        let events = runtime.handle_command(HostCommand::Run {
10316            input: "yes | cat | head -n 5".into(),
10317        });
10318
10319        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
10320        assert!(!has_output_limit_diagnostic(&events));
10321    }
10322
10323    #[test]
10324    fn streaming_yes_head_wc_respects_visible_output_limit() {
10325        let mut runtime = WorkerRuntime::new();
10326        runtime.handle_command(HostCommand::Init {
10327            step_budget: 0,
10328            allowed_hosts: vec![],
10329        });
10330        runtime.vm.limits.output_byte_limit = 8;
10331
10332        let events = runtime.handle_command(HostCommand::Run {
10333            input: "yes | head -n 5 | wc -l".into(),
10334        });
10335
10336        assert_eq!(get_stdout(&events), "5\n");
10337        assert!(!has_output_limit_diagnostic(&events));
10338    }
10339
10340    #[test]
10341    fn streaming_cat_file_head_respects_visible_output_limit() {
10342        let mut runtime = WorkerRuntime::new();
10343        runtime.handle_command(HostCommand::Init {
10344            step_budget: 0,
10345            allowed_hosts: vec![],
10346        });
10347        runtime.handle_command(HostCommand::WriteFile {
10348            path: "/big.txt".into(),
10349            data: b"abcdefghijklmnopqrstuvwxyz".to_vec(),
10350        });
10351        runtime.vm.limits.output_byte_limit = 10;
10352
10353        let events = runtime.handle_command(HostCommand::Run {
10354            input: "cat /big.txt | head -c 10".into(),
10355        });
10356
10357        assert_eq!(get_stdout(&events), "abcdefghij");
10358        assert!(!has_output_limit_diagnostic(&events));
10359    }
10360
10361    #[test]
10362    fn streaming_yes_tr_head_respects_visible_output_limit() {
10363        let mut runtime = WorkerRuntime::new();
10364        runtime.handle_command(HostCommand::Init {
10365            step_budget: 0,
10366            allowed_hosts: vec![],
10367        });
10368        runtime.vm.limits.output_byte_limit = 10;
10369
10370        let events = runtime.handle_command(HostCommand::Run {
10371            input: "yes | tr y z | head -n 5".into(),
10372        });
10373
10374        assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
10375        assert!(!has_output_limit_diagnostic(&events));
10376    }
10377
10378    #[test]
10379    fn streaming_yes_grep_head_respects_visible_output_limit() {
10380        let mut runtime = WorkerRuntime::new();
10381        runtime.handle_command(HostCommand::Init {
10382            step_budget: 0,
10383            allowed_hosts: vec![],
10384        });
10385        runtime.vm.limits.output_byte_limit = 10;
10386
10387        let events = runtime.handle_command(HostCommand::Run {
10388            input: "yes | grep y | head -n 5".into(),
10389        });
10390
10391        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
10392        assert!(!has_output_limit_diagnostic(&events));
10393    }
10394
10395    #[test]
10396    fn streaming_yes_tee_head_respects_visible_output_limit() {
10397        let mut runtime = WorkerRuntime::new();
10398        runtime.handle_command(HostCommand::Init {
10399            step_budget: 0,
10400            allowed_hosts: vec![],
10401        });
10402        runtime.vm.limits.output_byte_limit = 10;
10403
10404        let events = runtime.handle_command(HostCommand::Run {
10405            input: "yes | tee /tee.txt | head -n 5".into(),
10406        });
10407
10408        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
10409        assert!(!has_output_limit_diagnostic(&events));
10410
10411        let file_events = runtime.handle_command(HostCommand::ReadFile {
10412            path: "/tee.txt".into(),
10413        });
10414        assert_eq!(get_stdout(&file_events), "y\ny\ny\ny\ny\n");
10415    }
10416
10417    #[test]
10418    fn streaming_buffered_sort_tee_cat_preserves_sorted_output() {
10419        let mut runtime = WorkerRuntime::new();
10420        runtime.handle_command(HostCommand::Init {
10421            step_budget: 0,
10422            allowed_hosts: vec![],
10423        });
10424
10425        let events = runtime.handle_command(HostCommand::Run {
10426            input: "printf 'b\\na\\n' | sort | tee /sorted.txt | cat".into(),
10427        });
10428
10429        assert_eq!(get_stdout(&events), "a\nb\n");
10430        let file_events = runtime.handle_command(HostCommand::ReadFile {
10431            path: "/sorted.txt".into(),
10432        });
10433        assert_eq!(get_stdout(&file_events), "a\nb\n");
10434    }
10435
10436    #[test]
10437    fn streaming_yes_rev_head_respects_visible_output_limit() {
10438        let mut runtime = WorkerRuntime::new();
10439        runtime.handle_command(HostCommand::Init {
10440            step_budget: 0,
10441            allowed_hosts: vec![],
10442        });
10443        runtime.vm.limits.output_byte_limit = 10;
10444
10445        let events = runtime.handle_command(HostCommand::Run {
10446            input: "yes | rev | head -n 5".into(),
10447        });
10448
10449        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
10450        assert!(!has_output_limit_diagnostic(&events));
10451    }
10452
10453    #[test]
10454    fn streaming_echo_cut_head_respects_visible_output_limit() {
10455        let mut runtime = WorkerRuntime::new();
10456        runtime.handle_command(HostCommand::Init {
10457            step_budget: 0,
10458            allowed_hosts: vec![],
10459        });
10460        runtime.vm.limits.output_byte_limit = 6;
10461
10462        let events = runtime.handle_command(HostCommand::Run {
10463            input: "echo abc:def | cut -d: -f2 | head -c 4".into(),
10464        });
10465
10466        assert_eq!(get_stdout(&events), "def\n");
10467        assert!(!has_output_limit_diagnostic(&events));
10468    }
10469
10470    #[test]
10471    fn streaming_echo_tail_head_respects_visible_output_limit() {
10472        let mut runtime = WorkerRuntime::new();
10473        runtime.handle_command(HostCommand::Init {
10474            step_budget: 0,
10475            allowed_hosts: vec![],
10476        });
10477        runtime.vm.limits.output_byte_limit = 3;
10478
10479        let events = runtime.handle_command(HostCommand::Run {
10480            input: "echo -e 'a\\nb\\nc' | tail -n 2 | head -n 1".into(),
10481        });
10482
10483        assert_eq!(get_stdout(&events), "b\n");
10484        assert!(!has_output_limit_diagnostic(&events));
10485    }
10486
10487    #[test]
10488    fn streaming_yes_bat_head_respects_visible_output_limit() {
10489        let mut runtime = WorkerRuntime::new();
10490        runtime.handle_command(HostCommand::Init {
10491            step_budget: 0,
10492            allowed_hosts: vec![],
10493        });
10494        let expected = "    1   │ y\n    2   │ y\n";
10495        runtime.vm.limits.output_byte_limit = expected.len() as u64;
10496
10497        let events = runtime.handle_command(HostCommand::Run {
10498            input: "yes | bat --style=numbers | head -n 2".into(),
10499        });
10500
10501        assert_eq!(get_stdout(&events), expected);
10502        assert!(!has_output_limit_diagnostic(&events));
10503    }
10504
10505    #[test]
10506    fn streaming_yes_sed_head_respects_visible_output_limit() {
10507        let mut runtime = WorkerRuntime::new();
10508        runtime.handle_command(HostCommand::Init {
10509            step_budget: 0,
10510            allowed_hosts: vec![],
10511        });
10512        runtime.vm.limits.output_byte_limit = 10;
10513
10514        let events = runtime.handle_command(HostCommand::Run {
10515            input: "yes | sed 's/y/z/' | head -n 5".into(),
10516        });
10517
10518        assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
10519        assert!(!has_output_limit_diagnostic(&events));
10520    }
10521
10522    #[test]
10523    fn streaming_echo_paste_serial_head_respects_visible_output_limit() {
10524        let mut runtime = WorkerRuntime::new();
10525        runtime.handle_command(HostCommand::Init {
10526            step_budget: 0,
10527            allowed_hosts: vec![],
10528        });
10529        runtime.vm.limits.output_byte_limit = 6;
10530
10531        let events = runtime.handle_command(HostCommand::Run {
10532            input: "echo -e 'a\\nb\\nc' | paste -s -d , | head -c 6".into(),
10533        });
10534
10535        assert_eq!(get_stdout(&events), "a,b,c\n");
10536        assert!(!has_output_limit_diagnostic(&events));
10537    }
10538
10539    #[test]
10540    fn streaming_echo_column_head_respects_visible_output_limit() {
10541        let mut runtime = WorkerRuntime::new();
10542        runtime.handle_command(HostCommand::Init {
10543            step_budget: 0,
10544            allowed_hosts: vec![],
10545        });
10546        runtime.vm.limits.output_byte_limit = 4;
10547
10548        let events = runtime.handle_command(HostCommand::Run {
10549            input: "echo abc | column | head -c 4".into(),
10550        });
10551
10552        assert_eq!(get_stdout(&events), "abc\n");
10553        assert!(!has_output_limit_diagnostic(&events));
10554    }
10555
10556    #[test]
10557    fn streaming_echo_uniq_head_respects_visible_output_limit() {
10558        let mut runtime = WorkerRuntime::new();
10559        runtime.handle_command(HostCommand::Init {
10560            step_budget: 0,
10561            allowed_hosts: vec![],
10562        });
10563        runtime.vm.limits.output_byte_limit = 6;
10564
10565        let events = runtime.handle_command(HostCommand::Run {
10566            input: "echo -e 'a\\na\\nb' | uniq | head -n 2".into(),
10567        });
10568
10569        assert_eq!(get_stdout(&events), "a\nb\n");
10570        assert!(!has_output_limit_diagnostic(&events));
10571    }
10572
10573    #[test]
10574    fn streaming_buffered_printf_sort_head_respects_visible_output_limit() {
10575        let mut runtime = WorkerRuntime::new();
10576        runtime.handle_command(HostCommand::Init {
10577            step_budget: 0,
10578            allowed_hosts: vec![],
10579        });
10580        runtime.vm.limits.output_byte_limit = 2;
10581
10582        let events = runtime.handle_command(HostCommand::Run {
10583            input: "printf 'b\\na\\n' | sort | head -n 1".into(),
10584        });
10585
10586        assert_eq!(get_stdout(&events), "a\n");
10587        assert!(!has_output_limit_diagnostic(&events));
10588    }
10589
10590    #[test]
10591    fn streaming_buffered_function_stage_preserves_output() {
10592        let mut runtime = WorkerRuntime::new();
10593        runtime.handle_command(HostCommand::Init {
10594            step_budget: 0,
10595            allowed_hosts: vec![],
10596        });
10597
10598        let events = runtime.handle_command(HostCommand::Run {
10599            input: "f(){ cat; }\nprintf hi | f | head -c 2".into(),
10600        });
10601
10602        assert_eq!(get_stdout(&events), "hi");
10603        assert!(!has_output_limit_diagnostic(&events));
10604    }
10605
10606    #[test]
10607    fn streaming_buffered_function_pipe_stderr_respects_visible_output_limit() {
10608        let mut runtime = WorkerRuntime::new();
10609        runtime.handle_command(HostCommand::Init {
10610            step_budget: 0,
10611            allowed_hosts: vec![],
10612        });
10613        runtime.vm.limits.output_byte_limit = 8;
10614
10615        let events = runtime.handle_command(HostCommand::Run {
10616            input: "f(){ echo out; echo err >&2; }\nf |& head -n 2".into(),
10617        });
10618
10619        assert_eq!(get_stdout(&events), "out\nerr\n");
10620        assert!(!has_output_limit_diagnostic(&events));
10621    }
10622
10623    #[test]
10624    fn scheduled_group_stage_pipe_stderr_preserves_output() {
10625        let mut runtime = WorkerRuntime::new();
10626        runtime.handle_command(HostCommand::Init {
10627            step_budget: 0,
10628            allowed_hosts: vec![],
10629        });
10630
10631        let events = runtime.handle_command(HostCommand::Run {
10632            input: "printf x | { cat; echo err >&2; } |& cat".into(),
10633        });
10634
10635        let stdout = get_stdout(&events);
10636        assert!(stdout.contains('x'));
10637        assert!(stdout.contains("err"));
10638    }
10639
10640    #[test]
10641    fn streaming_tee_pipe_stderr_preserves_output_and_stage_status() {
10642        let mut runtime = WorkerRuntime::new();
10643        runtime.handle_command(HostCommand::Init {
10644            step_budget: 0,
10645            allowed_hosts: vec![],
10646        });
10647
10648        let events = runtime.handle_command(HostCommand::Run {
10649            input: "printf x | tee / |& cat\necho ${PIPESTATUS[*]}".into(),
10650        });
10651
10652        let stdout = get_stdout(&events);
10653        assert!(stdout.contains('x'));
10654        assert!(stdout.contains("tee: /: is a directory: /"));
10655        assert!(stdout.contains("0 1 0"));
10656        assert_eq!(get_stderr(&events), "");
10657    }
10658
10659    #[test]
10660    fn streaming_tee_pipe_stderr_respects_pipefail() {
10661        let mut runtime = WorkerRuntime::new();
10662        runtime.handle_command(HostCommand::Init {
10663            step_budget: 0,
10664            allowed_hosts: vec![],
10665        });
10666
10667        let events = runtime.handle_command(HostCommand::Run {
10668            input: "set -o pipefail\nprintf x | tee / |& cat".into(),
10669        });
10670
10671        assert_eq!(runtime.vm.state.last_status, 1);
10672        let stdout = get_stdout(&events);
10673        assert!(stdout.contains('x'));
10674        assert!(stdout.contains("tee: /: is a directory: /"));
10675    }
10676
10677    #[test]
10678    fn generic_pipeline_capture_does_not_count_hidden_stage_output() {
10679        let mut runtime = WorkerRuntime::new();
10680        runtime.handle_command(HostCommand::Init {
10681            step_budget: 0,
10682            allowed_hosts: vec![],
10683        });
10684        runtime.vm.limits.output_byte_limit = 2;
10685
10686        let events = runtime.handle_command(HostCommand::Run {
10687            input: "echo -e 'a\\nb' | grep b".into(),
10688        });
10689
10690        assert_eq!(get_stdout(&events), "b\n");
10691        assert!(!has_output_limit_diagnostic(&events));
10692    }
10693
10694    #[test]
10695    fn generic_pipeline_file_capture_preserves_redirection_behavior() {
10696        let mut runtime = WorkerRuntime::new();
10697        runtime.handle_command(HostCommand::Init {
10698            step_budget: 0,
10699            allowed_hosts: vec![],
10700        });
10701
10702        let events = runtime.handle_command(HostCommand::Run {
10703            input: "echo -e 'a\\nb' | grep b >/filtered.txt | wc -l".into(),
10704        });
10705
10706        assert_eq!(get_stdout(&events), "0\n");
10707
10708        let file_events = runtime.handle_command(HostCommand::ReadFile {
10709            path: "/filtered.txt".into(),
10710        });
10711        assert_eq!(get_stdout(&file_events), "b\n");
10712    }
10713
10714    #[test]
10715    fn scheduler_single_redirect_only_command_creates_target_file() {
10716        let mut runtime = WorkerRuntime::new();
10717        runtime.handle_command(HostCommand::Init {
10718            step_budget: 0,
10719            allowed_hosts: vec![],
10720        });
10721
10722        let events = runtime.handle_command(HostCommand::Run {
10723            input: "> /created.txt".into(),
10724        });
10725
10726        assert_eq!(runtime.vm.state.last_status, 0);
10727        assert_eq!(get_stdout(&events), "");
10728        assert_eq!(get_stderr(&events), "");
10729
10730        let file_events = runtime.handle_command(HostCommand::ReadFile {
10731            path: "/created.txt".into(),
10732        });
10733        assert_eq!(get_stdout(&file_events), "");
10734    }
10735
10736    #[test]
10737    fn command_substitution_keeps_inner_stderr_visible() {
10738        let mut runtime = WorkerRuntime::new();
10739        runtime.handle_command(HostCommand::Init {
10740            step_budget: 0,
10741            allowed_hosts: vec![],
10742        });
10743
10744        let events = runtime.handle_command(HostCommand::Run {
10745            input: "echo $(printf 'hello'; echo err >&2)".into(),
10746        });
10747
10748        assert_eq!(get_stdout(&events), "hello\n");
10749        assert_eq!(get_stderr(&events), "err\n");
10750    }
10751
10752    #[test]
10753    fn command_substitution_isolates_shell_state() {
10754        let mut runtime = WorkerRuntime::new();
10755        runtime.handle_command(HostCommand::Init {
10756            step_budget: 0,
10757            allowed_hosts: vec![],
10758        });
10759
10760        let events = runtime.handle_command(HostCommand::Run {
10761            input: "foo=before; echo $(foo=after; printf hi); echo $foo".into(),
10762        });
10763
10764        assert_eq!(get_stdout(&events), "hi\nbefore\n");
10765    }
10766
10767    #[test]
10768    fn process_substitution_out_feeds_inner_command() {
10769        let mut runtime = WorkerRuntime::new();
10770        runtime.handle_command(HostCommand::Init {
10771            step_budget: 0,
10772            allowed_hosts: vec![],
10773        });
10774
10775        let events = runtime.handle_command(HostCommand::Run {
10776            input: "printf hi > >(cat)".into(),
10777        });
10778
10779        assert_eq!(get_stdout(&events), "hi");
10780        assert_eq!(get_stderr(&events), "");
10781        assert_eq!(runtime.vm.state.last_status, 0);
10782    }
10783
10784    #[test]
10785    fn process_substitution_out_runs_schedulable_inner_pipeline() {
10786        let mut runtime = WorkerRuntime::new();
10787        runtime.handle_command(HostCommand::Init {
10788            step_budget: 0,
10789            allowed_hosts: vec![],
10790        });
10791
10792        let events = runtime.handle_command(HostCommand::Run {
10793            input: "printf 'a\\nb\\n' > >(head -n 1 | cat)".into(),
10794        });
10795
10796        assert_eq!(get_stdout(&events), "a\n");
10797        assert_eq!(get_stderr(&events), "");
10798        assert_eq!(runtime.vm.state.last_status, 0);
10799    }
10800
10801    #[test]
10802    fn process_substitution_out_runs_live_tail_pipeline() {
10803        let mut runtime = WorkerRuntime::new();
10804        runtime.handle_command(HostCommand::Init {
10805            step_budget: 0,
10806            allowed_hosts: vec![],
10807        });
10808
10809        runtime.proc_subst_out_scopes.push(Vec::new());
10810        let path = runtime.register_process_subst_out("tail -n 1 | cat");
10811
10812        {
10813            let sink = runtime
10814                .process_subst_out_sink_mut(&path)
10815                .expect("registered process substitution sink");
10816            match &sink.mode {
10817                PendingProcessSubstOutMode::Live { .. } => {}
10818                PendingProcessSubstOutMode::Buffered { .. } => {
10819                    panic!("expected live process substitution runner")
10820                }
10821            }
10822            sink.write(b"a\nb\n");
10823        }
10824
10825        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
10826        runtime.flush_process_subst_out_scope(scope);
10827        assert_eq!(runtime.vm.stdout, b"b\n");
10828    }
10829
10830    #[test]
10831    fn process_substitution_out_runs_live_buffered_pipeline() {
10832        let mut runtime = WorkerRuntime::new();
10833        runtime.handle_command(HostCommand::Init {
10834            step_budget: 0,
10835            allowed_hosts: vec![],
10836        });
10837
10838        runtime.proc_subst_out_scopes.push(Vec::new());
10839        let path = runtime.register_process_subst_out("sort | cat");
10840
10841        {
10842            let sink = runtime
10843                .process_subst_out_sink_mut(&path)
10844                .expect("registered process substitution sink");
10845            match &sink.mode {
10846                PendingProcessSubstOutMode::Live { runner } => {
10847                    assert!(runner.isolated_runtime.is_some());
10848                }
10849                PendingProcessSubstOutMode::Buffered { .. } => {
10850                    panic!("expected live buffered process substitution runner")
10851                }
10852            }
10853            sink.write(b"b\na\n");
10854        }
10855
10856        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
10857        runtime.flush_process_subst_out_scope(scope);
10858        assert_eq!(runtime.vm.stdout, b"a\nb\n");
10859    }
10860
10861    #[test]
10862    fn process_substitution_in_registers_live_reader_and_cleans_up() {
10863        let mut runtime = WorkerRuntime::new();
10864        runtime.handle_command(HostCommand::Init {
10865            step_budget: 0,
10866            allowed_hosts: vec![],
10867        });
10868
10869        runtime.proc_subst_in_scopes.push(Vec::new());
10870        let path = runtime
10871            .execute_process_subst_in("yes | head -n 2")
10872            .to_string();
10873        assert!(runtime.fs.stat(&path).is_ok());
10874
10875        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
10876        assert_eq!(get_stdout(&file), "y\ny\n");
10877        assert!(runtime.fs.stat(&path).is_err());
10878
10879        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
10880        runtime.flush_process_subst_in_scope(scope);
10881        assert!(runtime.fs.stat(&path).is_err());
10882    }
10883
10884    #[test]
10885    fn process_substitution_in_registers_live_sed_reader_and_cleans_up() {
10886        let mut runtime = WorkerRuntime::new();
10887        runtime.handle_command(HostCommand::Init {
10888            step_budget: 0,
10889            allowed_hosts: vec![],
10890        });
10891
10892        runtime.proc_subst_in_scopes.push(Vec::new());
10893        let path = runtime
10894            .execute_process_subst_in("yes | sed 's/y/z/' | head -n 2")
10895            .to_string();
10896        assert!(runtime.fs.stat(&path).is_ok());
10897
10898        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
10899        assert_eq!(get_stdout(&file), "z\nz\n");
10900        assert!(runtime.fs.stat(&path).is_err());
10901
10902        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
10903        runtime.flush_process_subst_in_scope(scope);
10904        assert!(runtime.fs.stat(&path).is_err());
10905    }
10906
10907    #[test]
10908    fn process_substitution_in_runs_live_buffered_reader_and_cleans_up() {
10909        let mut runtime = WorkerRuntime::new();
10910        runtime.handle_command(HostCommand::Init {
10911            step_budget: 0,
10912            allowed_hosts: vec![],
10913        });
10914
10915        runtime.proc_subst_in_scopes.push(Vec::new());
10916        let path = runtime
10917            .execute_process_subst_in("printf 'b\\na\\n' | sort")
10918            .to_string();
10919
10920        assert!(runtime.fs.stat(&path).is_ok());
10921        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
10922        assert_eq!(get_stdout(&file), "a\nb\n");
10923        assert!(runtime.fs.stat(&path).is_err());
10924
10925        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
10926        runtime.flush_process_subst_in_scope(scope);
10927        assert!(runtime.fs.stat(&path).is_err());
10928    }
10929
10930    #[test]
10931    fn live_process_substitution_runner_consumes_before_flush() {
10932        let mut runtime = WorkerRuntime::new();
10933        runtime.handle_command(HostCommand::Init {
10934            step_budget: 0,
10935            allowed_hosts: vec![],
10936        });
10937
10938        runtime.proc_subst_out_scopes.push(Vec::new());
10939        let path = runtime.register_process_subst_out("head -n 1 | cat");
10940
10941        {
10942            let sink = runtime
10943                .process_subst_out_sink_mut(&path)
10944                .expect("registered process substitution sink");
10945            sink.write(b"a\nb\n");
10946            match &sink.mode {
10947                PendingProcessSubstOutMode::Live { runner } => {
10948                    assert_eq!(runner.captured_stdout, b"a\n");
10949                }
10950                PendingProcessSubstOutMode::Buffered { .. } => {
10951                    panic!("expected live process substitution runner")
10952                }
10953            }
10954        }
10955
10956        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
10957        runtime.flush_process_subst_out_scope(scope);
10958        assert_eq!(runtime.vm.stdout, b"a\n");
10959    }
10960
10961    #[test]
10962    fn live_process_substitution_runner_tee_writes_before_flush() {
10963        let mut runtime = WorkerRuntime::new();
10964        runtime.handle_command(HostCommand::Init {
10965            step_budget: 0,
10966            allowed_hosts: vec![],
10967        });
10968
10969        runtime.proc_subst_out_scopes.push(Vec::new());
10970        let path = runtime.register_process_subst_out("tee /tee.txt | cat");
10971
10972        {
10973            let sink = runtime
10974                .process_subst_out_sink_mut(&path)
10975                .expect("registered process substitution sink");
10976            sink.write(b"a\nb\n");
10977            match &sink.mode {
10978                PendingProcessSubstOutMode::Live { runner } => {
10979                    assert!(runner.captured_stdout.starts_with(b"a\nb"));
10980                }
10981                PendingProcessSubstOutMode::Buffered { .. } => {
10982                    panic!("expected live process substitution runner")
10983                }
10984            }
10985        }
10986
10987        let file = runtime.handle_command(HostCommand::ReadFile {
10988            path: "/tee.txt".into(),
10989        });
10990        assert!(get_stdout(&file).starts_with("a\nb"));
10991
10992        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
10993        runtime.flush_process_subst_out_scope(scope);
10994        assert_eq!(runtime.vm.stdout, b"a\nb\n");
10995
10996        let file = runtime.handle_command(HostCommand::ReadFile {
10997            path: "/tee.txt".into(),
10998        });
10999        assert_eq!(get_stdout(&file), "a\nb\n");
11000    }
11001
11002    #[test]
11003    fn exec_live_redirections_preserve_left_to_right_dup_order() {
11004        let mut runtime = WorkerRuntime::new();
11005        runtime.handle_command(HostCommand::Init {
11006            step_budget: 0,
11007            allowed_hosts: vec![],
11008        });
11009
11010        let events = runtime.handle_command(HostCommand::Run {
11011            input: "printf hi > /first.txt 1>&2\nprintf hi 1>&2 > /second.txt".into(),
11012        });
11013
11014        assert_eq!(get_stdout(&events), "");
11015        assert_eq!(get_stderr(&events), "hi");
11016
11017        let first = runtime.handle_command(HostCommand::ReadFile {
11018            path: "/first.txt".into(),
11019        });
11020        assert_eq!(get_stdout(&first), "");
11021
11022        let second = runtime.handle_command(HostCommand::ReadFile {
11023            path: "/second.txt".into(),
11024        });
11025        assert_eq!(get_stdout(&second), "hi");
11026    }
11027
11028    #[test]
11029    fn exec_process_subst_redirections_preserve_left_to_right_dup_order() {
11030        let mut runtime = WorkerRuntime::new();
11031        runtime.handle_command(HostCommand::Init {
11032            step_budget: 0,
11033            allowed_hosts: vec![],
11034        });
11035
11036        let events = runtime.handle_command(HostCommand::Run {
11037            input: "printf hi > >(cat) 1>&2\nprintf hi 1>&2 > >(cat)".into(),
11038        });
11039
11040        assert_eq!(get_stdout(&events), "hi");
11041        assert_eq!(get_stderr(&events), "hi");
11042    }
11043
11044    #[test]
11045    fn builtin_and_utility_redirections_write_files_during_execution() {
11046        let mut runtime = WorkerRuntime::new();
11047        runtime.handle_command(HostCommand::Init {
11048            step_budget: 0,
11049            allowed_hosts: vec![],
11050        });
11051
11052        let events = runtime.handle_command(HostCommand::Run {
11053            input: "type printf > /builtin.txt\nprintf hi > /utility.txt".into(),
11054        });
11055
11056        let status = events
11057            .iter()
11058            .find_map(|event| {
11059                if let WorkerEvent::Exit(code) = event {
11060                    Some(*code)
11061                } else {
11062                    None
11063                }
11064            })
11065            .unwrap_or(-1);
11066        assert_eq!(status, 0);
11067        assert_eq!(get_stdout(&events), "");
11068        assert_eq!(get_stderr(&events), "");
11069
11070        let builtin = runtime.handle_command(HostCommand::ReadFile {
11071            path: "/builtin.txt".into(),
11072        });
11073        assert!(get_stdout(&builtin).contains("printf"));
11074
11075        let utility = runtime.handle_command(HostCommand::ReadFile {
11076            path: "/utility.txt".into(),
11077        });
11078        assert_eq!(get_stdout(&utility), "hi");
11079    }
11080
11081    #[test]
11082    fn special_param_underscore_uses_previous_command_last_argument() {
11083        let mut runtime = WorkerRuntime::new();
11084        runtime.handle_command(HostCommand::Init {
11085            step_budget: 0,
11086            allowed_hosts: vec![],
11087        });
11088
11089        let first = runtime.handle_command(HostCommand::Run {
11090            input: "echo alpha beta".into(),
11091        });
11092        assert_eq!(get_stdout(&first), "alpha beta\n");
11093        assert_eq!(runtime.vm.state.get_var("_").as_deref(), Some("beta"));
11094
11095        let events = runtime.handle_command(HostCommand::Run {
11096            input: "echo \"last=$_\"".into(),
11097        });
11098
11099        assert_eq!(get_stdout(&events), "last=beta\n");
11100        assert_eq!(runtime.vm.state.get_var("_").as_deref(), Some("last=beta"));
11101    }
11102
11103    #[test]
11104    fn amp_append_redirection_appends_stdout_and_stderr_for_simple_command() {
11105        let mut runtime = WorkerRuntime::new();
11106        runtime.handle_command(HostCommand::Init {
11107            step_budget: 0,
11108            allowed_hosts: vec![],
11109        });
11110
11111        let setup = runtime.handle_command(HostCommand::WriteFile {
11112            path: "/log.txt".into(),
11113            data: b"old\n".to_vec(),
11114        });
11115        assert_eq!(get_stderr(&setup), "");
11116
11117        let events = runtime.handle_command(HostCommand::Run {
11118            input: "f(){ printf 'out\\n'; printf 'err\\n' >&2; }\nf &>> /log.txt\ncat /log.txt"
11119                .into(),
11120        });
11121
11122        assert_eq!(get_stdout(&events), "old\nout\nerr\n");
11123        assert_eq!(get_stderr(&events), "");
11124    }
11125
11126    #[test]
11127    fn clobber_redirection_overrides_noclobber() {
11128        let mut runtime = WorkerRuntime::new();
11129        runtime.handle_command(HostCommand::Init {
11130            step_budget: 0,
11131            allowed_hosts: vec![],
11132        });
11133
11134        let setup = runtime.handle_command(HostCommand::WriteFile {
11135            path: "/existing.txt".into(),
11136            data: b"old\n".to_vec(),
11137        });
11138        assert_eq!(get_stderr(&setup), "");
11139
11140        let events = runtime.handle_command(HostCommand::Run {
11141            input: "set -o noclobber\necho blocked > /existing.txt\ncat /existing.txt\necho force >| /existing.txt\ncat /existing.txt".into(),
11142        });
11143
11144        assert_eq!(get_stdout(&events), "old\nforce\n");
11145        assert!(get_stderr(&events).contains("cannot overwrite existing file"));
11146    }
11147}