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;
15
16use std::cell::RefCell;
17use std::collections::VecDeque;
18use std::io::{Cursor, ErrorKind, Read};
19use std::rc::Rc;
20
21use indexmap::IndexMap;
22
23use crate::dbl_bracket::dbl_bracket_eval_or;
24use crate::fd_table::{ExecIo, InputTarget, OutputTarget};
25use crate::pattern::{glob_match_ext, glob_match_inner, has_extglob_pattern};
26use crate::signals::{find_runtime_signal_spec, RuntimeSignalSpec, SignalDefaultAction};
27
28pub use crate::pattern::extglob_match;
29use wasmsh_ast::{CaseTerminator, RedirectionOp, Word, WordPart};
30use wasmsh_expand::expand_words_argv;
31use wasmsh_fs::{BackendFs, FileHandle, OpenOptions, Vfs, VfsWriteSink};
32use wasmsh_hir::{
33    HirAndOr, HirAndOrOp, HirCommand, HirCompleteCommand, HirPipeline, HirProgram, HirRedirection,
34};
35use wasmsh_ir::{lower_supported_and_or, IrProgram, IrRedirection, LoweringError};
36use wasmsh_protocol::{DiagnosticLevel, HostCommand, WorkerEvent, PROTOCOL_VERSION};
37use wasmsh_state::ShellState;
38use wasmsh_utils::{UtilContext, UtilRegistry};
39use wasmsh_vm::pipe::{PipeBuffer, ReadResult, WriteResult};
40use wasmsh_vm::{BudgetCategory, ExecutionLimits, ExhaustionReason, StopReason, Vm, VmExecutor};
41
42/// Sentinel FD value for `&>` (redirect both stdout and stderr).
43const FD_BOTH: u32 = u32::MAX;
44
45// Runtime-level command names dispatched before builtins.
46const CMD_LOCAL: &str = "local";
47const CMD_BREAK: &str = "break";
48const CMD_CONTINUE: &str = "continue";
49const CMD_EXIT: &str = "exit";
50const CMD_EVAL: &str = "eval";
51const CMD_SOURCE: &str = "source";
52const CMD_DOT: &str = ".";
53const CMD_DECLARE: &str = "declare";
54const CMD_TYPESET: &str = "typeset";
55const CMD_LET: &str = "let";
56const CMD_SHOPT: &str = "shopt";
57const CMD_ALIAS: &str = "alias";
58const CMD_UNALIAS: &str = "unalias";
59const CMD_BUILTIN: &str = "builtin";
60const CMD_MAPFILE: &str = "mapfile";
61const CMD_READARRAY: &str = "readarray";
62const CMD_TYPE: &str = "type";
63const CMD_COMMAND: &str = "command";
64const CMD_EXEC: &str = "exec";
65const CMD_HASH: &str = "hash";
66const CMD_TIMES: &str = "times";
67const CMD_DIRS: &str = "dirs";
68const CMD_PUSHD: &str = "pushd";
69const CMD_POPD: &str = "popd";
70const CMD_UMASK: &str = "umask";
71const CMD_WAIT: &str = "wait";
72const CMD_ULIMIT: &str = "ulimit";
73
74/// Configuration for the browser runtime.
75#[derive(Debug, Clone)]
76pub struct BrowserConfig {
77    pub step_budget: u64,
78    /// Hostnames/IPs allowed for network access (empty = no network).
79    pub allowed_hosts: Vec<String>,
80    pub output_byte_limit: u64,
81    pub pipe_byte_limit: u64,
82    pub recursion_limit: u32,
83    pub vm_subset_enabled: bool,
84}
85
86impl Default for BrowserConfig {
87    fn default() -> Self {
88        Self {
89            step_budget: 100_000,
90            allowed_hosts: Vec::new(),
91            output_byte_limit: 0,
92            pipe_byte_limit: 0,
93            recursion_limit: MAX_RECURSION_DEPTH,
94            vm_subset_enabled: true,
95        }
96    }
97}
98
99/// Maximum recursion depth for eval, source, and command substitution.
100const MAX_RECURSION_DEPTH: u32 = 100;
101
102/// Transient execution state, reset between top-level commands.
103#[derive(Clone)]
104#[allow(clippy::struct_excessive_bools)]
105struct ExecState {
106    break_depth: u32,
107    loop_continue: bool,
108    exit_requested: Option<i32>,
109    errexit_suppressed: bool,
110    local_save_stack: Vec<(smol_str::SmolStr, Option<smol_str::SmolStr>)>,
111    recursion_depth: u32,
112    /// Set when a resource limit (step budget, output limit, cancel) is hit.
113    resource_exhausted: bool,
114    stop_reason: Option<StopReason>,
115    /// Set when word expansion reports a hard semantic error.
116    expansion_failed: bool,
117    /// Trap handlers suppress nested trap reentry while they run.
118    trap_depth: u32,
119    /// Nested shell scopes (functions, sourced files, command substitutions).
120    nested_shell_depth: u32,
121    /// Nested output capture scopes for pipelines and substitutions.
122    output_captures: Vec<OutputCapture>,
123}
124
125impl ExecState {
126    fn new() -> Self {
127        Self {
128            break_depth: 0,
129            loop_continue: false,
130            exit_requested: None,
131            errexit_suppressed: false,
132            local_save_stack: Vec::new(),
133            recursion_depth: 0,
134            resource_exhausted: false,
135            stop_reason: None,
136            expansion_failed: false,
137            trap_depth: 0,
138            nested_shell_depth: 0,
139            output_captures: Vec::new(),
140        }
141    }
142
143    fn reset(&mut self) {
144        self.break_depth = 0;
145        self.loop_continue = false;
146        self.exit_requested = None;
147        self.errexit_suppressed = false;
148        self.resource_exhausted = false;
149        self.stop_reason = None;
150        self.expansion_failed = false;
151        self.trap_depth = 0;
152        self.nested_shell_depth = 0;
153        self.output_captures.clear();
154    }
155}
156
157const STREAMING_YES_MAX_LINES: usize = 65_536;
158const PIPEBUFFER_STREAMING_CAPACITY: usize = 1;
159
160#[derive(Clone, Debug, Default)]
161struct OutputCapture {
162    capture_stdout: bool,
163    capture_stderr: bool,
164    stdout: Vec<u8>,
165    stderr: Vec<u8>,
166}
167
168#[derive(Clone, Debug, Default)]
169struct CapturedOutput {
170    stdout: Vec<u8>,
171    stderr: Vec<u8>,
172}
173
174struct RuntimeOutputRouter<'a> {
175    exec: &'a mut ExecState,
176    exec_io: Option<&'a mut ExecIo>,
177    proc_subst_out_scopes: &'a mut Vec<Vec<PendingProcessSubstOut>>,
178    vm_stdout: &'a mut Vec<u8>,
179    vm_stderr: &'a mut Vec<u8>,
180    vm_output_bytes: &'a mut u64,
181    vm_output_limit: u64,
182    vm_diagnostics: &'a mut Vec<wasmsh_vm::DiagnosticEvent>,
183}
184
185impl RuntimeOutputRouter<'_> {
186    fn process_subst_out_sink_mut(&mut self, path: &str) -> Option<&mut PendingProcessSubstOut> {
187        for scope in self.proc_subst_out_scopes.iter_mut().rev() {
188            if let Some(index) = scope.iter().position(|sink| sink.path == path) {
189                return scope.get_mut(index);
190            }
191        }
192        None
193    }
194
195    fn append_visible_output_direct(&mut self, data: &[u8], stdout: bool) {
196        if stdout {
197            self.vm_stdout.extend_from_slice(data);
198        } else {
199            self.vm_stderr.extend_from_slice(data);
200        }
201    }
202
203    fn write_output_destination_direct(&mut self, destination: &OutputTarget, data: &[u8]) -> bool {
204        match destination {
205            OutputTarget::InheritStdout => {
206                self.append_visible_output_direct(data, true);
207                true
208            }
209            OutputTarget::InheritStderr => {
210                self.append_visible_output_direct(data, false);
211                true
212            }
213            OutputTarget::ProcessSubst { path } => {
214                if let Some(sink) = self.process_subst_out_sink_mut(path) {
215                    sink.write(data);
216                }
217                false
218            }
219            OutputTarget::File { path, sink, .. } => {
220                if let Err(err) = sink.borrow_mut().write(data) {
221                    let msg = format!("wasmsh: write error: {err}\n");
222                    self.append_visible_output_direct(msg.as_bytes(), false);
223                    self.vm_diagnostics.push(wasmsh_vm::DiagnosticEvent {
224                        level: wasmsh_vm::DiagLevel::Error,
225                        category: wasmsh_vm::DiagCategory::Filesystem,
226                        message: format!("write failed for {path}: {err}"),
227                    });
228                }
229                false
230            }
231            OutputTarget::Pipe(pipe) => {
232                pipe.borrow_mut().write_all(data);
233                false
234            }
235            OutputTarget::Closed => false,
236        }
237    }
238
239    fn route_output(&mut self, data: &[u8], stdout: bool) -> bool {
240        let mut routed_stdout = stdout;
241        if let Some(exec_io) = self.exec_io.as_deref_mut() {
242            let destination = exec_io.output_target(stdout);
243            match destination {
244                OutputTarget::InheritStdout => {
245                    routed_stdout = true;
246                }
247                OutputTarget::InheritStderr => {
248                    routed_stdout = false;
249                }
250                OutputTarget::File { .. }
251                | OutputTarget::ProcessSubst { .. }
252                | OutputTarget::Pipe(_)
253                | OutputTarget::Closed => {
254                    return self.write_output_destination_direct(&destination, data);
255                }
256            }
257        }
258
259        for capture in self.exec.output_captures.iter_mut().rev() {
260            let should_capture = if routed_stdout {
261                capture.capture_stdout
262            } else {
263                capture.capture_stderr
264            };
265            if !should_capture {
266                continue;
267            }
268            if routed_stdout {
269                capture.stdout.extend_from_slice(data);
270            } else {
271                capture.stderr.extend_from_slice(data);
272            }
273            return false;
274        }
275
276        self.append_visible_output_direct(data, routed_stdout);
277        true
278    }
279
280    fn account_output(&mut self, bytes: usize) {
281        *self.vm_output_bytes += bytes as u64;
282        self.exec.stop_reason = None;
283        if self.exec.resource_exhausted {
284            return;
285        }
286        let used = *self.vm_output_bytes;
287        if self.vm_output_limit > 0 && used > self.vm_output_limit {
288            let reason = ExhaustionReason {
289                category: BudgetCategory::VisibleOutputBytes,
290                used,
291                limit: self.vm_output_limit,
292            };
293            self.exec.resource_exhausted = true;
294            self.exec.stop_reason = Some(StopReason::Exhausted(reason.clone()));
295            self.vm_diagnostics.push(wasmsh_vm::DiagnosticEvent {
296                level: wasmsh_vm::DiagLevel::Error,
297                category: wasmsh_vm::DiagCategory::Budget,
298                message: reason.diagnostic_message(),
299            });
300        }
301    }
302
303    fn write_stdout(&mut self, data: &[u8]) {
304        if self.route_output(data, true) {
305            self.account_output(data.len());
306        }
307    }
308
309    fn write_stderr(&mut self, data: &[u8]) {
310        if self.route_output(data, false) {
311            self.account_output(data.len());
312        }
313    }
314}
315
316struct RuntimeBuiltinSink<'a> {
317    router: &'a mut RuntimeOutputRouter<'a>,
318}
319
320impl wasmsh_builtins::OutputSink for RuntimeBuiltinSink<'_> {
321    fn stdout(&mut self, data: &[u8]) {
322        self.router.write_stdout(data);
323    }
324
325    fn stderr(&mut self, data: &[u8]) {
326        self.router.write_stderr(data);
327    }
328}
329
330struct RuntimeUtilSink<'a> {
331    router: &'a mut RuntimeOutputRouter<'a>,
332}
333
334impl wasmsh_utils::UtilOutput for RuntimeUtilSink<'_> {
335    fn stdout(&mut self, data: &[u8]) {
336        self.router.write_stdout(data);
337    }
338
339    fn stderr(&mut self, data: &[u8]) {
340        self.router.write_stderr(data);
341    }
342}
343
344fn resolve_path_from_cwd(cwd: &str, path: &str) -> String {
345    if path.starts_with('/') {
346        wasmsh_fs::normalize_path(path)
347    } else {
348        wasmsh_fs::normalize_path(&format!("{cwd}/{path}"))
349    }
350}
351
352struct PipeReader {
353    pipe: Rc<RefCell<PipeBuffer>>,
354}
355
356impl PipeReader {
357    fn new(pipe: Rc<RefCell<PipeBuffer>>) -> Self {
358        Self { pipe }
359    }
360}
361
362impl Read for PipeReader {
363    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
364        match self.pipe.borrow_mut().read(buf) {
365            ReadResult::Read(read) => Ok(read),
366            ReadResult::WouldBlock => Err(std::io::Error::new(ErrorKind::WouldBlock, "pipe empty")),
367            ReadResult::Eof => Ok(0),
368        }
369    }
370}
371
372impl Drop for PipeReader {
373    fn drop(&mut self) {
374        self.pipe.borrow_mut().close_read();
375    }
376}
377
378#[derive(Clone, Copy)]
379enum PipeProcessPoll {
380    Ready,
381    PendingRead,
382    PendingWrite,
383    Exited,
384}
385
386struct LiveProcessSubstRunner {
387    isolated_runtime: Option<Box<WorkerRuntime>>,
388    source_pipe: Rc<RefCell<PipeBuffer>>,
389    processes: Vec<StreamingPipeProcess<'static>>,
390    finished: Vec<bool>,
391    final_pipe: Rc<RefCell<PipeBuffer>>,
392    stage_stderr: Vec<Rc<RefCell<Vec<u8>>>>,
393    stage_pipe_stderr: Vec<bool>,
394    captured_stdout: Vec<u8>,
395    captured_stderr: Vec<u8>,
396    captured_diagnostics: Vec<wasmsh_vm::DiagnosticEvent>,
397    done: bool,
398    synced_steps: u64,
399}
400
401struct LiveProcessSubstInReader {
402    isolated_runtime: Option<Box<WorkerRuntime>>,
403    processes: Vec<StreamingPipeProcess<'static>>,
404    finished: Vec<bool>,
405    final_pipe: Rc<RefCell<PipeBuffer>>,
406    stage_stderr: Vec<Rc<RefCell<Vec<u8>>>>,
407    stage_pipe_stderr: Vec<bool>,
408    flushed_stderr: Rc<RefCell<Vec<u8>>>,
409    flushed_diagnostics: Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>,
410    done: bool,
411}
412
413impl LiveProcessSubstInReader {
414    fn finalize_stderr(&mut self) {
415        let mut flushed = self.flushed_stderr.borrow_mut();
416        for (idx, stderr) in self.stage_stderr.iter().enumerate() {
417            if self.stage_pipe_stderr[idx] {
418                continue;
419            }
420            let data = stderr.borrow();
421            if !data.is_empty() {
422                flushed.extend_from_slice(&data);
423            }
424        }
425        if let Some(runtime) = self.isolated_runtime.as_mut() {
426            self.flushed_diagnostics
427                .borrow_mut()
428                .extend(runtime.vm.diagnostics.drain(..));
429        }
430    }
431
432    fn pump(&mut self) -> bool {
433        if self.done {
434            return false;
435        }
436        let progressed = if self.isolated_runtime.is_some() {
437            self.pump_with_isolated_runtime()
438        } else {
439            self.pump_without_runtime_loop()
440        };
441        if self.finished.iter().all(|done| *done) {
442            self.finalize_stderr();
443            self.done = true;
444        }
445        progressed
446    }
447
448    fn pump_with_isolated_runtime(&mut self) -> bool {
449        let runtime = self
450            .isolated_runtime
451            .as_mut()
452            .expect("isolated runtime present");
453        let mut progressed = false;
454        for idx in (0..self.processes.len()).rev() {
455            if self.finished[idx] {
456                continue;
457            }
458            let outcome = self.processes[idx].poll(runtime.as_mut());
459            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
460                progressed = true;
461            }
462        }
463        progressed
464    }
465
466    fn pump_without_runtime_loop(&mut self) -> bool {
467        let mut progressed = false;
468        for idx in (0..self.processes.len()).rev() {
469            if self.finished[idx] {
470                continue;
471            }
472            let outcome = self.processes[idx].poll_without_runtime();
473            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
474                progressed = true;
475            }
476        }
477        progressed
478    }
479}
480
481fn apply_process_poll_outcome(finished: &mut bool, outcome: PipeProcessPoll) -> bool {
482    match outcome {
483        PipeProcessPoll::Ready => true,
484        PipeProcessPoll::PendingRead | PipeProcessPoll::PendingWrite => false,
485        PipeProcessPoll::Exited => {
486            *finished = true;
487            true
488        }
489    }
490}
491
492impl Read for LiveProcessSubstInReader {
493    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
494        loop {
495            let read_result = {
496                let mut pipe = self.final_pipe.borrow_mut();
497                pipe.read(buf)
498            };
499            match read_result {
500                ReadResult::Read(read) => return Ok(read),
501                ReadResult::Eof if self.done => return Ok(0),
502                ReadResult::WouldBlock | ReadResult::Eof => {}
503            }
504
505            if !self.pump() {
506                if self.done {
507                    continue;
508                }
509                return Err(std::io::Error::new(
510                    ErrorKind::WouldBlock,
511                    "process substitution pipeline stalled",
512                ));
513            }
514        }
515    }
516}
517
518impl Drop for LiveProcessSubstInReader {
519    fn drop(&mut self) {
520        self.final_pipe.borrow_mut().close_read();
521        if let Some(runtime) = self.isolated_runtime.as_mut() {
522            for process in &mut self.processes {
523                process.close(runtime.as_mut());
524            }
525        } else {
526            for process in &mut self.processes {
527                process.close_without_runtime();
528            }
529        }
530    }
531}
532
533impl LiveProcessSubstRunner {
534    fn sync_isolated_runtime_with_parent(&mut self, parent: &mut WorkerRuntime) {
535        let Some(runtime) = self.isolated_runtime.as_mut() else {
536            return;
537        };
538        if parent.vm.cancellation_token().is_cancelled() {
539            runtime.vm.cancellation_token().cancel();
540        }
541        let current_steps = runtime.vm.steps;
542        if current_steps > self.synced_steps {
543            let delta = current_steps - self.synced_steps;
544            parent.vm.steps = parent.vm.steps.saturating_add(delta);
545            parent.vm.budget.steps = parent.vm.steps;
546            self.synced_steps = current_steps;
547            if parent.vm.steps > parent.vm.limits.step_limit && parent.vm.limits.step_limit > 0 {
548                let reason = ExhaustionReason {
549                    category: BudgetCategory::Steps,
550                    used: parent.vm.steps,
551                    limit: parent.vm.limits.step_limit,
552                };
553                parent.mark_budget_exhaustion(reason.clone());
554                parent.vm.emit_diagnostic(
555                    wasmsh_vm::DiagLevel::Error,
556                    wasmsh_vm::DiagCategory::Budget,
557                    reason.diagnostic_message(),
558                );
559                runtime.vm.cancellation_token().cancel();
560            }
561        }
562    }
563
564    fn drain_final_pipe(&mut self) -> bool {
565        let mut progressed = false;
566        loop {
567            let mut buffer = [0u8; 4096];
568            let read_result = {
569                let mut pipe = self.final_pipe.borrow_mut();
570                pipe.read(&mut buffer)
571            };
572            match read_result {
573                ReadResult::Read(read) => {
574                    self.captured_stdout.extend_from_slice(&buffer[..read]);
575                    progressed = true;
576                }
577                ReadResult::WouldBlock | ReadResult::Eof => break,
578            }
579        }
580        progressed
581    }
582
583    fn finalize_stderr(&mut self) {
584        for (idx, stderr) in self.stage_stderr.iter().enumerate() {
585            if self.stage_pipe_stderr[idx] {
586                continue;
587            }
588            let data = stderr.borrow();
589            if !data.is_empty() {
590                self.captured_stderr.extend_from_slice(&data);
591            }
592        }
593        if let Some(runtime) = self.isolated_runtime.as_mut() {
594            self.captured_diagnostics
595                .append(&mut runtime.vm.diagnostics);
596        }
597    }
598
599    fn pump(&mut self, parent: Option<&mut WorkerRuntime>) -> bool {
600        if self.done {
601            return false;
602        }
603        let mut progressed = if self.isolated_runtime.is_some() {
604            self.pump_isolated_with_parent(parent)
605        } else {
606            self.pump_without_runtime_pass()
607        };
608        if self.drain_final_pipe() {
609            progressed = true;
610        }
611        if self.finished.iter().all(|done| *done) {
612            self.finalize_stderr();
613            self.done = true;
614        }
615        progressed
616    }
617
618    fn pump_isolated_with_parent(&mut self, parent: Option<&mut WorkerRuntime>) -> bool {
619        let mut parent = parent;
620        if let Some(parent_rt) = parent.as_deref_mut() {
621            self.sync_isolated_runtime_with_parent(parent_rt);
622        }
623        let progressed = self.pump_isolated_pass();
624        if let Some(parent_rt) = parent {
625            self.sync_isolated_runtime_with_parent(parent_rt);
626        }
627        progressed
628    }
629
630    fn pump_isolated_pass(&mut self) -> bool {
631        let runtime = self
632            .isolated_runtime
633            .as_mut()
634            .expect("isolated process substitution runtime missing");
635        let mut progressed = false;
636        for idx in (0..self.processes.len()).rev() {
637            if self.finished[idx] {
638                continue;
639            }
640            let outcome = self.processes[idx].poll(runtime.as_mut());
641            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
642                progressed = true;
643            }
644        }
645        progressed
646    }
647
648    fn pump_without_runtime_pass(&mut self) -> bool {
649        let mut progressed = false;
650        for idx in (0..self.processes.len()).rev() {
651            if self.finished[idx] {
652                continue;
653            }
654            let outcome = self.processes[idx].poll_without_runtime();
655            if apply_process_poll_outcome(&mut self.finished[idx], outcome) {
656                progressed = true;
657            }
658        }
659        progressed
660    }
661
662    fn write_input(&mut self, data: &[u8]) {
663        let mut offset = 0;
664        while offset < data.len() && !self.done {
665            let write_result = {
666                let mut pipe = self.source_pipe.borrow_mut();
667                pipe.write(&data[offset..])
668            };
669            match write_result {
670                WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
671                    offset += written;
672                    let _ = self.pump(None);
673                }
674                WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
675                    if !self.pump(None) {
676                        break;
677                    }
678                }
679                WriteResult::BrokenPipe => {
680                    self.source_pipe.borrow_mut().close_write();
681                    while self.pump(None) {}
682                    break;
683                }
684            }
685        }
686    }
687
688    fn write_input_with_parent(&mut self, parent: &mut WorkerRuntime, data: &[u8]) {
689        let mut offset = 0;
690        while offset < data.len() && !self.done {
691            let write_result = {
692                let mut pipe = self.source_pipe.borrow_mut();
693                pipe.write(&data[offset..])
694            };
695            match write_result {
696                WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
697                    offset += written;
698                    let _ = self.pump(Some(parent));
699                }
700                WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
701                    if !self.pump(Some(parent)) {
702                        break;
703                    }
704                }
705                WriteResult::BrokenPipe => {
706                    self.source_pipe.borrow_mut().close_write();
707                    while self.pump(Some(parent)) {}
708                    break;
709                }
710            }
711        }
712    }
713
714    fn finish(&mut self) {
715        if self.done {
716            return;
717        }
718        self.source_pipe.borrow_mut().close_write();
719        while self.pump(None) {}
720        if !self.done {
721            self.finalize_stderr();
722            self.done = true;
723        }
724        let _ = self.drain_final_pipe();
725    }
726
727    fn finish_with_parent(&mut self, parent: &mut WorkerRuntime) {
728        if self.done {
729            return;
730        }
731        self.source_pipe.borrow_mut().close_write();
732        while self.pump(Some(parent)) {}
733        if !self.done {
734            self.finalize_stderr();
735            self.done = true;
736        }
737        self.sync_isolated_runtime_with_parent(parent);
738        let _ = self.drain_final_pipe();
739    }
740}
741
742enum PendingProcessSubstOutMode {
743    Buffered { data: Vec<u8> },
744    Live { runner: LiveProcessSubstRunner },
745}
746
747struct PendingProcessSubstOut {
748    path: String,
749    inner: String,
750    mode: PendingProcessSubstOutMode,
751}
752
753impl std::fmt::Debug for PendingProcessSubstOut {
754    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
755        f.debug_struct("PendingProcessSubstOut")
756            .field("path", &self.path)
757            .field("inner", &self.inner)
758            .finish_non_exhaustive()
759    }
760}
761
762struct PendingProcessSubstIn {
763    path: String,
764    stderr: Option<Rc<RefCell<Vec<u8>>>>,
765    diagnostics: Option<Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>>,
766}
767
768impl PendingProcessSubstOut {
769    fn clear(&mut self) {
770        match &mut self.mode {
771            PendingProcessSubstOutMode::Buffered { data } => data.clear(),
772            PendingProcessSubstOutMode::Live { .. } => {}
773        }
774    }
775
776    fn write(&mut self, data: &[u8]) {
777        match &mut self.mode {
778            PendingProcessSubstOutMode::Buffered { data: buffered } => {
779                buffered.extend_from_slice(data);
780            }
781            PendingProcessSubstOutMode::Live { runner } => runner.write_input(data),
782        }
783    }
784
785    fn write_with_parent(&mut self, runtime: &mut WorkerRuntime, data: &[u8]) {
786        match &mut self.mode {
787            PendingProcessSubstOutMode::Buffered { data: buffered } => {
788                buffered.extend_from_slice(data);
789            }
790            PendingProcessSubstOutMode::Live { runner } => {
791                if runner.isolated_runtime.is_some() {
792                    runner.write_input_with_parent(runtime, data);
793                } else {
794                    runner.write_input(data);
795                }
796            }
797        }
798    }
799}
800
801#[derive(Clone, Debug)]
802enum BufferedPipelineCommand {
803    Argv(Vec<String>),
804    Hir(HirCommand),
805}
806
807enum StreamingPipeProcess<'a> {
808    Read(PipeReadProcess<'a>),
809    Head(HeadPipeProcess),
810    Tee(TeePipeProcess<'a>),
811    Buffered(BufferedPipeProcess),
812}
813
814impl StreamingPipeProcess<'_> {
815    fn poll(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
816        match self {
817            Self::Read(process) => process.poll(),
818            Self::Head(process) => process.poll(),
819            Self::Tee(process) => process.poll(),
820            Self::Buffered(process) => process.poll(runtime),
821        }
822    }
823
824    fn close(&mut self, runtime: &mut WorkerRuntime) {
825        match self {
826            Self::Tee(process) => process.close(),
827            Self::Buffered(process) => process.close(runtime),
828            Self::Read(_) | Self::Head(_) => {}
829        }
830    }
831
832    fn poll_without_runtime(&mut self) -> PipeProcessPoll {
833        match self {
834            Self::Read(process) => process.poll(),
835            Self::Head(process) => process.poll(),
836            Self::Tee(process) => process.poll(),
837            Self::Buffered(_) => {
838                unreachable!("buffered pipeline stage requires runtime access")
839            }
840        }
841    }
842
843    fn close_without_runtime(&mut self) {
844        match self {
845            Self::Tee(process) => process.close(),
846            Self::Read(_) | Self::Head(_) => {}
847            Self::Buffered(_) => {
848                unreachable!("buffered pipeline stage requires runtime access")
849            }
850        }
851    }
852}
853
854struct BufferedPipeProcess {
855    input: Option<Rc<RefCell<PipeBuffer>>>,
856    output: Rc<RefCell<PipeBuffer>>,
857    command: BufferedPipelineCommand,
858    pipe_stderr: bool,
859    pending_stdout: Vec<u8>,
860    pending_offset: usize,
861    finished: bool,
862    command_ran: bool,
863    stage_stderr: Rc<RefCell<Vec<u8>>>,
864    stage_status: Rc<RefCell<i32>>,
865    staging_path: Option<String>,
866    staging_handle: Option<FileHandle>,
867}
868
869impl BufferedPipeProcess {
870    fn new(
871        input: Option<Rc<RefCell<PipeBuffer>>>,
872        output: Rc<RefCell<PipeBuffer>>,
873        command: BufferedPipelineCommand,
874        pipe_stderr: bool,
875        stage_stderr: Rc<RefCell<Vec<u8>>>,
876        stage_status: Rc<RefCell<i32>>,
877    ) -> Self {
878        Self {
879            input,
880            output,
881            command,
882            pipe_stderr,
883            pending_stdout: Vec::new(),
884            pending_offset: 0,
885            finished: false,
886            command_ran: false,
887            stage_stderr,
888            stage_status,
889            staging_path: None,
890            staging_handle: None,
891        }
892    }
893
894    fn command_label(&self) -> String {
895        match &self.command {
896            BufferedPipelineCommand::Argv(argv) => argv
897                .first()
898                .cloned()
899                .unwrap_or_else(|| "command".to_string()),
900            BufferedPipelineCommand::Hir(cmd) => Self::hir_command_label(cmd).to_string(),
901        }
902    }
903
904    fn hir_command_label(cmd: &HirCommand) -> &'static str {
905        match cmd {
906            HirCommand::Exec(_) => "exec",
907            HirCommand::Assign(_) => "assign",
908            HirCommand::RedirectOnly(_) => "redirect",
909            HirCommand::If(_) => "if",
910            HirCommand::While(_) => "while",
911            HirCommand::Until(_) => "until",
912            HirCommand::For(_) => "for",
913            HirCommand::Subshell(_) => "subshell",
914            HirCommand::Group(_) => "group",
915            HirCommand::FunctionDef(_) => "function",
916            HirCommand::Case(_) => "case",
917            HirCommand::DoubleBracket(_) => "[[",
918            HirCommand::ArithFor(_) => "arith-for",
919            HirCommand::ArithCommand(_) => "arith",
920            HirCommand::Select(_) => "select",
921            _ => "command",
922        }
923    }
924
925    fn ensure_staging_handle(
926        &mut self,
927        runtime: &mut WorkerRuntime,
928    ) -> Result<(String, FileHandle), String> {
929        if let (Some(path), Some(handle)) = (&self.staging_path, self.staging_handle) {
930            return Ok((path.clone(), handle));
931        }
932        let path = format!(
933            "/tmp/_wasmsh_pipe_{}",
934            WorkerRuntime::next_pending_input_id()
935        );
936        let create_handle = runtime
937            .fs
938            .open(&path, OpenOptions::write())
939            .map_err(|err| err.to_string())?;
940        runtime.fs.close(create_handle);
941        let handle = runtime
942            .fs
943            .open(&path, OpenOptions::append())
944            .map_err(|err| err.to_string())?;
945        self.staging_path = Some(path.clone());
946        self.staging_handle = Some(handle);
947        Ok((path, handle))
948    }
949
950    fn emit_error(
951        &mut self,
952        runtime: &mut WorkerRuntime,
953        cmd_name: &str,
954        err: &str,
955    ) -> PipeProcessPoll {
956        *self.stage_status.borrow_mut() = 1;
957        self.stage_stderr.borrow_mut().extend_from_slice(
958            format!("wasmsh: {cmd_name}: failed to stage pipeline input for streaming: {err}\n")
959                .as_bytes(),
960        );
961        self.output.borrow_mut().close_write();
962        self.close(runtime);
963        self.finished = true;
964        PipeProcessPoll::Exited
965    }
966
967    fn run_command(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
968        if let Some(handle) = self.staging_handle.take() {
969            runtime.fs.close(handle);
970        }
971        let saved_exec_io = runtime.current_exec_io.take();
972        if let Some(path) = self.staging_path.take() {
973            runtime.set_pending_input_file(path, true);
974        }
975        let ((), captured) =
976            runtime.with_output_capture(true, self.pipe_stderr, |runtime| match &self.command {
977                BufferedPipelineCommand::Argv(argv) => runtime.execute_argv_command(argv),
978                BufferedPipelineCommand::Hir(cmd) => runtime.execute_command(cmd),
979            });
980        *self.stage_status.borrow_mut() = runtime.vm.state.last_status;
981        if self.pipe_stderr {
982            self.pending_stdout = captured.stdout;
983            self.pending_stdout.extend_from_slice(&captured.stderr);
984        } else {
985            self.pending_stdout = captured.stdout;
986            self.stage_stderr
987                .borrow_mut()
988                .extend_from_slice(&captured.stderr);
989        }
990        runtime.clear_pending_input();
991        runtime.current_exec_io = saved_exec_io;
992        self.pending_offset = 0;
993        self.command_ran = true;
994        if self.pending_stdout.is_empty() {
995            self.output.borrow_mut().close_write();
996            self.finished = true;
997            PipeProcessPoll::Exited
998        } else {
999            PipeProcessPoll::Ready
1000        }
1001    }
1002
1003    fn close(&mut self, runtime: &mut WorkerRuntime) {
1004        if let Some(handle) = self.staging_handle.take() {
1005            runtime.fs.close(handle);
1006        }
1007        if let Some(path) = self.staging_path.take() {
1008            let _ = runtime.fs.remove_file(&path);
1009        }
1010    }
1011
1012    fn poll(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
1013        if self.finished {
1014            return PipeProcessPoll::Exited;
1015        }
1016        if self.pending_offset < self.pending_stdout.len() {
1017            return self.buffered_drain_pending();
1018        }
1019        if self.command_ran {
1020            self.output.borrow_mut().close_write();
1021            self.finished = true;
1022            return PipeProcessPoll::Exited;
1023        }
1024        self.buffered_pump_input(runtime)
1025    }
1026
1027    fn buffered_drain_pending(&mut self) -> PipeProcessPoll {
1028        let write_result = {
1029            let mut pipe = self.output.borrow_mut();
1030            pipe.write(&self.pending_stdout[self.pending_offset..])
1031        };
1032        match write_result {
1033            WriteResult::Written(written) => {
1034                self.pending_offset += written;
1035                if self.pending_offset == self.pending_stdout.len() {
1036                    self.pending_stdout.clear();
1037                    self.pending_offset = 0;
1038                    if self.command_ran {
1039                        self.output.borrow_mut().close_write();
1040                        self.finished = true;
1041                        return PipeProcessPoll::Exited;
1042                    }
1043                }
1044                PipeProcessPoll::Ready
1045            }
1046            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1047            WriteResult::WouldBlock(written) => {
1048                self.pending_offset += written;
1049                PipeProcessPoll::Ready
1050            }
1051            WriteResult::BrokenPipe => {
1052                self.output.borrow_mut().close_write();
1053                self.finished = true;
1054                PipeProcessPoll::Exited
1055            }
1056        }
1057    }
1058
1059    fn buffered_pump_input(&mut self, runtime: &mut WorkerRuntime) -> PipeProcessPoll {
1060        let Some(input) = &self.input else {
1061            return self.run_command(runtime);
1062        };
1063        let cmd_name = self.command_label();
1064        let mut scratch = [0u8; 4096];
1065        let read_result = {
1066            let mut input = input.borrow_mut();
1067            input.read(&mut scratch)
1068        };
1069        match read_result {
1070            ReadResult::Read(read) => {
1071                let (_, handle) = match self.ensure_staging_handle(runtime) {
1072                    Ok(parts) => parts,
1073                    Err(err) => return self.emit_error(runtime, &cmd_name, &err),
1074                };
1075                if let Err(err) = runtime.fs.write_file(handle, &scratch[..read]) {
1076                    return self.emit_error(runtime, &cmd_name, &err.to_string());
1077                }
1078                PipeProcessPoll::Ready
1079            }
1080            ReadResult::WouldBlock => PipeProcessPoll::PendingRead,
1081            ReadResult::Eof => {
1082                input.borrow_mut().close_read();
1083                self.run_command(runtime)
1084            }
1085        }
1086    }
1087}
1088
1089struct HeadPipeProcess {
1090    input: Rc<RefCell<PipeBuffer>>,
1091    output: Rc<RefCell<PipeBuffer>>,
1092    mode: StreamingHeadMode,
1093    pending: Vec<u8>,
1094    pending_offset: usize,
1095    lines_seen: usize,
1096    input_closed: bool,
1097    stream_complete: bool,
1098    finished: bool,
1099}
1100
1101impl HeadPipeProcess {
1102    fn new(
1103        input: Rc<RefCell<PipeBuffer>>,
1104        output: Rc<RefCell<PipeBuffer>>,
1105        mode: StreamingHeadMode,
1106    ) -> Self {
1107        Self {
1108            input,
1109            output,
1110            mode,
1111            pending: Vec::new(),
1112            pending_offset: 0,
1113            lines_seen: 0,
1114            input_closed: false,
1115            stream_complete: false,
1116            finished: false,
1117        }
1118    }
1119
1120    fn close_input(&mut self) {
1121        if !self.input_closed {
1122            self.input.borrow_mut().close_read();
1123            self.input_closed = true;
1124        }
1125    }
1126
1127    fn finish(&mut self) -> PipeProcessPoll {
1128        self.close_input();
1129        self.output.borrow_mut().close_write();
1130        self.finished = true;
1131        PipeProcessPoll::Exited
1132    }
1133
1134    fn try_flush_pending(&mut self) -> Option<PipeProcessPoll> {
1135        if self.pending_offset >= self.pending.len() {
1136            return None;
1137        }
1138        let write_result = {
1139            let mut pipe = self.output.borrow_mut();
1140            pipe.write(&self.pending[self.pending_offset..])
1141        };
1142        match write_result {
1143            WriteResult::Written(written) => {
1144                self.pending_offset += written;
1145                if self.pending_offset == self.pending.len() {
1146                    self.pending.clear();
1147                    self.pending_offset = 0;
1148                    if self.stream_complete {
1149                        return Some(self.finish());
1150                    }
1151                }
1152                Some(PipeProcessPoll::Ready)
1153            }
1154            WriteResult::WouldBlock(0) => Some(PipeProcessPoll::PendingWrite),
1155            WriteResult::WouldBlock(written) => {
1156                self.pending_offset += written;
1157                Some(PipeProcessPoll::Ready)
1158            }
1159            WriteResult::BrokenPipe => Some(self.finish()),
1160        }
1161    }
1162
1163    fn update_head_limit(&mut self, byte: u8, read: usize) {
1164        match &mut self.mode {
1165            StreamingHeadMode::Bytes(remaining) => {
1166                *remaining = remaining.saturating_sub(read);
1167                if *remaining == 0 {
1168                    self.stream_complete = true;
1169                    self.close_input();
1170                }
1171            }
1172            StreamingHeadMode::Lines(limit) => {
1173                if byte == b'\n' {
1174                    self.lines_seen += 1;
1175                    if self.lines_seen >= *limit {
1176                        self.stream_complete = true;
1177                        self.close_input();
1178                    }
1179                }
1180            }
1181        }
1182    }
1183
1184    fn poll(&mut self) -> PipeProcessPoll {
1185        if self.finished {
1186            return PipeProcessPoll::Exited;
1187        }
1188        loop {
1189            if let Some(result) = self.try_flush_pending() {
1190                return result;
1191            }
1192            if self.stream_complete {
1193                return self.finish();
1194            }
1195
1196            let mut one = [0u8; 1];
1197            let read_result = {
1198                let mut input = self.input.borrow_mut();
1199                input.read(&mut one)
1200            };
1201            match read_result {
1202                ReadResult::Read(read) => {
1203                    self.pending.extend_from_slice(&one[..read]);
1204                    self.update_head_limit(one[0], read);
1205                }
1206                ReadResult::WouldBlock => return PipeProcessPoll::PendingRead,
1207                ReadResult::Eof => {
1208                    self.stream_complete = true;
1209                    self.close_input();
1210                }
1211            }
1212        }
1213    }
1214}
1215
1216struct PipeReadProcess<'a> {
1217    reader: Option<Box<dyn Read + 'a>>,
1218    output: Rc<RefCell<PipeBuffer>>,
1219    pending: Vec<u8>,
1220    pending_offset: usize,
1221    stderr_offset: usize,
1222    finished: bool,
1223    stderr: Rc<RefCell<Vec<u8>>>,
1224    status: Rc<RefCell<i32>>,
1225    label: &'static str,
1226    pipe_stderr: bool,
1227    reader_done: bool,
1228}
1229
1230impl<'a> PipeReadProcess<'a> {
1231    fn new(
1232        reader: Box<dyn Read + 'a>,
1233        output: Rc<RefCell<PipeBuffer>>,
1234        stderr: Rc<RefCell<Vec<u8>>>,
1235        status: Rc<RefCell<i32>>,
1236        label: &'static str,
1237        pipe_stderr: bool,
1238    ) -> Self {
1239        Self {
1240            reader: Some(reader),
1241            output,
1242            pending: Vec::new(),
1243            pending_offset: 0,
1244            stderr_offset: 0,
1245            finished: false,
1246            stderr,
1247            status,
1248            label,
1249            pipe_stderr,
1250            reader_done: false,
1251        }
1252    }
1253
1254    fn finish(&mut self) -> PipeProcessPoll {
1255        self.output.borrow_mut().close_write();
1256        self.reader = None;
1257        self.finished = true;
1258        PipeProcessPoll::Exited
1259    }
1260
1261    fn poll_stderr(&mut self) -> Option<PipeProcessPoll> {
1262        if !self.pipe_stderr {
1263            return None;
1264        }
1265        let len = self.stderr.borrow().len();
1266        if self.stderr_offset >= len {
1267            return None;
1268        }
1269        let chunk = {
1270            let stderr = self.stderr.borrow();
1271            stderr[self.stderr_offset..].to_vec()
1272        };
1273        let write_result = {
1274            let mut output = self.output.borrow_mut();
1275            output.write(&chunk)
1276        };
1277        match write_result {
1278            WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
1279                self.stderr_offset += written;
1280                Some(PipeProcessPoll::Ready)
1281            }
1282            WriteResult::Written(_) | WriteResult::WouldBlock(_) => {
1283                Some(PipeProcessPoll::PendingWrite)
1284            }
1285            WriteResult::BrokenPipe => Some(self.finish()),
1286        }
1287    }
1288
1289    fn poll(&mut self) -> PipeProcessPoll {
1290        if self.finished {
1291            return PipeProcessPoll::Exited;
1292        }
1293        loop {
1294            if let Some(poll) = self.read_drain_pending() {
1295                return poll;
1296            }
1297            if let Some(poll) = self.poll_stderr() {
1298                return poll;
1299            }
1300            if self.reader_done {
1301                return self.finish();
1302            }
1303            if let Some(poll) = self.read_fill_from_reader() {
1304                return poll;
1305            }
1306        }
1307    }
1308
1309    fn read_drain_pending(&mut self) -> Option<PipeProcessPoll> {
1310        if self.pending_offset >= self.pending.len() {
1311            return None;
1312        }
1313        let write_result = {
1314            let mut pipe = self.output.borrow_mut();
1315            pipe.write(&self.pending[self.pending_offset..])
1316        };
1317        Some(match write_result {
1318            WriteResult::Written(written) => {
1319                self.pending_offset += written;
1320                if self.pending_offset == self.pending.len() {
1321                    self.pending.clear();
1322                    self.pending_offset = 0;
1323                }
1324                PipeProcessPoll::Ready
1325            }
1326            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1327            WriteResult::WouldBlock(written) => {
1328                self.pending_offset += written;
1329                PipeProcessPoll::Ready
1330            }
1331            WriteResult::BrokenPipe => self.finish(),
1332        })
1333    }
1334
1335    fn read_fill_from_reader(&mut self) -> Option<PipeProcessPoll> {
1336        let mut buffer = [0u8; 4096];
1337        let reader = self
1338            .reader
1339            .as_mut()
1340            .expect("pipe read process polled after reader finished");
1341        match reader.read(&mut buffer) {
1342            Ok(0) => {
1343                self.reader_done = true;
1344                None
1345            }
1346            Ok(read) => {
1347                self.pending.extend_from_slice(&buffer[..read]);
1348                None
1349            }
1350            Err(err) if err.kind() == ErrorKind::WouldBlock => Some(PipeProcessPoll::PendingRead),
1351            Err(err) => {
1352                *self.status.borrow_mut() = 1;
1353                self.stderr.borrow_mut().extend_from_slice(
1354                    format!(
1355                        "wasmsh: {}: streaming pipeline read error: {err}\n",
1356                        self.label
1357                    )
1358                    .as_bytes(),
1359                );
1360                self.reader_done = true;
1361                None
1362            }
1363        }
1364    }
1365}
1366
1367struct TeePipeProcess<'a> {
1368    reader: Option<Box<dyn Read + 'a>>,
1369    output: Rc<RefCell<PipeBuffer>>,
1370    pending: Vec<u8>,
1371    pending_offset: usize,
1372    stderr_offset: usize,
1373    finished: bool,
1374    stderr: Rc<RefCell<Vec<u8>>>,
1375    status: Rc<RefCell<i32>>,
1376    targets: Vec<TeeTarget>,
1377    pipe_stderr: bool,
1378    reader_done: bool,
1379}
1380
1381impl<'a> TeePipeProcess<'a> {
1382    fn new(
1383        reader: Box<dyn Read + 'a>,
1384        output: Rc<RefCell<PipeBuffer>>,
1385        fs: &mut BackendFs,
1386        cwd: &str,
1387        stage: &StreamingTeeStage,
1388        stderr: Rc<RefCell<Vec<u8>>>,
1389        status: Rc<RefCell<i32>>,
1390        pipe_stderr: bool,
1391    ) -> Self {
1392        let mut targets = Vec::new();
1393        for path in &stage.paths {
1394            let resolved = resolve_path_from_cwd(cwd, path);
1395            match fs.open_write_sink(&resolved, stage.append) {
1396                Ok(sink) => targets.push(TeeTarget {
1397                    display_path: path.clone(),
1398                    sink,
1399                }),
1400                Err(err) => {
1401                    stderr
1402                        .borrow_mut()
1403                        .extend_from_slice(format!("tee: {path}: {err}\n").as_bytes());
1404                    *status.borrow_mut() = 1;
1405                }
1406            }
1407        }
1408        Self {
1409            reader: Some(reader),
1410            output,
1411            pending: Vec::new(),
1412            pending_offset: 0,
1413            stderr_offset: 0,
1414            finished: false,
1415            stderr,
1416            status,
1417            targets,
1418            pipe_stderr,
1419            reader_done: false,
1420        }
1421    }
1422
1423    fn close(&mut self) {
1424        self.reader = None;
1425        self.targets.clear();
1426    }
1427
1428    fn finish(&mut self) -> PipeProcessPoll {
1429        self.output.borrow_mut().close_write();
1430        self.close();
1431        self.finished = true;
1432        PipeProcessPoll::Exited
1433    }
1434
1435    fn write_targets(&mut self, chunk: &[u8]) {
1436        for target in &mut self.targets {
1437            if let Err(err) = target.sink.write(chunk) {
1438                self.stderr
1439                    .borrow_mut()
1440                    .extend_from_slice(format!("tee: {}: {err}\n", target.display_path).as_bytes());
1441                *self.status.borrow_mut() = 1;
1442            }
1443        }
1444    }
1445
1446    fn poll(&mut self) -> PipeProcessPoll {
1447        if self.finished {
1448            return PipeProcessPoll::Exited;
1449        }
1450        loop {
1451            if let Some(poll) = self.tee_drain_pending() {
1452                return poll;
1453            }
1454            if let Some(poll) = self.tee_drain_stderr() {
1455                return poll;
1456            }
1457            if self.reader_done {
1458                return self.finish();
1459            }
1460            if let Some(poll) = self.tee_fill_from_reader() {
1461                return poll;
1462            }
1463        }
1464    }
1465
1466    fn tee_drain_pending(&mut self) -> Option<PipeProcessPoll> {
1467        if self.pending_offset >= self.pending.len() {
1468            return None;
1469        }
1470        let write_result = {
1471            let mut pipe = self.output.borrow_mut();
1472            pipe.write(&self.pending[self.pending_offset..])
1473        };
1474        Some(match write_result {
1475            WriteResult::Written(written) => {
1476                let end = self.pending_offset + written;
1477                let chunk = self.pending[self.pending_offset..end].to_vec();
1478                self.write_targets(&chunk);
1479                self.pending_offset += written;
1480                if self.pending_offset == self.pending.len() {
1481                    self.pending.clear();
1482                    self.pending_offset = 0;
1483                }
1484                PipeProcessPoll::Ready
1485            }
1486            WriteResult::WouldBlock(0) => PipeProcessPoll::PendingWrite,
1487            WriteResult::WouldBlock(written) => {
1488                let end = self.pending_offset + written;
1489                let chunk = self.pending[self.pending_offset..end].to_vec();
1490                self.write_targets(&chunk);
1491                self.pending_offset += written;
1492                PipeProcessPoll::Ready
1493            }
1494            WriteResult::BrokenPipe => self.finish(),
1495        })
1496    }
1497
1498    fn tee_drain_stderr(&mut self) -> Option<PipeProcessPoll> {
1499        if !self.pipe_stderr {
1500            return None;
1501        }
1502        let len = self.stderr.borrow().len();
1503        if self.stderr_offset >= len {
1504            return None;
1505        }
1506        let chunk = {
1507            let stderr = self.stderr.borrow();
1508            stderr[self.stderr_offset..].to_vec()
1509        };
1510        let write_result = {
1511            let mut output = self.output.borrow_mut();
1512            output.write(&chunk)
1513        };
1514        Some(match write_result {
1515            WriteResult::Written(written) | WriteResult::WouldBlock(written) if written > 0 => {
1516                self.stderr_offset += written;
1517                PipeProcessPoll::Ready
1518            }
1519            WriteResult::Written(_) | WriteResult::WouldBlock(_) => PipeProcessPoll::PendingWrite,
1520            WriteResult::BrokenPipe => self.finish(),
1521        })
1522    }
1523
1524    fn tee_fill_from_reader(&mut self) -> Option<PipeProcessPoll> {
1525        let mut buffer = [0u8; 4096];
1526        let reader = self
1527            .reader
1528            .as_mut()
1529            .expect("tee pipe process polled after reader finished");
1530        match reader.read(&mut buffer) {
1531            Ok(0) => {
1532                self.reader_done = true;
1533                None
1534            }
1535            Ok(read) => {
1536                self.pending.extend_from_slice(&buffer[..read]);
1537                None
1538            }
1539            Err(err) if err.kind() == ErrorKind::WouldBlock => Some(PipeProcessPoll::PendingRead),
1540            Err(err) => {
1541                *self.status.borrow_mut() = 1;
1542                self.stderr.borrow_mut().extend_from_slice(
1543                    format!("wasmsh: tee: streaming pipeline read error: {err}\n").as_bytes(),
1544                );
1545                self.reader_done = true;
1546                None
1547            }
1548        }
1549    }
1550}
1551
1552#[derive(Clone, Copy, Debug)]
1553enum StreamingHeadMode {
1554    Lines(usize),
1555    Bytes(usize),
1556}
1557
1558#[derive(Clone, Copy, Debug)]
1559enum StreamingTailMode {
1560    Lines(usize),
1561    Bytes(usize),
1562}
1563
1564struct YesStreamReader {
1565    line: Vec<u8>,
1566    offset: usize,
1567    remaining_lines: usize,
1568}
1569
1570impl YesStreamReader {
1571    fn new(line: Vec<u8>, remaining_lines: usize) -> Self {
1572        Self {
1573            line,
1574            offset: 0,
1575            remaining_lines,
1576        }
1577    }
1578}
1579
1580impl Read for YesStreamReader {
1581    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1582        if buf.is_empty() || self.line.is_empty() || self.remaining_lines == 0 {
1583            return Ok(0);
1584        }
1585        let mut written = 0usize;
1586        while written < buf.len() && self.remaining_lines > 0 {
1587            let remaining_line = &self.line[self.offset..];
1588            let to_copy = remaining_line.len().min(buf.len() - written);
1589            buf[written..written + to_copy].copy_from_slice(&remaining_line[..to_copy]);
1590            written += to_copy;
1591            self.offset += to_copy;
1592            if self.offset == self.line.len() {
1593                self.offset = 0;
1594                self.remaining_lines = self.remaining_lines.saturating_sub(1);
1595            }
1596        }
1597        Ok(written)
1598    }
1599}
1600
1601struct HeadStreamReader<R> {
1602    inner: R,
1603    mode: StreamingHeadMode,
1604    finished: bool,
1605    pending: Vec<u8>,
1606    pending_offset: usize,
1607    lines_seen: usize,
1608}
1609
1610struct TailStreamReader<R> {
1611    inner: R,
1612    mode: StreamingTailMode,
1613    output_pending: Vec<u8>,
1614    output_offset: usize,
1615    finalized: bool,
1616    byte_ring: VecDeque<u8>,
1617    line_ring: VecDeque<Vec<u8>>,
1618    current_line: Vec<u8>,
1619}
1620
1621#[derive(Clone, Copy, Debug)]
1622struct StreamingBatStage {
1623    show_numbers: bool,
1624    show_header: bool,
1625    line_range: Option<(Option<usize>, Option<usize>)>,
1626    show_all: bool,
1627}
1628
1629struct BatStreamReader<R> {
1630    inner: R,
1631    stage: StreamingBatStage,
1632    input_pending: Vec<u8>,
1633    output_pending: Vec<u8>,
1634    output_offset: usize,
1635    finished: bool,
1636    header_emitted: bool,
1637    footer_emitted: bool,
1638    line_num: usize,
1639}
1640
1641#[derive(Clone, Debug)]
1642struct StreamingSedSubstitute {
1643    pattern: String,
1644    replacement: String,
1645    global: bool,
1646}
1647
1648#[derive(Clone, Debug)]
1649enum StreamingSedAddr {
1650    None,
1651    Line(usize),
1652    Last,
1653    Regex(String),
1654    Range(Box<StreamingSedAddr>, Box<StreamingSedAddr>),
1655}
1656
1657#[derive(Clone, Debug)]
1658enum StreamingSedCmd {
1659    Substitute(StreamingSedSubstitute),
1660    Delete,
1661    Print,
1662    Transliterate(Vec<char>, Vec<char>),
1663    AppendText(String),
1664    InsertText(String),
1665    ChangeText(String),
1666    Quit,
1667}
1668
1669#[derive(Clone, Debug)]
1670struct StreamingSedInstruction {
1671    addr: StreamingSedAddr,
1672    cmd: StreamingSedCmd,
1673}
1674
1675#[derive(Clone, Debug)]
1676struct StreamingSedStage {
1677    suppress_print: bool,
1678    instructions: Vec<StreamingSedInstruction>,
1679}
1680
1681struct SedStreamReader<R> {
1682    inner: R,
1683    stage: StreamingSedStage,
1684    input_pending: Vec<u8>,
1685    output_pending: Vec<u8>,
1686    output_offset: usize,
1687    initialized: bool,
1688    finished: bool,
1689    current: Option<(String, bool)>,
1690    next: Option<(String, bool)>,
1691    line_num: usize,
1692    range_states: Vec<bool>,
1693    input_eof: bool,
1694}
1695
1696#[derive(Clone, Debug)]
1697struct StreamingPasteStage {
1698    delimiter: String,
1699    serial: bool,
1700}
1701
1702struct PasteStreamReader<R> {
1703    inner: R,
1704    stage: StreamingPasteStage,
1705    input_pending: Vec<u8>,
1706    output_pending: Vec<u8>,
1707    output_offset: usize,
1708    finalized: bool,
1709    ended_with_newline: bool,
1710    serial_first: bool,
1711}
1712
1713#[derive(Clone, Copy, Debug)]
1714struct StreamingColumnStage;
1715
1716struct ColumnStreamReader<R> {
1717    inner: R,
1718    output_pending: Vec<u8>,
1719    output_offset: usize,
1720    finalized: bool,
1721    ended_with_newline: bool,
1722}
1723
1724#[derive(Clone, Debug)]
1725struct StreamingTeeStage {
1726    append: bool,
1727    paths: Vec<String>,
1728}
1729
1730struct TeeTarget {
1731    display_path: String,
1732    sink: Box<dyn VfsWriteSink>,
1733}
1734
1735#[derive(Clone, Copy, Debug)]
1736#[allow(clippy::struct_excessive_bools)]
1737struct StreamingWcFlags {
1738    lines: bool,
1739    words: bool,
1740    bytes: bool,
1741    max_line_length: bool,
1742}
1743
1744#[allow(clippy::struct_excessive_bools)]
1745struct WcStreamReader<R> {
1746    inner: R,
1747    flags: StreamingWcFlags,
1748    summary: Vec<u8>,
1749    summary_offset: usize,
1750    finalized: bool,
1751    lines: usize,
1752    words: usize,
1753    bytes: usize,
1754    max_line_length: usize,
1755    current_line_length: usize,
1756    in_word: bool,
1757    saw_input: bool,
1758    ended_with_newline: bool,
1759}
1760
1761#[derive(Clone, Debug)]
1762#[allow(clippy::struct_excessive_bools)]
1763struct StreamingGrepFlags {
1764    ignore_case: bool,
1765    invert: bool,
1766    count_only: bool,
1767    show_line_numbers: bool,
1768    files_only: bool,
1769    word_match: bool,
1770    only_matching: bool,
1771    quiet: bool,
1772    extended: bool,
1773    fixed: bool,
1774    after_context: usize,
1775    before_context: usize,
1776    max_count: Option<usize>,
1777    show_filename: Option<bool>,
1778}
1779
1780#[derive(Clone, Debug)]
1781struct StreamingGrepStage {
1782    flags: StreamingGrepFlags,
1783    patterns: Vec<String>,
1784}
1785
1786#[derive(Copy, Clone, Debug)]
1787enum StreamingGrepStep {
1788    Advance(usize),
1789    NotMatched,
1790}
1791
1792#[derive(Copy, Clone, Debug)]
1793enum StreamingSedStep {
1794    Advance(usize),
1795    Break,
1796}
1797
1798struct StreamingCutParseState {
1799    delim: char,
1800    mode: Option<StreamingCutMode>,
1801    complement: bool,
1802    only_delimited: bool,
1803    output_delim: Option<String>,
1804}
1805
1806#[derive(Default)]
1807#[allow(clippy::struct_excessive_bools)]
1808struct TypeFlags {
1809    all: bool,
1810    skip_functions: bool,
1811    path_only: bool,
1812    force_path: bool,
1813    type_only: bool,
1814}
1815
1816#[derive(Clone, Debug)]
1817#[allow(clippy::struct_excessive_bools)]
1818struct StreamingUniqFlags {
1819    count: bool,
1820    duplicates_only: bool,
1821    unique_only: bool,
1822    ignore_case: bool,
1823    skip_fields: usize,
1824    skip_chars: usize,
1825    compare_chars: Option<usize>,
1826}
1827
1828impl<R> WcStreamReader<R> {
1829    fn new(inner: R, flags: StreamingWcFlags) -> Self {
1830        Self {
1831            inner,
1832            flags,
1833            summary: Vec::new(),
1834            summary_offset: 0,
1835            finalized: false,
1836            lines: 0,
1837            words: 0,
1838            bytes: 0,
1839            max_line_length: 0,
1840            current_line_length: 0,
1841            in_word: false,
1842            saw_input: false,
1843            ended_with_newline: false,
1844        }
1845    }
1846
1847    fn take_summary(&mut self, buf: &mut [u8]) -> usize {
1848        if self.summary_offset >= self.summary.len() {
1849            return 0;
1850        }
1851        let remaining = &self.summary[self.summary_offset..];
1852        let to_copy = remaining.len().min(buf.len());
1853        buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
1854        self.summary_offset += to_copy;
1855        to_copy
1856    }
1857
1858    fn process_chunk(&mut self, chunk: &[u8]) {
1859        if chunk.is_empty() {
1860            return;
1861        }
1862        self.saw_input = true;
1863        self.bytes += chunk.len();
1864        for &byte in chunk {
1865            let is_whitespace = byte.is_ascii_whitespace();
1866            if is_whitespace {
1867                self.in_word = false;
1868            } else if !self.in_word {
1869                self.words += 1;
1870                self.in_word = true;
1871            }
1872
1873            if byte == b'\n' {
1874                self.lines += 1;
1875                self.max_line_length = self.max_line_length.max(self.current_line_length);
1876                self.current_line_length = 0;
1877                self.ended_with_newline = true;
1878            } else {
1879                self.current_line_length += 1;
1880                self.ended_with_newline = false;
1881            }
1882        }
1883    }
1884
1885    fn finalize_summary(&mut self) {
1886        if self.finalized {
1887            return;
1888        }
1889        self.finalized = true;
1890        if self.saw_input && !self.ended_with_newline {
1891            self.lines += 1;
1892            self.max_line_length = self.max_line_length.max(self.current_line_length);
1893        }
1894
1895        let mut parts = Vec::new();
1896        if self.flags.lines {
1897            parts.push(self.lines.to_string());
1898        }
1899        if self.flags.words {
1900            parts.push(self.words.to_string());
1901        }
1902        if self.flags.bytes {
1903            parts.push(self.bytes.to_string());
1904        }
1905        if self.flags.max_line_length {
1906            parts.push(self.max_line_length.to_string());
1907        }
1908        let mut output = parts.join(" ");
1909        output.push('\n');
1910        self.summary = output.into_bytes();
1911    }
1912}
1913
1914impl<R: Read> Read for WcStreamReader<R> {
1915    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1916        if buf.is_empty() {
1917            return Ok(0);
1918        }
1919        let copied = self.take_summary(buf);
1920        if copied > 0 {
1921            return Ok(copied);
1922        }
1923        if self.finalized {
1924            return Ok(0);
1925        }
1926
1927        let mut scratch = [0u8; 4096];
1928        loop {
1929            let read = self.inner.read(&mut scratch)?;
1930            if read == 0 {
1931                self.finalize_summary();
1932                return Ok(self.take_summary(buf));
1933            }
1934            self.process_chunk(&scratch[..read]);
1935        }
1936    }
1937}
1938
1939impl<R> HeadStreamReader<R> {
1940    fn new(inner: R, mode: StreamingHeadMode) -> Self {
1941        Self {
1942            inner,
1943            mode,
1944            finished: false,
1945            pending: Vec::new(),
1946            pending_offset: 0,
1947            lines_seen: 0,
1948        }
1949    }
1950
1951    fn take_from_pending(&mut self, buf: &mut [u8]) -> usize {
1952        if self.pending_offset >= self.pending.len() {
1953            self.pending.clear();
1954            self.pending_offset = 0;
1955            return 0;
1956        }
1957        let remaining = &self.pending[self.pending_offset..];
1958        let to_copy = remaining.len().min(buf.len());
1959        buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
1960        self.pending_offset += to_copy;
1961        if self.pending_offset == self.pending.len() {
1962            self.pending.clear();
1963            self.pending_offset = 0;
1964        }
1965        to_copy
1966    }
1967}
1968
1969impl<R: Read> Read for HeadStreamReader<R> {
1970    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1971        if buf.is_empty() {
1972            return Ok(0);
1973        }
1974        let copied = self.take_from_pending(buf);
1975        if copied > 0 {
1976            return Ok(copied);
1977        }
1978        if self.finished {
1979            return Ok(0);
1980        }
1981        match self.mode {
1982            StreamingHeadMode::Bytes(_) => self.read_bytes_mode(buf),
1983            StreamingHeadMode::Lines(limit) => self.read_lines_mode(buf, limit),
1984        }
1985    }
1986}
1987
1988impl<R: Read> HeadStreamReader<R> {
1989    fn read_bytes_mode(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1990        let StreamingHeadMode::Bytes(ref mut remaining) = self.mode else {
1991            unreachable!("read_bytes_mode called in non-Bytes mode")
1992        };
1993        if *remaining == 0 {
1994            self.finished = true;
1995            return Ok(0);
1996        }
1997        let to_read = (*remaining).min(buf.len());
1998        let read = self.inner.read(&mut buf[..to_read])?;
1999        *remaining = remaining.saturating_sub(read);
2000        if read == 0 || *remaining == 0 {
2001            self.finished = true;
2002        }
2003        Ok(read)
2004    }
2005
2006    fn read_lines_mode(&mut self, buf: &mut [u8], limit: usize) -> std::io::Result<usize> {
2007        if self.lines_seen >= limit {
2008            self.finished = true;
2009            return Ok(0);
2010        }
2011        let mut produced = 0usize;
2012        while produced < buf.len() && self.lines_seen < limit {
2013            match self.read_one_line_byte(&mut buf[produced..=produced], produced)? {
2014                HeadLinesStep::Produced => produced += 1,
2015                HeadLinesStep::EofBreak => break,
2016                HeadLinesStep::WouldBlockYield => return Ok(produced),
2017            }
2018        }
2019        if self.lines_seen >= limit {
2020            self.finished = true;
2021        }
2022        Ok(produced)
2023    }
2024
2025    fn read_one_line_byte(
2026        &mut self,
2027        slot: &mut [u8],
2028        produced: usize,
2029    ) -> std::io::Result<HeadLinesStep> {
2030        let read = match self.inner.read(slot) {
2031            Ok(n) => n,
2032            Err(err) if err.kind() == ErrorKind::WouldBlock && produced > 0 => {
2033                return Ok(HeadLinesStep::WouldBlockYield);
2034            }
2035            Err(err) => return Err(err),
2036        };
2037        if read == 0 {
2038            self.finished = true;
2039            return Ok(HeadLinesStep::EofBreak);
2040        }
2041        if slot[0] == b'\n' {
2042            self.lines_seen += 1;
2043        }
2044        Ok(HeadLinesStep::Produced)
2045    }
2046}
2047
2048enum HeadLinesStep {
2049    Produced,
2050    EofBreak,
2051    WouldBlockYield,
2052}
2053
2054impl<R> TailStreamReader<R> {
2055    fn new(inner: R, mode: StreamingTailMode) -> Self {
2056        Self {
2057            inner,
2058            mode,
2059            output_pending: Vec::new(),
2060            output_offset: 0,
2061            finalized: false,
2062            byte_ring: VecDeque::new(),
2063            line_ring: VecDeque::new(),
2064            current_line: Vec::new(),
2065        }
2066    }
2067
2068    fn push_tail_byte(&mut self, byte: u8) {
2069        let StreamingTailMode::Bytes(limit) = self.mode else {
2070            return;
2071        };
2072        if limit == 0 {
2073            return;
2074        }
2075        if self.byte_ring.len() == limit {
2076            self.byte_ring.pop_front();
2077        }
2078        self.byte_ring.push_back(byte);
2079    }
2080
2081    fn push_tail_line(&mut self, line: Vec<u8>) {
2082        let StreamingTailMode::Lines(limit) = self.mode else {
2083            return;
2084        };
2085        if limit == 0 {
2086            return;
2087        }
2088        if self.line_ring.len() == limit {
2089            self.line_ring.pop_front();
2090        }
2091        self.line_ring.push_back(line);
2092    }
2093
2094    fn process_chunk(&mut self, chunk: &[u8]) {
2095        match self.mode {
2096            StreamingTailMode::Bytes(_) => {
2097                for &byte in chunk {
2098                    self.push_tail_byte(byte);
2099                }
2100            }
2101            StreamingTailMode::Lines(_) => {
2102                for &byte in chunk {
2103                    if byte == b'\n' {
2104                        let line = std::mem::take(&mut self.current_line);
2105                        self.push_tail_line(line);
2106                    } else {
2107                        self.current_line.push(byte);
2108                    }
2109                }
2110            }
2111        }
2112    }
2113
2114    fn finalize_output(&mut self) {
2115        if self.finalized {
2116            return;
2117        }
2118        match self.mode {
2119            StreamingTailMode::Bytes(_) => {
2120                self.output_pending.extend(self.byte_ring.drain(..));
2121            }
2122            StreamingTailMode::Lines(_) => {
2123                if !self.current_line.is_empty() {
2124                    let line = std::mem::take(&mut self.current_line);
2125                    self.push_tail_line(line);
2126                }
2127                for line in self.line_ring.drain(..) {
2128                    self.output_pending.extend_from_slice(&line);
2129                    self.output_pending.push(b'\n');
2130                }
2131            }
2132        }
2133        self.finalized = true;
2134    }
2135}
2136
2137impl<R: Read> Read for TailStreamReader<R> {
2138    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2139        if buf.is_empty() {
2140            return Ok(0);
2141        }
2142        let copied = take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2143        if copied > 0 {
2144            return Ok(copied);
2145        }
2146        if self.finalized {
2147            return Ok(0);
2148        }
2149        loop {
2150            let mut scratch = [0u8; 4096];
2151            match self.inner.read(&mut scratch) {
2152                Ok(0) => {
2153                    self.finalize_output();
2154                    return Ok(take_pending_output(
2155                        &mut self.output_pending,
2156                        &mut self.output_offset,
2157                        buf,
2158                    ));
2159                }
2160                Ok(read) => self.process_chunk(&scratch[..read]),
2161                Err(err) => return Err(err),
2162            }
2163        }
2164    }
2165}
2166
2167fn streaming_bat_in_range(line_num: usize, range: Option<(Option<usize>, Option<usize>)>) -> bool {
2168    let Some((start, end)) = range else {
2169        return true;
2170    };
2171    if start.is_some_and(|s| line_num < s) {
2172        return false;
2173    }
2174    end.is_none_or(|e| line_num <= e)
2175}
2176
2177fn streaming_make_visible(s: &str) -> String {
2178    let mut out = String::with_capacity(s.len());
2179    for ch in s.chars() {
2180        if ch == '\t' {
2181            out.push_str("\\t");
2182        } else if ch == '\r' {
2183            out.push_str("\\r");
2184        } else if ch.is_control() {
2185            let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\x{:02x}", ch as u32));
2186        } else {
2187            out.push(ch);
2188        }
2189    }
2190    out
2191}
2192
2193impl<R> BatStreamReader<R> {
2194    fn new(inner: R, stage: StreamingBatStage) -> Self {
2195        Self {
2196            inner,
2197            stage,
2198            input_pending: Vec::new(),
2199            output_pending: Vec::new(),
2200            output_offset: 0,
2201            finished: false,
2202            header_emitted: false,
2203            footer_emitted: false,
2204            line_num: 0,
2205        }
2206    }
2207
2208    fn emit_header(&mut self) {
2209        if !self.stage.show_header || self.header_emitted {
2210            return;
2211        }
2212        self.header_emitted = true;
2213        let separator = "\u{2500}";
2214        let rule_left: String = separator.repeat(7);
2215        let rule_right: String = separator.repeat(20);
2216        let top_corner = "\u{252C}";
2217        let mid_corner = "\u{253C}";
2218        self.output_pending
2219            .extend_from_slice(format!("{rule_left}{top_corner}{rule_right}\n").as_bytes());
2220        self.output_pending
2221            .extend_from_slice(format!("{rule_left}{mid_corner}{rule_right}\n").as_bytes());
2222    }
2223
2224    fn emit_footer(&mut self) {
2225        if !self.stage.show_header || self.footer_emitted {
2226            return;
2227        }
2228        self.footer_emitted = true;
2229        let separator = "\u{2500}";
2230        let rule_left: String = separator.repeat(7);
2231        let rule_right: String = separator.repeat(20);
2232        let bot_corner = "\u{2534}";
2233        self.output_pending
2234            .extend_from_slice(format!("{rule_left}{bot_corner}{rule_right}\n").as_bytes());
2235    }
2236}
2237
2238impl<R: Read> Read for BatStreamReader<R> {
2239    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2240        if buf.is_empty() {
2241            return Ok(0);
2242        }
2243        loop {
2244            let copied =
2245                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2246            if copied > 0 {
2247                return Ok(copied);
2248            }
2249            if self.finished {
2250                return Ok(0);
2251            }
2252            self.emit_header();
2253            let copied =
2254                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2255            if copied > 0 {
2256                return Ok(copied);
2257            }
2258            self.pump_next_bat_line()?;
2259        }
2260    }
2261}
2262
2263impl<R: Read> BatStreamReader<R> {
2264    fn pump_next_bat_line(&mut self) -> std::io::Result<()> {
2265        if let Some((line, _had_newline)) =
2266            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2267        {
2268            self.line_num += 1;
2269            if streaming_bat_in_range(self.line_num, self.stage.line_range) {
2270                self.emit_bat_line(&line);
2271            }
2272        } else {
2273            self.emit_footer();
2274            self.finished = true;
2275        }
2276        Ok(())
2277    }
2278
2279    fn emit_bat_line(&mut self, line: &str) {
2280        let display_line = if self.stage.show_all {
2281            streaming_make_visible(line)
2282        } else {
2283            line.to_string()
2284        };
2285        if self.stage.show_numbers {
2286            self.output_pending.extend_from_slice(
2287                format!("{:>5}   \u{2502} {display_line}\n", self.line_num).as_bytes(),
2288            );
2289        } else {
2290            self.output_pending
2291                .extend_from_slice(format!("{display_line}\n").as_bytes());
2292        }
2293    }
2294}
2295
2296fn streaming_simple_grep_match(line: &str, pattern: &str) -> bool {
2297    if let Some(rest) = pattern.strip_prefix('^') {
2298        if let Some(mid) = rest.strip_suffix('$') {
2299            line == mid
2300        } else {
2301            line.starts_with(rest)
2302        }
2303    } else if let Some(rest) = pattern.strip_suffix('$') {
2304        line.ends_with(rest)
2305    } else {
2306        line.contains(pattern)
2307    }
2308}
2309
2310fn parse_streaming_sed_substitute(expr: &str) -> Option<StreamingSedSubstitute> {
2311    if !expr.starts_with('s') || expr.len() < 4 {
2312        return None;
2313    }
2314    let delim = expr.as_bytes()[1] as char;
2315    let rest = &expr[2..];
2316    let parts: Vec<&str> = rest.split(delim).collect();
2317    if parts.len() < 2 {
2318        return None;
2319    }
2320    Some(StreamingSedSubstitute {
2321        pattern: parts[0].to_string(),
2322        replacement: parts[1].to_string(),
2323        global: parts.get(2).is_some_and(|flags| flags.contains('g')),
2324    })
2325}
2326
2327fn parse_streaming_sed_addr(s: &str) -> (StreamingSedAddr, &str) {
2328    if let Some(stripped) = s.strip_prefix('/') {
2329        if let Some(end) = stripped.find('/') {
2330            let pat = &stripped[..end];
2331            let rest = &stripped[end + 1..];
2332            if let Some(after_comma) = rest.strip_prefix(',') {
2333                let (addr2, rest2) = parse_streaming_sed_addr(after_comma);
2334                return (
2335                    StreamingSedAddr::Range(
2336                        Box::new(StreamingSedAddr::Regex(pat.to_string())),
2337                        Box::new(addr2),
2338                    ),
2339                    rest2,
2340                );
2341            }
2342            return (StreamingSedAddr::Regex(pat.to_string()), rest);
2343        }
2344    }
2345    if let Some(rest) = s.strip_prefix('$') {
2346        if let Some(after_comma) = rest.strip_prefix(',') {
2347            let (addr2, rest2) = parse_streaming_sed_addr(after_comma);
2348            return (
2349                StreamingSedAddr::Range(Box::new(StreamingSedAddr::Last), Box::new(addr2)),
2350                rest2,
2351            );
2352        }
2353        return (StreamingSedAddr::Last, rest);
2354    }
2355    let num_end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
2356    if num_end > 0 {
2357        if let Ok(n) = s[..num_end].parse::<usize>() {
2358            let rest = &s[num_end..];
2359            if let Some(after_comma) = rest.strip_prefix(',') {
2360                let (addr2, rest2) = parse_streaming_sed_addr(after_comma);
2361                return (
2362                    StreamingSedAddr::Range(Box::new(StreamingSedAddr::Line(n)), Box::new(addr2)),
2363                    rest2,
2364                );
2365            }
2366            return (StreamingSedAddr::Line(n), rest);
2367        }
2368    }
2369    (StreamingSedAddr::None, s)
2370}
2371
2372fn parse_streaming_sed_cmd(rest: &str) -> Option<StreamingSedCmd> {
2373    if rest.starts_with('s') {
2374        return parse_streaming_sed_substitute(rest).map(StreamingSedCmd::Substitute);
2375    }
2376    match rest {
2377        "d" => return Some(StreamingSedCmd::Delete),
2378        "p" => return Some(StreamingSedCmd::Print),
2379        "q" => return Some(StreamingSedCmd::Quit),
2380        _ => {}
2381    }
2382    if rest.starts_with("y/") || rest.starts_with("y|") {
2383        let delim = rest.as_bytes()[1] as char;
2384        let parts: Vec<&str> = rest[2..].split(delim).collect();
2385        return (parts.len() >= 2).then(|| {
2386            StreamingSedCmd::Transliterate(parts[0].chars().collect(), parts[1].chars().collect())
2387        });
2388    }
2389    if let Some(text) = rest.strip_prefix("a\\") {
2390        return Some(StreamingSedCmd::AppendText(text.trim_start().to_string()));
2391    }
2392    if let Some(text) = rest.strip_prefix("i\\") {
2393        return Some(StreamingSedCmd::InsertText(text.trim_start().to_string()));
2394    }
2395    if let Some(text) = rest.strip_prefix("c\\") {
2396        return Some(StreamingSedCmd::ChangeText(text.trim_start().to_string()));
2397    }
2398    None
2399}
2400
2401fn parse_streaming_sed_script(script: &str) -> Vec<StreamingSedInstruction> {
2402    let mut instructions = Vec::new();
2403    for part in script.split(';') {
2404        let part = part.trim();
2405        if part.is_empty() {
2406            continue;
2407        }
2408        let (addr, rest) = parse_streaming_sed_addr(part);
2409        if let Some(cmd) = parse_streaming_sed_cmd(rest.trim()) {
2410            instructions.push(StreamingSedInstruction { addr, cmd });
2411        }
2412    }
2413    instructions
2414}
2415
2416fn streaming_sed_addr_matches(
2417    addr: &StreamingSedAddr,
2418    line_num: usize,
2419    is_last: bool,
2420    line: &str,
2421    in_range: &mut bool,
2422) -> bool {
2423    match addr {
2424        StreamingSedAddr::None => true,
2425        StreamingSedAddr::Line(n) => line_num == *n,
2426        StreamingSedAddr::Last => is_last,
2427        StreamingSedAddr::Regex(pat) => {
2428            use posix_regex::compile::PosixRegexBuilder;
2429            PosixRegexBuilder::new(pat.as_bytes())
2430                .with_default_classes()
2431                .compile()
2432                .map_or_else(
2433                    |_| streaming_simple_grep_match(line, pat),
2434                    |re| !re.matches(line.as_bytes(), Some(1)).is_empty(),
2435                )
2436        }
2437        StreamingSedAddr::Range(start, end) => {
2438            if *in_range {
2439                if streaming_sed_addr_matches(end, line_num, is_last, line, &mut false) {
2440                    *in_range = false;
2441                }
2442                true
2443            } else if streaming_sed_addr_matches(start, line_num, is_last, line, &mut false) {
2444                *in_range = true;
2445                true
2446            } else {
2447                false
2448            }
2449        }
2450    }
2451}
2452
2453/// Perform a sed `s///` substitution with POSIX BRE regex support.
2454/// Falls back to literal replacement if the pattern fails to compile.
2455///
2456/// For global (`g`) replacements we iterate one match at a time because
2457/// `posix-regex`'s `matches()` may return fewer results than expected
2458/// for simple patterns — the safe approach is to find-then-advance in
2459/// a loop.
2460fn streaming_sed_substitute(text: &str, pattern: &str, replacement: &str, global: bool) -> String {
2461    use posix_regex::compile::PosixRegexBuilder;
2462
2463    let compiled = PosixRegexBuilder::new(pattern.as_bytes())
2464        .with_default_classes()
2465        .compile();
2466
2467    let Ok(re) = compiled else {
2468        // Fall back to literal replacement.
2469        return if global {
2470            text.replace(pattern, replacement)
2471        } else {
2472            text.replacen(pattern, replacement, 1)
2473        };
2474    };
2475
2476    let mut out = String::with_capacity(text.len());
2477    let mut cursor = 0usize;
2478
2479    loop {
2480        if cursor > text.len() {
2481            break;
2482        }
2483        let remaining = &text.as_bytes()[cursor..];
2484        let matches = re.matches(remaining, Some(1));
2485        let Some(caps) = matches.into_iter().next() else {
2486            break;
2487        };
2488        let Some(Some((rel_start, rel_end))) = caps.first().copied() else {
2489            break;
2490        };
2491        let abs_start = cursor + rel_start;
2492        let abs_end = cursor + rel_end;
2493
2494        out.push_str(&text[cursor..abs_start]);
2495        // Expand replacement template with captures relative to `remaining`.
2496        streaming_sed_expand_replacement(&mut out, replacement, &text[cursor..], &caps);
2497        cursor = if abs_end == abs_start {
2498            abs_end + 1
2499        } else {
2500            abs_end
2501        };
2502
2503        if !global {
2504            break;
2505        }
2506    }
2507
2508    if cursor <= text.len() {
2509        out.push_str(&text[cursor..]);
2510    }
2511    out
2512}
2513
2514fn streaming_sed_expand_replacement(
2515    out: &mut String,
2516    template: &str,
2517    subject: &str,
2518    caps: &[Option<(usize, usize)>],
2519) {
2520    let mut chars = template.chars().peekable();
2521    while let Some(c) = chars.next() {
2522        match c {
2523            '\\' => streaming_sed_expand_escape(out, &mut chars, subject, caps),
2524            '&' => streaming_sed_expand_whole_match(out, subject, caps),
2525            other => out.push(other),
2526        }
2527    }
2528}
2529
2530fn streaming_sed_expand_escape(
2531    out: &mut String,
2532    chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
2533    subject: &str,
2534    caps: &[Option<(usize, usize)>],
2535) {
2536    let Some(&next) = chars.peek() else {
2537        out.push('\\');
2538        return;
2539    };
2540    if let Some(digit) = next.to_digit(10) {
2541        chars.next();
2542        if let Some(Some((s, e))) = caps.get(digit as usize).copied() {
2543            out.push_str(&subject[s..e]);
2544        }
2545        return;
2546    }
2547    chars.next();
2548    match next {
2549        '\\' => out.push('\\'),
2550        '&' => out.push('&'),
2551        'n' => out.push('\n'),
2552        't' => out.push('\t'),
2553        other => out.push(other),
2554    }
2555}
2556
2557fn streaming_sed_expand_whole_match(
2558    out: &mut String,
2559    subject: &str,
2560    caps: &[Option<(usize, usize)>],
2561) {
2562    if let Some(Some((s, e))) = caps.first().copied() {
2563        out.push_str(&subject[s..e]);
2564    }
2565}
2566
2567fn streaming_sed_emit_line(output: &mut Vec<u8>, line: &str) {
2568    output.extend_from_slice(line.as_bytes());
2569    output.push(b'\n');
2570}
2571
2572impl<R> SedStreamReader<R> {
2573    fn new(inner: R, stage: StreamingSedStage) -> Self {
2574        let range_states = vec![false; stage.instructions.len()];
2575        Self {
2576            inner,
2577            stage,
2578            input_pending: Vec::new(),
2579            output_pending: Vec::new(),
2580            output_offset: 0,
2581            initialized: false,
2582            finished: false,
2583            current: None,
2584            next: None,
2585            line_num: 1,
2586            range_states,
2587            input_eof: false,
2588        }
2589    }
2590
2591    fn fill_lookahead(&mut self) -> std::io::Result<()>
2592    where
2593        R: Read,
2594    {
2595        if self.current.is_none() {
2596            self.current = streaming_read_next_line(&mut self.inner, &mut self.input_pending)?;
2597        }
2598        if self.current.is_some() && self.next.is_none() && !self.input_eof {
2599            match streaming_read_next_line(&mut self.inner, &mut self.input_pending)? {
2600                Some(line) => self.next = Some(line),
2601                None => self.input_eof = true,
2602            }
2603        }
2604        Ok(())
2605    }
2606
2607    fn initialize(&mut self) -> std::io::Result<()>
2608    where
2609        R: Read,
2610    {
2611        if self.initialized {
2612            return Ok(());
2613        }
2614        self.fill_lookahead()?;
2615        self.initialized = true;
2616        Ok(())
2617    }
2618
2619    fn apply_instructions(&mut self, line: String, is_last: bool) -> StreamingSedLineResult {
2620        let mut current_text = line;
2621        let mut printed = false;
2622        for idx in 0..self.stage.instructions.len() {
2623            let matches_addr = streaming_sed_addr_matches(
2624                &self.stage.instructions[idx].addr,
2625                self.line_num,
2626                is_last,
2627                &current_text,
2628                &mut self.range_states[idx],
2629            );
2630            if !matches_addr {
2631                continue;
2632            }
2633            if let Some(result) = self.apply_sed_instruction(idx, &mut current_text, &mut printed) {
2634                return result;
2635            }
2636        }
2637        if !self.stage.suppress_print && !printed {
2638            streaming_sed_emit_line(&mut self.output_pending, &current_text);
2639        }
2640        StreamingSedLineResult::Continue
2641    }
2642
2643    fn apply_sed_instruction(
2644        &mut self,
2645        idx: usize,
2646        current_text: &mut String,
2647        printed: &mut bool,
2648    ) -> Option<StreamingSedLineResult> {
2649        let cmd = self.stage.instructions[idx].cmd.clone();
2650        match cmd {
2651            StreamingSedCmd::Substitute(sub) => {
2652                *current_text = streaming_sed_substitute(
2653                    current_text,
2654                    &sub.pattern,
2655                    &sub.replacement,
2656                    sub.global,
2657                );
2658            }
2659            StreamingSedCmd::Delete => return Some(StreamingSedLineResult::Delete),
2660            StreamingSedCmd::Print => {
2661                streaming_sed_emit_line(&mut self.output_pending, current_text);
2662                *printed = true;
2663            }
2664            StreamingSedCmd::Transliterate(from, to) => {
2665                *current_text = streaming_sed_transliterate(current_text, &from, &to);
2666            }
2667            StreamingSedCmd::AppendText(text) => {
2668                if !self.stage.suppress_print && !*printed {
2669                    streaming_sed_emit_line(&mut self.output_pending, current_text);
2670                    *printed = true;
2671                }
2672                streaming_sed_emit_line(&mut self.output_pending, &text);
2673            }
2674            StreamingSedCmd::InsertText(text) => {
2675                streaming_sed_emit_line(&mut self.output_pending, &text);
2676            }
2677            StreamingSedCmd::ChangeText(text) => {
2678                streaming_sed_emit_line(&mut self.output_pending, &text);
2679                return Some(StreamingSedLineResult::Delete);
2680            }
2681            StreamingSedCmd::Quit => {
2682                if !self.stage.suppress_print && !*printed {
2683                    streaming_sed_emit_line(&mut self.output_pending, current_text);
2684                }
2685                return Some(StreamingSedLineResult::Quit);
2686            }
2687        }
2688        None
2689    }
2690}
2691
2692enum StreamingSedLineResult {
2693    Continue,
2694    Delete,
2695    Quit,
2696}
2697
2698fn streaming_sed_transliterate(text: &str, from: &[char], to: &[char]) -> String {
2699    text.chars()
2700        .map(|c| {
2701            if let Some(pos) = from.iter().position(|&fc| fc == c) {
2702                to.get(pos).or(to.last()).copied().unwrap_or(c)
2703            } else {
2704                c
2705            }
2706        })
2707        .collect()
2708}
2709
2710impl<R: Read> Read for SedStreamReader<R> {
2711    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2712        if buf.is_empty() {
2713            return Ok(0);
2714        }
2715        loop {
2716            let copied =
2717                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2718            if copied > 0 {
2719                return Ok(copied);
2720            }
2721            if self.finished {
2722                return Ok(0);
2723            }
2724
2725            self.initialize()?;
2726            self.fill_lookahead()?;
2727            let Some((line, _had_newline)) = self.current.take() else {
2728                self.finished = true;
2729                continue;
2730            };
2731
2732            let is_last = self.input_eof && self.next.is_none();
2733            match self.apply_instructions(line, is_last) {
2734                StreamingSedLineResult::Quit => self.finished = true,
2735                StreamingSedLineResult::Delete | StreamingSedLineResult::Continue => {
2736                    self.current = self.next.take();
2737                    if self.current.is_some() {
2738                        self.line_num += 1;
2739                        self.fill_lookahead()?;
2740                    } else {
2741                        self.finished = true;
2742                    }
2743                }
2744            }
2745        }
2746    }
2747}
2748
2749impl<R> PasteStreamReader<R> {
2750    fn new(inner: R, stage: StreamingPasteStage) -> Self {
2751        Self {
2752            inner,
2753            stage,
2754            input_pending: Vec::new(),
2755            output_pending: Vec::new(),
2756            output_offset: 0,
2757            finalized: false,
2758            ended_with_newline: true,
2759            serial_first: true,
2760        }
2761    }
2762
2763    fn finalize_serial(&mut self) -> std::io::Result<()>
2764    where
2765        R: Read,
2766    {
2767        while let Some((line, _had_newline)) =
2768            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2769        {
2770            if !self.serial_first {
2771                self.output_pending
2772                    .extend_from_slice(self.stage.delimiter.as_bytes());
2773            }
2774            self.output_pending.extend_from_slice(line.as_bytes());
2775            self.serial_first = false;
2776        }
2777        self.output_pending.push(b'\n');
2778        Ok(())
2779    }
2780}
2781
2782impl<R: Read> Read for PasteStreamReader<R> {
2783    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2784        if buf.is_empty() {
2785            return Ok(0);
2786        }
2787        loop {
2788            let copied =
2789                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2790            if copied > 0 {
2791                return Ok(copied);
2792            }
2793            if self.finalized {
2794                return Ok(0);
2795            }
2796
2797            if self.stage.serial {
2798                self.finalize_serial()?;
2799                self.finalized = true;
2800                continue;
2801            }
2802
2803            let mut scratch = [0u8; 4096];
2804            let read = self.inner.read(&mut scratch)?;
2805            if read == 0 {
2806                if !self.ended_with_newline {
2807                    self.output_pending.push(b'\n');
2808                }
2809                self.finalized = true;
2810                continue;
2811            }
2812            self.ended_with_newline = scratch[read - 1] == b'\n';
2813            self.output_pending.extend_from_slice(&scratch[..read]);
2814        }
2815    }
2816}
2817
2818impl<R> ColumnStreamReader<R> {
2819    fn new(inner: R) -> Self {
2820        Self {
2821            inner,
2822            output_pending: Vec::new(),
2823            output_offset: 0,
2824            finalized: false,
2825            ended_with_newline: true,
2826        }
2827    }
2828}
2829
2830impl<R: Read> Read for ColumnStreamReader<R> {
2831    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2832        if buf.is_empty() {
2833            return Ok(0);
2834        }
2835        loop {
2836            let copied =
2837                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2838            if copied > 0 {
2839                return Ok(copied);
2840            }
2841            if self.finalized {
2842                return Ok(0);
2843            }
2844
2845            let mut scratch = [0u8; 4096];
2846            let read = self.inner.read(&mut scratch)?;
2847            if read == 0 {
2848                if !self.ended_with_newline {
2849                    self.output_pending.push(b'\n');
2850                }
2851                self.finalized = true;
2852                continue;
2853            }
2854            self.ended_with_newline = scratch[read - 1] == b'\n';
2855            self.output_pending.extend_from_slice(&scratch[..read]);
2856        }
2857    }
2858}
2859
2860fn take_pending_output(pending: &mut Vec<u8>, pending_offset: &mut usize, buf: &mut [u8]) -> usize {
2861    if *pending_offset >= pending.len() {
2862        pending.clear();
2863        *pending_offset = 0;
2864        return 0;
2865    }
2866    let remaining = &pending[*pending_offset..];
2867    let to_copy = remaining.len().min(buf.len());
2868    buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
2869    *pending_offset += to_copy;
2870    if *pending_offset == pending.len() {
2871        pending.clear();
2872        *pending_offset = 0;
2873    }
2874    to_copy
2875}
2876
2877fn streaming_read_next_line(
2878    reader: &mut dyn Read,
2879    pending: &mut Vec<u8>,
2880) -> std::io::Result<Option<(String, bool)>> {
2881    loop {
2882        if let Some(pos) = pending.iter().position(|&b| b == b'\n') {
2883            let mut line = pending.drain(..=pos).collect::<Vec<u8>>();
2884            let _ = line.pop();
2885            return Ok(Some((String::from_utf8_lossy(&line).to_string(), true)));
2886        }
2887
2888        let mut buffer = [0u8; 4096];
2889        match reader.read(&mut buffer) {
2890            Ok(0) => {
2891                if pending.is_empty() {
2892                    return Ok(None);
2893                }
2894                let line = std::mem::take(pending);
2895                return Ok(Some((String::from_utf8_lossy(&line).to_string(), false)));
2896            }
2897            Ok(read) => pending.extend_from_slice(&buffer[..read]),
2898            Err(err) => return Err(err),
2899        }
2900    }
2901}
2902
2903struct RevStreamReader<R> {
2904    inner: R,
2905    input_pending: Vec<u8>,
2906    output_pending: Vec<u8>,
2907    output_offset: usize,
2908    finished: bool,
2909}
2910
2911impl<R> RevStreamReader<R> {
2912    fn new(inner: R) -> Self {
2913        Self {
2914            inner,
2915            input_pending: Vec::new(),
2916            output_pending: Vec::new(),
2917            output_offset: 0,
2918            finished: false,
2919        }
2920    }
2921}
2922
2923impl<R: Read> Read for RevStreamReader<R> {
2924    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
2925        if buf.is_empty() {
2926            return Ok(0);
2927        }
2928        let copied = take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
2929        if copied > 0 {
2930            return Ok(copied);
2931        }
2932        if self.finished {
2933            return Ok(0);
2934        }
2935
2936        if let Some((line, _had_newline)) =
2937            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
2938        {
2939            let reversed: String = line.chars().rev().collect();
2940            self.output_pending.extend_from_slice(reversed.as_bytes());
2941            self.output_pending.push(b'\n');
2942            Ok(take_pending_output(
2943                &mut self.output_pending,
2944                &mut self.output_offset,
2945                buf,
2946            ))
2947        } else {
2948            self.finished = true;
2949            Ok(0)
2950        }
2951    }
2952}
2953
2954fn streaming_cut_range_includes(ranges: &[StreamingCutRange], idx: usize) -> bool {
2955    ranges.iter().any(|range| {
2956        let start = range.start.unwrap_or(1);
2957        let end = range.end.unwrap_or(usize::MAX);
2958        idx >= start && idx <= end
2959    })
2960}
2961
2962fn apply_streaming_cut(line: &str, stage: &StreamingCutStage) -> Option<Vec<u8>> {
2963    match &stage.mode {
2964        StreamingCutMode::Fields(ranges) => {
2965            if stage.only_delimited && !line.contains(stage.delim) {
2966                return None;
2967            }
2968            let parts: Vec<&str> = line.split(stage.delim).collect();
2969            let selected: Vec<&str> = parts
2970                .iter()
2971                .enumerate()
2972                .filter(|(idx, _)| {
2973                    let included = streaming_cut_range_includes(ranges, idx + 1);
2974                    if stage.complement {
2975                        !included
2976                    } else {
2977                        included
2978                    }
2979                })
2980                .map(|(_, part)| *part)
2981                .collect();
2982            Some(selected.join(&stage.output_delim).into_bytes())
2983        }
2984        StreamingCutMode::Chars(ranges) | StreamingCutMode::Bytes(ranges) => {
2985            let chars: Vec<char> = line.chars().collect();
2986            let selected: String = chars
2987                .iter()
2988                .enumerate()
2989                .filter(|(idx, _)| {
2990                    let included = streaming_cut_range_includes(ranges, idx + 1);
2991                    if stage.complement {
2992                        !included
2993                    } else {
2994                        included
2995                    }
2996                })
2997                .map(|(_, ch)| *ch)
2998                .collect();
2999            Some(selected.into_bytes())
3000        }
3001    }
3002}
3003
3004struct CutStreamReader<R> {
3005    inner: R,
3006    stage: StreamingCutStage,
3007    input_pending: Vec<u8>,
3008    output_pending: Vec<u8>,
3009    output_offset: usize,
3010    finished: bool,
3011}
3012
3013impl<R> CutStreamReader<R> {
3014    fn new(inner: R, stage: StreamingCutStage) -> Self {
3015        Self {
3016            inner,
3017            stage,
3018            input_pending: Vec::new(),
3019            output_pending: Vec::new(),
3020            output_offset: 0,
3021            finished: false,
3022        }
3023    }
3024}
3025
3026impl<R: Read> Read for CutStreamReader<R> {
3027    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3028        if buf.is_empty() {
3029            return Ok(0);
3030        }
3031        loop {
3032            let copied =
3033                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
3034            if copied > 0 {
3035                return Ok(copied);
3036            }
3037            if self.finished {
3038                return Ok(0);
3039            }
3040
3041            match streaming_read_next_line(&mut self.inner, &mut self.input_pending)? {
3042                Some((line, _had_newline)) => {
3043                    if let Some(mut out) = apply_streaming_cut(&line, &self.stage) {
3044                        out.push(b'\n');
3045                        self.output_pending.extend_from_slice(&out);
3046                    }
3047                }
3048                None => {
3049                    self.finished = true;
3050                }
3051            }
3052        }
3053    }
3054}
3055
3056fn streaming_grep_match_single(line: &str, pattern: &str, flags: &StreamingGrepFlags) -> bool {
3057    use posix_regex::compile::PosixRegexBuilder;
3058
3059    if flags.word_match {
3060        return line
3061            .split(|c: char| !c.is_alphanumeric() && c != '_')
3062            .any(|word| word == pattern);
3063    }
3064    if flags.fixed {
3065        return line.contains(pattern);
3066    }
3067    // Try POSIX regex first; fall back to literal substring on compile error.
3068    match PosixRegexBuilder::new(pattern.as_bytes())
3069        .with_default_classes()
3070        .compile()
3071    {
3072        Ok(re) => !re.matches(line.as_bytes(), Some(1)).is_empty(),
3073        Err(_) => line.contains(pattern),
3074    }
3075}
3076
3077fn streaming_grep_match_pattern(line: &str, pattern: &str, flags: &StreamingGrepFlags) -> bool {
3078    let (line_cmp, pattern_cmp) = if flags.ignore_case {
3079        (line.to_lowercase(), pattern.to_lowercase())
3080    } else {
3081        (line.to_string(), pattern.to_string())
3082    };
3083    if flags.extended && pattern_cmp.contains('|') {
3084        return pattern_cmp
3085            .split('|')
3086            .any(|alt| streaming_grep_match_single(&line_cmp, alt.trim(), flags));
3087    }
3088    streaming_grep_match_single(&line_cmp, &pattern_cmp, flags)
3089}
3090
3091fn streaming_grep_find_match<'a>(
3092    line: &'a str,
3093    pattern: &str,
3094    flags: &StreamingGrepFlags,
3095) -> Option<&'a str> {
3096    let (line_cmp, pattern_cmp) = if flags.ignore_case {
3097        (line.to_lowercase(), pattern.to_lowercase())
3098    } else {
3099        (line.to_string(), pattern.to_string())
3100    };
3101    if flags.word_match {
3102        let start = line_cmp.find(&pattern_cmp)?;
3103        if start > 0 && line_cmp.as_bytes()[start - 1].is_ascii_alphanumeric() {
3104            return None;
3105        }
3106        let end = start + pattern_cmp.len();
3107        if end < line_cmp.len() && line_cmp.as_bytes()[end].is_ascii_alphanumeric() {
3108            return None;
3109        }
3110        Some(&line[start..start + pattern_cmp.len()])
3111    } else {
3112        let idx = line_cmp.find(&pattern_cmp)?;
3113        Some(&line[idx..idx + pattern_cmp.len()])
3114    }
3115}
3116
3117fn streaming_grep_line_matches(
3118    line: &str,
3119    flags: &StreamingGrepFlags,
3120    patterns: &[String],
3121) -> bool {
3122    let matched = patterns
3123        .iter()
3124        .any(|pattern| streaming_grep_match_pattern(line, pattern, flags));
3125    matched != flags.invert
3126}
3127
3128fn emit_streaming_grep_one(
3129    output: &mut Vec<u8>,
3130    line: &str,
3131    line_num: usize,
3132    flags: &StreamingGrepFlags,
3133    patterns: &[String],
3134) {
3135    let mut prefix = String::new();
3136    if flags.show_filename == Some(true) {
3137        prefix.push_str("(standard input):");
3138    }
3139    if flags.show_line_numbers {
3140        use std::fmt::Write;
3141        let _ = write!(prefix, "{line_num}:");
3142    }
3143    if flags.only_matching {
3144        for pattern in patterns {
3145            if let Some(matched) = streaming_grep_find_match(line, pattern, flags) {
3146                output.extend_from_slice(prefix.as_bytes());
3147                output.extend_from_slice(matched.as_bytes());
3148                output.push(b'\n');
3149            }
3150        }
3151    } else {
3152        output.extend_from_slice(prefix.as_bytes());
3153        output.extend_from_slice(line.as_bytes());
3154        output.push(b'\n');
3155    }
3156}
3157
3158#[allow(clippy::struct_excessive_bools)]
3159struct GrepStreamReader<R> {
3160    inner: R,
3161    stage: StreamingGrepStage,
3162    status: Rc<RefCell<i32>>,
3163    input_pending: Vec<u8>,
3164    output_pending: Vec<u8>,
3165    output_offset: usize,
3166    finished: bool,
3167    match_count: u64,
3168    found: bool,
3169    remaining_after: usize,
3170    printed_separator: bool,
3171    before_buf: VecDeque<(usize, String)>,
3172    line_num: usize,
3173    emitted_count_summary: bool,
3174}
3175
3176impl<R> GrepStreamReader<R> {
3177    fn new(inner: R, stage: StreamingGrepStage, status: Rc<RefCell<i32>>) -> Self {
3178        Self {
3179            inner,
3180            stage,
3181            status,
3182            input_pending: Vec::new(),
3183            output_pending: Vec::new(),
3184            output_offset: 0,
3185            finished: false,
3186            match_count: 0,
3187            found: false,
3188            remaining_after: 0,
3189            printed_separator: false,
3190            before_buf: VecDeque::new(),
3191            line_num: 0,
3192            emitted_count_summary: false,
3193        }
3194    }
3195
3196    fn emit_count_summary(&mut self) {
3197        if self.stage.flags.count_only && !self.stage.flags.quiet && !self.emitted_count_summary {
3198            if self.stage.flags.show_filename == Some(true) {
3199                self.output_pending.extend_from_slice(
3200                    format!("(standard input):{}\n", self.match_count).as_bytes(),
3201                );
3202            } else {
3203                self.output_pending
3204                    .extend_from_slice(format!("{}\n", self.match_count).as_bytes());
3205            }
3206            self.emitted_count_summary = true;
3207        }
3208    }
3209}
3210
3211impl<R: Read> Read for GrepStreamReader<R> {
3212    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3213        if buf.is_empty() {
3214            return Ok(0);
3215        }
3216        loop {
3217            let copied =
3218                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
3219            if copied > 0 {
3220                return Ok(copied);
3221            }
3222            if self.finished {
3223                return Ok(0);
3224            }
3225            self.pump_one_line()?;
3226        }
3227    }
3228}
3229
3230impl<R: Read> GrepStreamReader<R> {
3231    fn pump_one_line(&mut self) -> std::io::Result<()> {
3232        let Some((line, _had_newline)) =
3233            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
3234        else {
3235            self.emit_count_summary();
3236            if !self.found {
3237                *self.status.borrow_mut() = 1;
3238            }
3239            self.finished = true;
3240            return Ok(());
3241        };
3242        self.line_num += 1;
3243        if streaming_grep_line_matches(&line, &self.stage.flags, &self.stage.patterns) {
3244            self.on_match(&line);
3245        } else {
3246            self.on_nonmatch(line);
3247        }
3248        Ok(())
3249    }
3250
3251    fn on_match(&mut self, line: &str) {
3252        self.found = true;
3253        *self.status.borrow_mut() = 0;
3254        self.match_count += 1;
3255
3256        if self.stage.flags.quiet || self.stage.flags.files_only {
3257            self.check_max_count();
3258            return;
3259        }
3260
3261        if !self.stage.flags.count_only {
3262            self.flush_before_context();
3263            emit_streaming_grep_one(
3264                &mut self.output_pending,
3265                line,
3266                self.line_num,
3267                &self.stage.flags,
3268                &self.stage.patterns,
3269            );
3270            self.remaining_after = self.stage.flags.after_context;
3271            self.printed_separator = true;
3272        }
3273
3274        if self.should_stop_for_max_count() {
3275            self.emit_count_summary();
3276            self.finished = true;
3277        }
3278    }
3279
3280    fn on_nonmatch(&mut self, line: String) {
3281        if self.remaining_after > 0 && !self.stage.flags.count_only {
3282            emit_streaming_grep_one(
3283                &mut self.output_pending,
3284                &line,
3285                self.line_num,
3286                &self.stage.flags,
3287                &self.stage.patterns,
3288            );
3289            self.remaining_after -= 1;
3290            return;
3291        }
3292        if self.stage.flags.before_context > 0 {
3293            self.before_buf.push_back((self.line_num, line));
3294            if self.before_buf.len() > self.stage.flags.before_context {
3295                self.before_buf.pop_front();
3296            }
3297        }
3298    }
3299
3300    fn flush_before_context(&mut self) {
3301        if self.stage.flags.before_context == 0 || self.before_buf.is_empty() {
3302            self.before_buf.clear();
3303            return;
3304        }
3305        if self.printed_separator {
3306            self.output_pending.extend_from_slice(b"--\n");
3307        }
3308        for (before_line_num, before_line) in &self.before_buf {
3309            emit_streaming_grep_one(
3310                &mut self.output_pending,
3311                before_line,
3312                *before_line_num,
3313                &self.stage.flags,
3314                &self.stage.patterns,
3315            );
3316        }
3317        self.before_buf.clear();
3318    }
3319
3320    fn should_stop_for_max_count(&self) -> bool {
3321        self.stage
3322            .flags
3323            .max_count
3324            .is_some_and(|m| self.match_count >= m as u64)
3325    }
3326
3327    fn check_max_count(&mut self) {
3328        if self.should_stop_for_max_count() {
3329            self.finished = true;
3330        }
3331    }
3332}
3333
3334fn streaming_uniq_compare_key(line: &str, flags: &StreamingUniqFlags) -> String {
3335    let mut slice = line;
3336    for _ in 0..flags.skip_fields {
3337        slice = slice.trim_start();
3338        if let Some(pos) = slice.find(char::is_whitespace) {
3339            slice = &slice[pos..];
3340        } else {
3341            slice = "";
3342            break;
3343        }
3344    }
3345    if flags.skip_chars > 0 {
3346        let chars: Vec<char> = slice.chars().collect();
3347        slice = if flags.skip_chars < chars.len() {
3348            &slice[chars[..flags.skip_chars]
3349                .iter()
3350                .map(|ch| ch.len_utf8())
3351                .sum::<usize>()..]
3352        } else {
3353            ""
3354        };
3355    }
3356    let mut key = slice.to_string();
3357    if let Some(limit) = flags.compare_chars {
3358        key = key.chars().take(limit).collect();
3359    }
3360    if flags.ignore_case {
3361        key = key.to_lowercase();
3362    }
3363    key
3364}
3365
3366fn emit_streaming_uniq_line(
3367    output: &mut Vec<u8>,
3368    line: &str,
3369    count: usize,
3370    flags: &StreamingUniqFlags,
3371) {
3372    if flags.duplicates_only && count < 2 {
3373        return;
3374    }
3375    if flags.unique_only && count > 1 {
3376        return;
3377    }
3378    if flags.count {
3379        output.extend_from_slice(format!("{count:>7} {line}\n").as_bytes());
3380    } else {
3381        output.extend_from_slice(line.as_bytes());
3382        output.push(b'\n');
3383    }
3384}
3385
3386struct UniqStreamReader<R> {
3387    inner: R,
3388    flags: StreamingUniqFlags,
3389    input_pending: Vec<u8>,
3390    output_pending: Vec<u8>,
3391    output_offset: usize,
3392    finished: bool,
3393    prev: Option<(String, String)>,
3394    count: usize,
3395}
3396
3397impl<R> UniqStreamReader<R> {
3398    fn new(inner: R, flags: StreamingUniqFlags) -> Self {
3399        Self {
3400            inner,
3401            flags,
3402            input_pending: Vec::new(),
3403            output_pending: Vec::new(),
3404            output_offset: 0,
3405            finished: false,
3406            prev: None,
3407            count: 0,
3408        }
3409    }
3410}
3411
3412impl<R: Read> Read for UniqStreamReader<R> {
3413    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3414        if buf.is_empty() {
3415            return Ok(0);
3416        }
3417        loop {
3418            let copied =
3419                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
3420            if copied > 0 {
3421                return Ok(copied);
3422            }
3423            if self.finished {
3424                return Ok(0);
3425            }
3426            self.pump_next_uniq_line()?;
3427        }
3428    }
3429}
3430
3431impl<R: Read> UniqStreamReader<R> {
3432    fn pump_next_uniq_line(&mut self) -> std::io::Result<()> {
3433        if let Some((line, _had_newline)) =
3434            streaming_read_next_line(&mut self.inner, &mut self.input_pending)?
3435        {
3436            self.handle_uniq_line(line);
3437        } else {
3438            self.flush_uniq_prev();
3439            self.finished = true;
3440        }
3441        Ok(())
3442    }
3443
3444    fn handle_uniq_line(&mut self, line: String) {
3445        let key = streaming_uniq_compare_key(&line, &self.flags);
3446        if self
3447            .prev
3448            .as_ref()
3449            .is_some_and(|(_, prev_key)| *prev_key == key)
3450        {
3451            self.count += 1;
3452            return;
3453        }
3454        self.flush_uniq_prev();
3455        self.prev = Some((line, key));
3456        self.count = 1;
3457    }
3458
3459    fn flush_uniq_prev(&mut self) {
3460        if let Some((prev_line, _)) = self.prev.take() {
3461            emit_streaming_uniq_line(
3462                &mut self.output_pending,
3463                &prev_line,
3464                self.count,
3465                &self.flags,
3466            );
3467        }
3468    }
3469}
3470
3471fn streaming_tr_expand_posix_class(class_name: &str, chars: &mut Vec<char>) {
3472    match class_name {
3473        "upper" => chars.extend('A'..='Z'),
3474        "lower" => chars.extend('a'..='z'),
3475        "digit" => chars.extend('0'..='9'),
3476        "alpha" => {
3477            chars.extend('A'..='Z');
3478            chars.extend('a'..='z');
3479        }
3480        "alnum" => {
3481            chars.extend('0'..='9');
3482            chars.extend('A'..='Z');
3483            chars.extend('a'..='z');
3484        }
3485        "space" => chars.extend([' ', '\t', '\n', '\r', '\x0b', '\x0c']),
3486        "blank" => chars.extend([' ', '\t']),
3487        "punct" => chars.extend("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".chars()),
3488        _ => {}
3489    }
3490}
3491
3492fn streaming_tr_expand_set(s: &str) -> Vec<char> {
3493    let mut chars = Vec::new();
3494    let mut iter = s.chars().peekable();
3495    while let Some(ch) = iter.next() {
3496        if ch == '[' && iter.peek() == Some(&':') {
3497            iter.next();
3498            let class_name: String = iter.by_ref().take_while(|&c| c != ':').collect();
3499            let _ = iter.next();
3500            streaming_tr_expand_posix_class(&class_name, &mut chars);
3501        } else if iter.peek() == Some(&'-') {
3502            streaming_tr_expand_range(ch, &mut iter, &mut chars);
3503        } else if ch == '\\' {
3504            chars.push(streaming_tr_unescape(&mut iter));
3505        } else {
3506            chars.push(ch);
3507        }
3508    }
3509    chars
3510}
3511
3512fn streaming_tr_expand_range(
3513    ch: char,
3514    iter: &mut std::iter::Peekable<std::str::Chars<'_>>,
3515    chars: &mut Vec<char>,
3516) {
3517    let saved = iter.clone();
3518    iter.next(); // consume '-'
3519    if let Some(&end_ch) = iter.peek() {
3520        if end_ch > ch {
3521            chars.extend(ch..=end_ch);
3522            iter.next();
3523        } else {
3524            chars.push(ch);
3525            *iter = saved;
3526            iter.next();
3527            chars.push('-');
3528        }
3529    } else {
3530        chars.push(ch);
3531        chars.push('-');
3532    }
3533}
3534
3535fn streaming_tr_unescape(iter: &mut std::iter::Peekable<std::str::Chars<'_>>) -> char {
3536    match iter.next() {
3537        Some('n') => '\n',
3538        Some('t') => '\t',
3539        Some('r') => '\r',
3540        Some('\\') | None => '\\',
3541        Some(other) => other,
3542    }
3543}
3544
3545fn streaming_tr_process_utf8_chunk(pending: &mut Vec<u8>, chunk: &[u8], mut f: impl FnMut(char)) {
3546    pending.extend_from_slice(chunk);
3547    while streaming_tr_drain_once(pending, &mut f) {}
3548}
3549
3550/// Consumes one contiguous UTF-8 slice (or one invalid byte) from `pending`.
3551/// Returns true if more work may remain, false if `pending` is either empty,
3552/// fully consumed, or ends in an incomplete UTF-8 sequence that must wait.
3553fn streaming_tr_drain_once(pending: &mut Vec<u8>, f: &mut impl FnMut(char)) -> bool {
3554    match std::str::from_utf8(pending) {
3555        Ok(text) => {
3556            for ch in text.chars() {
3557                f(ch);
3558            }
3559            pending.clear();
3560            false
3561        }
3562        Err(err) => streaming_tr_consume_invalid_prefix(pending, &err, f),
3563    }
3564}
3565
3566fn streaming_tr_consume_invalid_prefix(
3567    pending: &mut Vec<u8>,
3568    err: &std::str::Utf8Error,
3569    f: &mut impl FnMut(char),
3570) -> bool {
3571    let valid = err.valid_up_to();
3572    if valid > 0 {
3573        let text = String::from_utf8_lossy(&pending[..valid]).to_string();
3574        for ch in text.chars() {
3575            f(ch);
3576        }
3577        pending.drain(..valid);
3578        return true;
3579    }
3580    if err.error_len().is_some() {
3581        let text = String::from_utf8_lossy(&pending[..1]).to_string();
3582        for ch in text.chars() {
3583            f(ch);
3584        }
3585        pending.drain(..1);
3586        return true;
3587    }
3588    false
3589}
3590
3591fn streaming_tr_flush_pending_lossy(pending: &mut Vec<u8>, mut f: impl FnMut(char)) {
3592    if pending.is_empty() {
3593        return;
3594    }
3595    let text = String::from_utf8_lossy(pending).to_string();
3596    pending.clear();
3597    for ch in text.chars() {
3598        f(ch);
3599    }
3600}
3601
3602struct TrStreamReader<R> {
3603    inner: R,
3604    stage: StreamingTrStage,
3605    input_pending: Vec<u8>,
3606    output_pending: Vec<u8>,
3607    output_offset: usize,
3608    finished: bool,
3609    prev: Option<char>,
3610}
3611
3612impl<R> TrStreamReader<R> {
3613    fn new(inner: R, stage: StreamingTrStage) -> Self {
3614        Self {
3615            inner,
3616            stage,
3617            input_pending: Vec::new(),
3618            output_pending: Vec::new(),
3619            output_offset: 0,
3620            finished: false,
3621            prev: None,
3622        }
3623    }
3624
3625    fn emit_char(&mut self, ch: char) {
3626        let mut buffer = [0u8; 4];
3627        self.output_pending
3628            .extend_from_slice(ch.encode_utf8(&mut buffer).as_bytes());
3629        self.prev = Some(ch);
3630    }
3631
3632    fn is_in_from_set(&self, ch: char) -> bool {
3633        let in_set = self.stage.from_chars.contains(&ch);
3634        if self.stage.complement {
3635            !in_set
3636        } else {
3637            in_set
3638        }
3639    }
3640
3641    fn process_char(&mut self, ch: char) {
3642        if self.stage.delete {
3643            self.process_char_delete(ch);
3644            return;
3645        }
3646        if self.stage.squeeze && self.stage.to_chars.is_empty() {
3647            if self.stage.from_chars.contains(&ch) && self.prev == Some(ch) {
3648                return;
3649            }
3650            self.emit_char(ch);
3651            return;
3652        }
3653        self.process_char_translate(ch);
3654    }
3655
3656    fn process_char_delete(&mut self, ch: char) {
3657        if self.is_in_from_set(ch) {
3658            return;
3659        }
3660        if self.stage.squeeze && self.stage.to_chars.contains(&ch) && self.prev == Some(ch) {
3661            return;
3662        }
3663        self.emit_char(ch);
3664    }
3665
3666    fn process_char_translate(&mut self, ch: char) {
3667        let from_set = if self.stage.complement {
3668            (0u8..=127)
3669                .map(|b| b as char)
3670                .filter(|candidate| !self.stage.from_chars.contains(candidate))
3671                .collect::<Vec<_>>()
3672        } else {
3673            self.stage.from_chars.clone()
3674        };
3675        let translated = from_set
3676            .iter()
3677            .position(|&source| source == ch)
3678            .and_then(|pos| {
3679                self.stage
3680                    .to_chars
3681                    .get(pos)
3682                    .or(self.stage.to_chars.last())
3683                    .copied()
3684            })
3685            .unwrap_or(ch);
3686        if self.stage.squeeze
3687            && self.stage.to_chars.contains(&translated)
3688            && self.prev == Some(translated)
3689        {
3690            return;
3691        }
3692        self.emit_char(translated);
3693    }
3694}
3695
3696impl<R: Read> Read for TrStreamReader<R> {
3697    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3698        if buf.is_empty() {
3699            return Ok(0);
3700        }
3701        loop {
3702            let copied =
3703                take_pending_output(&mut self.output_pending, &mut self.output_offset, buf);
3704            if copied > 0 {
3705                return Ok(copied);
3706            }
3707            if self.finished {
3708                return Ok(0);
3709            }
3710
3711            let mut scratch = [0u8; 4096];
3712            let read = self.inner.read(&mut scratch)?;
3713            if read == 0 {
3714                let mut pending = std::mem::take(&mut self.input_pending);
3715                let mut chars = Vec::new();
3716                streaming_tr_flush_pending_lossy(&mut pending, |ch| chars.push(ch));
3717                self.input_pending = pending;
3718                for ch in chars {
3719                    self.process_char(ch);
3720                }
3721                self.finished = true;
3722                continue;
3723            }
3724            let mut pending = std::mem::take(&mut self.input_pending);
3725            let mut chars = Vec::new();
3726            streaming_tr_process_utf8_chunk(&mut pending, &scratch[..read], |ch| chars.push(ch));
3727            self.input_pending = pending;
3728            for ch in chars {
3729                self.process_char(ch);
3730            }
3731        }
3732    }
3733}
3734
3735/// Result from an external command handler.
3736#[derive(Debug)]
3737pub struct ExternalCommandResult {
3738    /// Data written to stdout.
3739    pub stdout: Vec<u8>,
3740    /// Data written to stderr.
3741    pub stderr: Vec<u8>,
3742    /// Exit code (0 = success).
3743    pub status: i32,
3744}
3745
3746pub struct ExternalCommandStdin<'a> {
3747    reader: Box<dyn Read + 'a>,
3748}
3749
3750impl std::fmt::Debug for ExternalCommandStdin<'_> {
3751    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3752        f.debug_struct("ExternalCommandStdin")
3753            .finish_non_exhaustive()
3754    }
3755}
3756
3757impl<'a> ExternalCommandStdin<'a> {
3758    #[must_use]
3759    pub fn from_bytes(data: &'a [u8]) -> Self {
3760        Self {
3761            reader: Box::new(Cursor::new(data)),
3762        }
3763    }
3764
3765    #[must_use]
3766    pub fn from_reader<R>(reader: R) -> Self
3767    where
3768        R: Read + 'a,
3769    {
3770        Self {
3771            reader: Box::new(reader),
3772        }
3773    }
3774
3775    pub fn read_chunk(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3776        self.reader.read(buf)
3777    }
3778}
3779
3780impl Read for ExternalCommandStdin<'_> {
3781    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3782        self.read_chunk(buf)
3783    }
3784}
3785
3786/// Callback type for external (host-provided) commands.
3787///
3788/// Called with `(command_name, argv, stdin)`. Returns `Some(result)` if
3789/// the command was handled, `None` to fall through to "command not found".
3790pub type ExternalCommandHandler = Box<
3791    dyn FnMut(&str, &[String], Option<ExternalCommandStdin<'_>>) -> Option<ExternalCommandResult>,
3792>;
3793
3794#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3795enum RuntimeCommandKind {
3796    Local,
3797    Break,
3798    Continue,
3799    Exit,
3800    Eval,
3801    Source,
3802    Declare,
3803    Let,
3804    Shopt,
3805    Alias,
3806    Unalias,
3807    BuiltinKeyword,
3808    Mapfile,
3809    Type,
3810    CommandKeyword,
3811    ExecKeyword,
3812    Hash,
3813    Times,
3814    Dirs,
3815    Pushd,
3816    Popd,
3817    Umask,
3818    Wait,
3819    Ulimit,
3820}
3821
3822#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3823enum UtilityCommandKind {
3824    Plain,
3825    FindWithExec,
3826    Xargs,
3827}
3828
3829#[derive(Clone, Debug)]
3830enum ResolvedCommand {
3831    Runtime(RuntimeCommandKind),
3832    ShellScript,
3833    /// A file with `#!/bin/bash` or similar shebang, executed directly by path.
3834    ShebangScript,
3835    Function(HirCommand),
3836    Builtin,
3837    Utility(UtilityCommandKind),
3838    External,
3839}
3840
3841#[derive(Clone, Debug)]
3842struct ActiveRun {
3843    input: String,
3844    hir: HirProgram,
3845    complete_index: usize,
3846    and_or_index: usize,
3847}
3848
3849impl ActiveRun {
3850    fn new(input: String, hir: HirProgram) -> Self {
3851        Self {
3852            input,
3853            hir,
3854            complete_index: 0,
3855            and_or_index: 0,
3856        }
3857    }
3858
3859    fn is_done(&self) -> bool {
3860        self.complete_index >= self.hir.items.len()
3861    }
3862}
3863
3864#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3865enum ActiveRunStep {
3866    Pending,
3867    Done,
3868}
3869
3870#[derive(Clone, Debug, PartialEq, Eq)]
3871pub enum ExecutionPoll {
3872    Yield(Vec<WorkerEvent>),
3873    Done(Vec<WorkerEvent>),
3874}
3875
3876#[derive(Clone, Debug, PartialEq, Eq)]
3877enum VmSubsetFallbackReason {
3878    Disabled,
3879    Lowering(LoweringError),
3880    AssignmentShape,
3881    UnsupportedWord,
3882    ShellExpansion,
3883    AliasExpansion,
3884    NonBuiltinCommand,
3885    CommandEnvPrefixes,
3886    UnsupportedRedirection,
3887}
3888
3889struct RuntimeVmExecutor<'a> {
3890    fs: &'a mut BackendFs,
3891    builtins: &'a wasmsh_builtins::BuiltinRegistry,
3892    current_exec_io: &'a mut Option<ExecIo>,
3893    proc_subst_out_scopes: &'a mut Vec<Vec<PendingProcessSubstOut>>,
3894    exec: &'a mut ExecState,
3895}
3896
3897impl RuntimeVmExecutor<'_> {
3898    fn prepare_exec_io(
3899        &mut self,
3900        state: &mut ShellState,
3901        redirections: &[IrRedirection],
3902    ) -> Result<Option<ExecIo>, String> {
3903        let mut exec_io = self.current_exec_io.clone().unwrap_or_default();
3904        let mut handled_any = false;
3905
3906        for redirection in redirections {
3907            let fd = redirection.fd.unwrap_or(1);
3908            let append = matches!(redirection.op, RedirectionOp::Append);
3909            let target = wasmsh_expand::expand_word(&redirection.target, state);
3910            let path = resolve_path_from_cwd(&state.cwd, &target);
3911            if matches!(redirection.op, RedirectionOp::Output)
3912                && state.get_var("SHOPT_C").as_deref() == Some("1")
3913                && self.fs.stat(&path).is_ok()
3914            {
3915                return Err(format!(
3916                    "wasmsh: {target}: cannot overwrite existing file\n"
3917                ));
3918            }
3919            let sink = match self.fs.open_write_sink(&path, append) {
3920                Ok(sink) => sink,
3921                Err(err) => {
3922                    return Err(format!("wasmsh: {target}: {err}\n"));
3923                }
3924            };
3925            exec_io.fds_mut().open_output(
3926                fd,
3927                OutputTarget::File {
3928                    path,
3929                    append,
3930                    sink: Rc::new(RefCell::new(sink)),
3931                },
3932            );
3933            handled_any = true;
3934        }
3935
3936        Ok(handled_any.then_some(exec_io))
3937    }
3938
3939    fn with_exec_io_scope<T>(
3940        current_exec_io: &mut Option<ExecIo>,
3941        proc_subst_out_scopes: &mut Vec<Vec<PendingProcessSubstOut>>,
3942        exec: &mut ExecState,
3943        exec_io: Option<ExecIo>,
3944        f: impl FnOnce(&mut Option<ExecIo>, &mut Vec<Vec<PendingProcessSubstOut>>, &mut ExecState) -> T,
3945    ) -> T {
3946        if let Some(exec_io) = exec_io {
3947            let saved = current_exec_io.replace(exec_io);
3948            let result = f(current_exec_io, proc_subst_out_scopes, exec);
3949            let current = current_exec_io.take();
3950            *current_exec_io = match (saved, current) {
3951                (Some(mut saved), Some(mut current)) => {
3952                    let stdin = current.take_stdin();
3953                    saved.fds_mut().set_input(stdin);
3954                    Some(saved)
3955                }
3956                (saved, _) => saved,
3957            };
3958            result
3959        } else {
3960            f(current_exec_io, proc_subst_out_scopes, exec)
3961        }
3962    }
3963
3964    fn write_visible_stderr(&mut self, vm: &mut Vm, data: &[u8]) {
3965        let mut router = RuntimeOutputRouter {
3966            exec: self.exec,
3967            exec_io: self.current_exec_io.as_mut(),
3968            proc_subst_out_scopes: self.proc_subst_out_scopes,
3969            vm_stdout: &mut vm.stdout,
3970            vm_stderr: &mut vm.stderr,
3971            vm_output_bytes: &mut vm.output_bytes,
3972            vm_output_limit: vm.limits.output_byte_limit,
3973            vm_diagnostics: &mut vm.diagnostics,
3974        };
3975        router.write_stderr(data);
3976    }
3977
3978    fn take_pending_input_reader(
3979        &mut self,
3980        cmd_name: &str,
3981    ) -> Result<Option<Box<dyn Read>>, String> {
3982        let Some(exec_io) = self.current_exec_io.as_mut() else {
3983            return Ok(None);
3984        };
3985        match exec_io.take_stdin() {
3986            InputTarget::Inherit | InputTarget::Closed => Ok(None),
3987            InputTarget::Bytes(data) => Ok(Some(Box::new(Cursor::new(data)))),
3988            InputTarget::File {
3989                path,
3990                remove_after_read,
3991            } => {
3992                let handle = self
3993                    .fs
3994                    .open(&path, OpenOptions::read())
3995                    .map_err(|err| format!("wasmsh: {cmd_name}: {err}\n"))?;
3996                let reader = self
3997                    .fs
3998                    .stream_file(handle)
3999                    .map_err(|err| format!("wasmsh: {cmd_name}: {err}\n"));
4000                self.fs.close(handle);
4001                if remove_after_read {
4002                    let _ = self.fs.remove_file(&path);
4003                }
4004                reader.map(Some)
4005            }
4006            InputTarget::Pipe(pipe) => Ok(Some(Box::new(PipeReader::new(pipe)))),
4007        }
4008    }
4009
4010    fn take_builtin_stdin(
4011        &mut self,
4012        cmd_name: &str,
4013    ) -> Result<Option<wasmsh_builtins::BuiltinStdin<'static>>, String> {
4014        let reader = self.take_pending_input_reader(cmd_name)?;
4015        Ok(reader.map(wasmsh_builtins::BuiltinStdin::from_reader))
4016    }
4017
4018    /// Drain a pending nounset error from parameter expansion so the VM-subset
4019    /// path reports it the same way the fallback interpreter does.
4020    fn consume_nounset_error(&mut self, vm: &mut Vm) -> bool {
4021        let Some(var_name) = vm.state.take_nounset_error() else {
4022            return false;
4023        };
4024        let msg = format!("wasmsh: {var_name}: unbound variable\n");
4025        self.write_visible_stderr(vm, msg.as_bytes());
4026        vm.state.last_status = 1;
4027        true
4028    }
4029}
4030
4031impl VmExecutor for RuntimeVmExecutor<'_> {
4032    fn assign(&mut self, vm: &mut Vm, name: &str, value: Option<&Word>) {
4033        let value = value.map_or_else(String::new, |word| {
4034            wasmsh_expand::expand_word(word, &mut vm.state)
4035        });
4036        if self.consume_nounset_error(vm) {
4037            return;
4038        }
4039        let trimmed = value.trim();
4040        if trimmed.starts_with('(') && trimmed.ends_with(')') {
4041            let inner = &trimmed[1..trimmed.len() - 1];
4042            let elements = WorkerRuntime::parse_array_elements(inner);
4043            let name_key = smol_str::SmolStr::from(name);
4044
4045            if WorkerRuntime::is_assoc_array_assignment(inner, &elements) {
4046                vm.state.init_assoc_array(name_key.clone());
4047                for (key, value) in WorkerRuntime::parse_assoc_pairs(inner) {
4048                    vm.state.set_array_element(
4049                        name_key.clone(),
4050                        &key,
4051                        smol_str::SmolStr::from(value.as_str()),
4052                    );
4053                }
4054            } else {
4055                vm.state.init_indexed_array(name_key.clone());
4056                for (idx, element) in elements.iter().enumerate() {
4057                    vm.state
4058                        .set_array_element(name_key.clone(), &idx.to_string(), element.clone());
4059                }
4060            }
4061            vm.state.last_status = 0;
4062            return;
4063        }
4064
4065        let assigned = if vm.state.env.get(name).is_some_and(|var| var.integer) {
4066            wasmsh_expand::eval_arithmetic(trimmed, &mut vm.state).to_string()
4067        } else {
4068            value
4069        };
4070        vm.state.set_var(name.into(), assigned.into());
4071        vm.state.last_status = 0;
4072    }
4073
4074    fn execute_builtin(
4075        &mut self,
4076        vm: &mut Vm,
4077        name: &str,
4078        argv: &[Word],
4079        redirections: &[IrRedirection],
4080    ) -> i32 {
4081        let Some(builtin_fn) = self.builtins.get(name) else {
4082            vm.emit_diagnostic(
4083                wasmsh_vm::DiagLevel::Error,
4084                wasmsh_vm::DiagCategory::Builtin,
4085                format!("unknown builtin: {name}"),
4086            );
4087            vm.state.last_status = 127;
4088            return 127;
4089        };
4090        let expanded: Vec<String> = argv
4091            .iter()
4092            .map(|word| wasmsh_expand::expand_word(word, &mut vm.state))
4093            .collect();
4094        if self.consume_nounset_error(vm) {
4095            return 1;
4096        }
4097        let argv_refs: Vec<&str> = expanded.iter().map(String::as_str).collect();
4098        let stdin = match self.take_builtin_stdin(name) {
4099            Ok(stdin) => stdin,
4100            Err(message) => {
4101                self.write_visible_stderr(vm, message.as_bytes());
4102                vm.state.last_status = 1;
4103                return 1;
4104            }
4105        };
4106        let exec_io = match self.prepare_exec_io(&mut vm.state, redirections) {
4107            Ok(exec_io) => exec_io,
4108            Err(message) => {
4109                self.write_visible_stderr(vm, message.as_bytes());
4110                vm.state.last_status = 1;
4111                return 1;
4112            }
4113        };
4114
4115        let fs = &*self.fs;
4116        let status = Self::with_exec_io_scope(
4117            &mut *self.current_exec_io,
4118            &mut *self.proc_subst_out_scopes,
4119            &mut *self.exec,
4120            exec_io,
4121            |current_exec_io, proc_subst_out_scopes, exec| {
4122                let mut router = RuntimeOutputRouter {
4123                    exec,
4124                    exec_io: current_exec_io.as_mut(),
4125                    proc_subst_out_scopes,
4126                    vm_stdout: &mut vm.stdout,
4127                    vm_stderr: &mut vm.stderr,
4128                    vm_output_bytes: &mut vm.output_bytes,
4129                    vm_output_limit: vm.limits.output_byte_limit,
4130                    vm_diagnostics: &mut vm.diagnostics,
4131                };
4132                let mut sink = RuntimeBuiltinSink {
4133                    router: &mut router,
4134                };
4135                {
4136                    let mut ctx = wasmsh_builtins::BuiltinContext {
4137                        state: &mut vm.state,
4138                        output: &mut sink,
4139                        fs: Some(fs),
4140                        stdin,
4141                    };
4142                    builtin_fn(&mut ctx, &argv_refs)
4143                }
4144            },
4145        );
4146        if let Some(last) = expanded.last() {
4147            vm.state.set_last_argument(last.as_str());
4148        }
4149        vm.state.last_status = status;
4150        status
4151    }
4152}
4153
4154/// The worker-side runtime that processes host commands.
4155#[allow(missing_debug_implementations)]
4156pub struct WorkerRuntime {
4157    config: BrowserConfig,
4158    vm: Vm,
4159    fs: BackendFs,
4160    utils: UtilRegistry,
4161    builtins: wasmsh_builtins::BuiltinRegistry,
4162    initialized: bool,
4163    /// Command-scoped stdin/stdout/stderr routing for the currently executing command.
4164    current_exec_io: Option<ExecIo>,
4165    /// Deferred `>(...)` sinks scoped to the currently executing command.
4166    proc_subst_out_scopes: Vec<Vec<PendingProcessSubstOut>>,
4167    /// Deferred `<(...)` cleanup and stderr flush scoped to the current command.
4168    proc_subst_in_scopes: Vec<Vec<PendingProcessSubstIn>>,
4169    /// Registered shell functions (name → HIR body).
4170    functions: IndexMap<String, HirCommand>,
4171    /// Transient execution state (loop control, exit, locals).
4172    exec: ExecState,
4173    /// Shell aliases (name → replacement text).
4174    aliases: IndexMap<String, String>,
4175    /// Optional handler for external commands (e.g. python3 in Pyodide).
4176    external_handler: Option<ExternalCommandHandler>,
4177    /// Optional network backend for curl/wget utilities.
4178    network: Option<Box<dyn wasmsh_utils::net_types::NetworkBackend>>,
4179    /// Active top-level execution, if a run has been started and not yet completed.
4180    active_run: Option<ActiveRun>,
4181    /// Signals queued for the next progressive poll.
4182    pending_signals: VecDeque<&'static RuntimeSignalSpec>,
4183}
4184
4185/// Action to take for a character during array element parsing.
4186enum ArrayCharAction {
4187    Append(char),
4188    Skip,
4189    SplitField,
4190}
4191
4192enum StreamingPipelineStage {
4193    Literal(Vec<u8>),
4194    File(String),
4195    Yes { line: Vec<u8> },
4196    BufferedCommand(BufferedPipelineCommand),
4197    Cat,
4198    Head(StreamingHeadMode),
4199    Tail(StreamingTailMode),
4200    Bat(StreamingBatStage),
4201    Sed(StreamingSedStage),
4202    Tee(StreamingTeeStage),
4203    Paste(StreamingPasteStage),
4204    Column(StreamingColumnStage),
4205    Grep(StreamingGrepStage),
4206    Uniq(StreamingUniqFlags),
4207    Rev,
4208    Cut(StreamingCutStage),
4209    Tr(StreamingTrStage),
4210    Wc(StreamingWcFlags),
4211}
4212
4213struct StreamingStageCtx<'a> {
4214    stages: &'a [StreamingPipelineStage],
4215    stage_pipe_stderr: &'a [bool],
4216    stage_statuses: &'a [Rc<RefCell<i32>>],
4217    stage_stderr: &'a [Rc<RefCell<Vec<u8>>>],
4218    output_pipes: &'a [Rc<RefCell<PipeBuffer>>],
4219}
4220
4221#[derive(Clone, Debug)]
4222enum StreamingCutMode {
4223    Fields(Vec<StreamingCutRange>),
4224    Chars(Vec<StreamingCutRange>),
4225    Bytes(Vec<StreamingCutRange>),
4226}
4227
4228#[derive(Clone, Debug)]
4229struct StreamingCutStage {
4230    mode: StreamingCutMode,
4231    delim: char,
4232    complement: bool,
4233    only_delimited: bool,
4234    output_delim: String,
4235}
4236
4237#[derive(Clone, Debug)]
4238struct StreamingCutRange {
4239    start: Option<usize>,
4240    end: Option<usize>,
4241}
4242
4243#[derive(Clone, Debug)]
4244struct StreamingTrStage {
4245    delete: bool,
4246    squeeze: bool,
4247    complement: bool,
4248    from_chars: Vec<char>,
4249    to_chars: Vec<char>,
4250}
4251
4252/// Quoting state for parsing array elements.
4253#[derive(Default)]
4254struct ArrayParseState {
4255    in_single_quote: bool,
4256    in_double_quote: bool,
4257    escape_next: bool,
4258}
4259
4260impl ArrayParseState {
4261    fn process_char(&mut self, ch: char) -> ArrayCharAction {
4262        if self.escape_next {
4263            self.escape_next = false;
4264            return ArrayCharAction::Append(ch);
4265        }
4266        if ch == '\\' && !self.in_single_quote {
4267            self.escape_next = true;
4268            return ArrayCharAction::Skip;
4269        }
4270        if ch == '\'' && !self.in_double_quote {
4271            self.in_single_quote = !self.in_single_quote;
4272            return ArrayCharAction::Skip;
4273        }
4274        if ch == '"' && !self.in_single_quote {
4275            self.in_double_quote = !self.in_double_quote;
4276            return ArrayCharAction::Skip;
4277        }
4278        if ch.is_ascii_whitespace() && !self.in_single_quote && !self.in_double_quote {
4279            return ArrayCharAction::SplitField;
4280        }
4281        ArrayCharAction::Append(ch)
4282    }
4283}
4284
4285/// Parsed flags for `declare`/`typeset`.
4286#[allow(clippy::struct_excessive_bools)]
4287struct DeclareFlags {
4288    is_assoc: bool,
4289    is_indexed: bool,
4290    is_integer: bool,
4291    is_export: bool,
4292    is_readonly: bool,
4293    is_lower: bool,
4294    is_upper: bool,
4295    is_print: bool,
4296    is_nameref: bool,
4297    is_functions: bool,
4298    is_function_names: bool,
4299    is_trace: bool,
4300}
4301
4302#[derive(Clone, Copy, Debug)]
4303enum CommandLookupKind {
4304    Alias,
4305    Function,
4306    Builtin,
4307    File,
4308}
4309
4310#[derive(Clone, Debug)]
4311struct CommandLookup {
4312    kind: CommandLookupKind,
4313    name: String,
4314    detail: String,
4315}
4316
4317fn format_command_verbose(lookup: &CommandLookup) -> String {
4318    match lookup.kind {
4319        CommandLookupKind::Alias => format!("alias {}='{}'", lookup.name, lookup.detail),
4320        CommandLookupKind::Function | CommandLookupKind::Builtin => lookup.name.clone(),
4321        CommandLookupKind::File => lookup.detail.clone(),
4322    }
4323}
4324
4325fn format_type_lookup(lookup: &CommandLookup, type_only: bool, path_only: bool) -> String {
4326    if type_only {
4327        return match lookup.kind {
4328            CommandLookupKind::Alias => "alias".to_string(),
4329            CommandLookupKind::Function => "function".to_string(),
4330            CommandLookupKind::Builtin => "builtin".to_string(),
4331            CommandLookupKind::File => "file".to_string(),
4332        };
4333    }
4334    if path_only {
4335        return lookup.detail.clone();
4336    }
4337    match lookup.kind {
4338        CommandLookupKind::Alias => {
4339            format!("{} is aliased to `{}`", lookup.name, lookup.detail)
4340        }
4341        CommandLookupKind::Function => format!("{} is a function", lookup.name),
4342        CommandLookupKind::Builtin => format!("{} is a shell builtin", lookup.name),
4343        CommandLookupKind::File => format!("{} is {}", lookup.name, lookup.detail),
4344    }
4345}
4346
4347#[derive(Clone, Debug)]
4348struct MapfileOptions {
4349    strip_delimiter: bool,
4350    delimiter: u8,
4351    count: Option<usize>,
4352    origin: usize,
4353    skip: usize,
4354    fd: u32,
4355    array_name: String,
4356}
4357
4358/// Parse declare/typeset flags from argv, returning (flags, `name_indices`).
4359fn parse_declare_flags(argv: &[String]) -> (DeclareFlags, Vec<usize>) {
4360    let mut flags = DeclareFlags {
4361        is_assoc: false,
4362        is_indexed: false,
4363        is_integer: false,
4364        is_export: false,
4365        is_readonly: false,
4366        is_lower: false,
4367        is_upper: false,
4368        is_print: false,
4369        is_nameref: false,
4370        is_functions: false,
4371        is_function_names: false,
4372        is_trace: false,
4373    };
4374    let mut names = Vec::new();
4375
4376    for (i, arg) in argv[1..].iter().enumerate() {
4377        if arg.starts_with('-') && arg.len() > 1 {
4378            for ch in arg[1..].chars() {
4379                match ch {
4380                    'A' => flags.is_assoc = true,
4381                    'a' => flags.is_indexed = true,
4382                    'i' => flags.is_integer = true,
4383                    'x' => flags.is_export = true,
4384                    'r' => flags.is_readonly = true,
4385                    'l' => flags.is_lower = true,
4386                    'u' => flags.is_upper = true,
4387                    'p' => flags.is_print = true,
4388                    'n' => flags.is_nameref = true,
4389                    'f' => flags.is_functions = true,
4390                    'F' => flags.is_function_names = true,
4391                    't' => flags.is_trace = true,
4392                    _ => {}
4393                }
4394            }
4395        } else {
4396            names.push(i + 1);
4397        }
4398    }
4399    (flags, names)
4400}
4401
4402impl WorkerRuntime {
4403    #[must_use]
4404    pub fn new() -> Self {
4405        Self {
4406            config: BrowserConfig::default(),
4407            vm: Vm::with_limits(ShellState::new(), ExecutionLimits::default()),
4408            fs: BackendFs::new(),
4409            utils: UtilRegistry::new(),
4410            builtins: wasmsh_builtins::BuiltinRegistry::new(),
4411            initialized: false,
4412            current_exec_io: None,
4413            proc_subst_out_scopes: Vec::new(),
4414            proc_subst_in_scopes: Vec::new(),
4415            functions: IndexMap::new(),
4416            exec: ExecState::new(),
4417            aliases: IndexMap::new(),
4418            external_handler: None,
4419            network: None,
4420            active_run: None,
4421            pending_signals: VecDeque::new(),
4422        }
4423    }
4424
4425    /// Register a handler for external commands (e.g. `python3` in Pyodide).
4426    pub fn set_external_handler(&mut self, handler: ExternalCommandHandler) {
4427        self.external_handler = Some(handler);
4428    }
4429
4430    /// Register a network backend for `curl`/`wget` utilities.
4431    pub fn set_network_backend(
4432        &mut self,
4433        backend: Box<dyn wasmsh_utils::net_types::NetworkBackend>,
4434    ) {
4435        self.network = Some(backend);
4436    }
4437
4438    /// Process a host command and return a list of events to send back.
4439    pub fn handle_command(&mut self, cmd: HostCommand) -> Vec<WorkerEvent> {
4440        match cmd {
4441            HostCommand::Init {
4442                step_budget,
4443                allowed_hosts,
4444            } => self.handle_init_command(step_budget, allowed_hosts),
4445            HostCommand::Run { input } => self.handle_run_command(input, true),
4446            HostCommand::StartRun { input } => self.handle_run_command(input, false),
4447            HostCommand::PollRun => self.handle_poll_run_command(),
4448            HostCommand::Signal { signal } => self.handle_signal_command(&signal),
4449            HostCommand::Cancel => {
4450                self.cancel_active_execution();
4451                vec![WorkerEvent::Diagnostic(
4452                    DiagnosticLevel::Info,
4453                    "cancel received".into(),
4454                )]
4455            }
4456            HostCommand::ReadFile { path } => self.handle_read_file_command(&path),
4457            HostCommand::WriteFile { path, data } => self.handle_write_file_command(path, &data),
4458            HostCommand::ListDir { path } => self.handle_list_dir_command(&path),
4459            HostCommand::Mount { .. } => {
4460                vec![WorkerEvent::Diagnostic(
4461                    DiagnosticLevel::Warning,
4462                    "mount not yet implemented".into(),
4463                )]
4464            }
4465            _ => vec![WorkerEvent::Diagnostic(
4466                DiagnosticLevel::Warning,
4467                "unknown command".into(),
4468            )],
4469        }
4470    }
4471
4472    fn handle_init_command(
4473        &mut self,
4474        step_budget: u64,
4475        allowed_hosts: Vec<String>,
4476    ) -> Vec<WorkerEvent> {
4477        self.config.step_budget = step_budget;
4478        self.config.allowed_hosts = allowed_hosts;
4479        self.vm = Vm::with_limits(
4480            ShellState::new(),
4481            ExecutionLimits {
4482                step_limit: step_budget,
4483                output_byte_limit: self.config.output_byte_limit,
4484                pipe_byte_limit: self.config.pipe_byte_limit,
4485                recursion_limit: self.config.recursion_limit,
4486            },
4487        );
4488        self.fs = BackendFs::new();
4489        self.current_exec_io = None;
4490        self.proc_subst_out_scopes.clear();
4491        self.proc_subst_in_scopes.clear();
4492        self.functions = IndexMap::new();
4493        self.exec.reset();
4494        self.aliases = IndexMap::new();
4495        self.active_run = None;
4496        self.pending_signals.clear();
4497        self.initialized = true;
4498        // Set default shopt options (bash defaults)
4499        self.vm.state.set_var("SHOPT_extglob".into(), "1".into());
4500        self.vm
4501            .state
4502            .set_var("SHOPT_expand_aliases".into(), "1".into());
4503        self.vm.state.set_var("SHOPT_sourcepath".into(), "1".into());
4504        vec![WorkerEvent::Version(PROTOCOL_VERSION.to_string())]
4505    }
4506
4507    fn handle_run_command(&mut self, input: String, run_to_completion: bool) -> Vec<WorkerEvent> {
4508        if !self.initialized {
4509            return vec![WorkerEvent::Diagnostic(
4510                DiagnosticLevel::Error,
4511                "runtime not initialized".into(),
4512            )];
4513        }
4514        match self.start_execution(input) {
4515            Ok(()) => {
4516                if run_to_completion {
4517                    self.poll_active_run_to_completion()
4518                } else {
4519                    vec![WorkerEvent::Yielded]
4520                }
4521            }
4522            Err(events) => events,
4523        }
4524    }
4525
4526    fn handle_poll_run_command(&mut self) -> Vec<WorkerEvent> {
4527        match self.poll_active_run() {
4528            Some(ExecutionPoll::Yield(mut events)) => {
4529                events.push(WorkerEvent::Yielded);
4530                events
4531            }
4532            Some(ExecutionPoll::Done(events)) => events,
4533            None => vec![WorkerEvent::Diagnostic(
4534                DiagnosticLevel::Error,
4535                "no active run".into(),
4536            )],
4537        }
4538    }
4539
4540    fn handle_read_file_command(&mut self, path: &str) -> Vec<WorkerEvent> {
4541        use wasmsh_fs::OpenOptions;
4542        let handle = match self.fs.open(path, OpenOptions::read()) {
4543            Ok(h) => h,
4544            Err(e) => {
4545                return vec![WorkerEvent::Diagnostic(
4546                    DiagnosticLevel::Error,
4547                    format!("read error: {e}"),
4548                )];
4549            }
4550        };
4551        let result = self.fs.read_file(handle);
4552        self.fs.close(handle);
4553        match result {
4554            Ok(data) => vec![WorkerEvent::Stdout(data)],
4555            Err(e) => vec![WorkerEvent::Diagnostic(
4556                DiagnosticLevel::Error,
4557                format!("read error: {path}: {e}"),
4558            )],
4559        }
4560    }
4561
4562    fn handle_write_file_command(&mut self, path: String, data: &[u8]) -> Vec<WorkerEvent> {
4563        use wasmsh_fs::OpenOptions;
4564        match self.fs.open(&path, OpenOptions::write()) {
4565            Ok(h) => {
4566                if let Err(e) = self.fs.write_file(h, data) {
4567                    self.write_stderr(format!("wasmsh: write error: {e}\n").as_bytes());
4568                }
4569                self.fs.close(h);
4570                vec![WorkerEvent::FsChanged(path)]
4571            }
4572            Err(e) => vec![WorkerEvent::Diagnostic(
4573                DiagnosticLevel::Error,
4574                format!("write error: {e}"),
4575            )],
4576        }
4577    }
4578
4579    fn handle_list_dir_command(&mut self, path: &str) -> Vec<WorkerEvent> {
4580        match self.fs.read_dir(path) {
4581            Ok(entries) => {
4582                let names: Vec<u8> = entries
4583                    .iter()
4584                    .map(|e| e.name.as_str())
4585                    .collect::<Vec<_>>()
4586                    .join("\n")
4587                    .into_bytes();
4588                vec![WorkerEvent::Stdout(names)]
4589            }
4590            Err(e) => vec![WorkerEvent::Diagnostic(
4591                DiagnosticLevel::Error,
4592                format!("readdir error: {e}"),
4593            )],
4594        }
4595    }
4596
4597    pub fn start_execution(&mut self, input: String) -> Result<(), Vec<WorkerEvent>> {
4598        if !self.initialized {
4599            return Err(vec![WorkerEvent::Diagnostic(
4600                DiagnosticLevel::Error,
4601                "runtime not initialized".into(),
4602            )]);
4603        }
4604        if self.active_run.is_some() {
4605            return Err(vec![WorkerEvent::Diagnostic(
4606                DiagnosticLevel::Error,
4607                "execution already active".into(),
4608            )]);
4609        }
4610
4611        let hir = match wasmsh_parse::parse(&input) {
4612            Ok(ast) => wasmsh_hir::lower(&ast),
4613            Err(e) => {
4614                self.vm.state.last_status = 2;
4615                return Err(vec![
4616                    WorkerEvent::Stderr(format!("wasmsh: parse error: {e}\n").into_bytes()),
4617                    WorkerEvent::Exit(2),
4618                ]);
4619            }
4620        };
4621
4622        self.exec.reset();
4623        self.current_exec_io = None;
4624        self.proc_subst_out_scopes.clear();
4625        self.proc_subst_in_scopes.clear();
4626        self.vm.steps = 0;
4627        self.vm.budget.steps = 0;
4628        self.vm.budget.visible_output_bytes = self.vm.output_bytes;
4629        self.vm.budget.pipe_bytes = 0;
4630        self.vm.budget.recursion_depth = 0;
4631        self.vm.budget.clear_stop_reason();
4632        self.vm.cancellation_token().reset();
4633        self.pending_signals.clear();
4634        self.active_run = Some(ActiveRun::new(input, hir));
4635        Ok(())
4636    }
4637
4638    /// Minimum per-poll step limit so that small batch sizes (e.g. `step_budget=1`
4639    /// for progressive yield-per-command) still allow enough internal steps for
4640    /// pipelines and compound commands to complete.
4641    const MIN_POLL_STEPS: u64 = 100;
4642
4643    pub fn poll_active_run(&mut self) -> Option<ExecutionPoll> {
4644        let mut run = self.active_run.take()?;
4645        let previous_step_limit = self.vm.limits.step_limit;
4646        self.vm.steps = 0;
4647        self.vm.budget.steps = 0;
4648        // Keep the VM step_limit active so that loops (while/for) can enforce
4649        // the budget via `check_resource_limits()` on each iteration.  The
4650        // outer `remaining` counter governs how many top-level commands we
4651        // execute per poll; the VM limit catches runaway inner loops.
4652        self.vm.limits.step_limit = if self.config.step_budget == 0 {
4653            0
4654        } else {
4655            self.config.step_budget.max(Self::MIN_POLL_STEPS)
4656        };
4657
4658        let mut remaining = if self.config.step_budget == 0 {
4659            usize::MAX
4660        } else {
4661            self.config.step_budget as usize
4662        };
4663        let pending_signal_events = self.drain_pending_signal_events();
4664        let mut finished = run.is_done();
4665
4666        while !finished && remaining > 0 {
4667            // Check cancellation without advancing the step counter — the
4668            // step counter is advanced inside command/loop dispatch.
4669            if self.vm.cancellation_token().is_cancelled() {
4670                self.vm.budget.note_cancelled();
4671                self.exec.resource_exhausted = true;
4672            }
4673            if self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
4674                finished = true;
4675                break;
4676            }
4677
4678            let step_outcome = self.poll_active_run_step(&mut run);
4679            remaining -= 1;
4680            finished = matches!(step_outcome, ActiveRunStep::Done);
4681        }
4682
4683        self.vm.limits.step_limit = previous_step_limit;
4684
4685        if finished || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
4686            self.ensure_stop_reason();
4687            let mut events = pending_signal_events;
4688            self.run_exit_trap_if_needed(&mut events);
4689            self.drain_io_events(&mut events);
4690            self.drain_diagnostic_events(&mut events);
4691            let exit_status = self.current_run_exit_status();
4692            events.push(WorkerEvent::Exit(exit_status));
4693            self.active_run = None;
4694            Some(ExecutionPoll::Done(events))
4695        } else {
4696            let mut events = pending_signal_events;
4697            events.extend(self.drain_partial_run_events());
4698            self.active_run = Some(run);
4699            Some(ExecutionPoll::Yield(events))
4700        }
4701    }
4702
4703    pub fn cancel_active_execution(&mut self) {
4704        self.vm.cancellation_token().cancel();
4705    }
4706
4707    fn handle_signal_command(&mut self, signal: &str) -> Vec<WorkerEvent> {
4708        if !self.initialized {
4709            return vec![WorkerEvent::Diagnostic(
4710                DiagnosticLevel::Error,
4711                "runtime not initialized".into(),
4712            )];
4713        }
4714
4715        let Some(spec) = find_runtime_signal_spec(signal) else {
4716            return vec![WorkerEvent::Diagnostic(
4717                DiagnosticLevel::Error,
4718                format!("unsupported signal: {signal}"),
4719            )];
4720        };
4721
4722        if self.active_run.is_some() {
4723            self.pending_signals.push_back(spec);
4724            if self.signal_trap_handler(spec).is_some()
4725                || self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1")
4726            {
4727                return Vec::new();
4728            }
4729            return match spec.default_action {
4730                SignalDefaultAction::Terminate => vec![WorkerEvent::Diagnostic(
4731                    DiagnosticLevel::Info,
4732                    format!("signal {} received", spec.name),
4733                )],
4734                SignalDefaultAction::Ignore => Vec::new(),
4735                SignalDefaultAction::StopLike => vec![WorkerEvent::Diagnostic(
4736                    DiagnosticLevel::Warning,
4737                    format!(
4738                        "signal {} requires job-control stop semantics and is not modeled yet",
4739                        spec.name
4740                    ),
4741                )],
4742                SignalDefaultAction::ContinueLike => vec![WorkerEvent::Diagnostic(
4743                    DiagnosticLevel::Info,
4744                    format!(
4745                        "signal {} has no effect without a stopped job in the current sandbox model",
4746                        spec.name
4747                    ),
4748                )],
4749            };
4750        }
4751
4752        if let Some(handler) = self.signal_trap_handler(spec) {
4753            let mut events = self.run_signal_trap(spec, &handler);
4754            self.drain_diagnostic_events(&mut events);
4755            if self.exec.exit_requested.is_some() {
4756                events.extend(self.finish_idle_signal_exit());
4757            }
4758            return events;
4759        }
4760
4761        if self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1") {
4762            return Vec::new();
4763        }
4764
4765        match spec.default_action {
4766            SignalDefaultAction::Terminate => {
4767                self.exec.exit_requested = Some(128 + spec.number);
4768                if self.active_run.is_some() {
4769                    vec![WorkerEvent::Diagnostic(
4770                        DiagnosticLevel::Info,
4771                        format!("signal {} received", spec.name),
4772                    )]
4773                } else {
4774                    self.finish_idle_signal_exit()
4775                }
4776            }
4777            SignalDefaultAction::Ignore => Vec::new(),
4778            SignalDefaultAction::StopLike => vec![WorkerEvent::Diagnostic(
4779                DiagnosticLevel::Warning,
4780                format!(
4781                    "signal {} requires job-control stop semantics and is not modeled yet",
4782                    spec.name
4783                ),
4784            )],
4785            SignalDefaultAction::ContinueLike => vec![WorkerEvent::Diagnostic(
4786                DiagnosticLevel::Info,
4787                format!(
4788                    "signal {} has no effect without a stopped job in the current sandbox model",
4789                    spec.name
4790                ),
4791            )],
4792        }
4793    }
4794
4795    fn drain_pending_signal_events(&mut self) -> Vec<WorkerEvent> {
4796        let mut events = Vec::new();
4797        while let Some(spec) = self.pending_signals.pop_front() {
4798            if let Some(handler) = self.signal_trap_handler(spec) {
4799                events.extend(self.run_signal_trap(spec, &handler));
4800                self.drain_diagnostic_events(&mut events);
4801            } else if self.vm.state.get_var(spec.ignore_var).as_deref() == Some("1") {
4802                continue;
4803            } else {
4804                match spec.default_action {
4805                    SignalDefaultAction::Terminate => {
4806                        self.exec.exit_requested = Some(128 + spec.number);
4807                    }
4808                    SignalDefaultAction::Ignore => {}
4809                    SignalDefaultAction::StopLike => events.push(WorkerEvent::Diagnostic(
4810                        DiagnosticLevel::Warning,
4811                        format!(
4812                            "signal {} requires job-control stop semantics and is not modeled yet",
4813                            spec.name
4814                        ),
4815                    )),
4816                    SignalDefaultAction::ContinueLike => events.push(WorkerEvent::Diagnostic(
4817                        DiagnosticLevel::Info,
4818                        format!(
4819                            "signal {} has no effect without a stopped job in the current sandbox model",
4820                            spec.name
4821                        ),
4822                    )),
4823                }
4824            }
4825
4826            if self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
4827                break;
4828            }
4829        }
4830        events
4831    }
4832
4833    fn finish_idle_signal_exit(&mut self) -> Vec<WorkerEvent> {
4834        let mut events = Vec::new();
4835        self.run_exit_trap_if_needed(&mut events);
4836        self.drain_io_events(&mut events);
4837        self.drain_diagnostic_events(&mut events);
4838        let exit_status = self.current_run_exit_status();
4839        events.push(WorkerEvent::Exit(exit_status));
4840        self.exec.reset();
4841        events
4842    }
4843
4844    fn poll_active_run_to_completion(&mut self) -> Vec<WorkerEvent> {
4845        let mut events = Vec::new();
4846        while let Some(poll) = self.poll_active_run() {
4847            match poll {
4848                ExecutionPoll::Yield(mut batch) => {
4849                    events.append(&mut batch);
4850                }
4851                ExecutionPoll::Done(mut batch) => {
4852                    events.append(&mut batch);
4853                    break;
4854                }
4855            }
4856        }
4857        events
4858    }
4859
4860    fn poll_active_run_step(&mut self, run: &mut ActiveRun) -> ActiveRunStep {
4861        if run.is_done() || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
4862            return ActiveRunStep::Done;
4863        }
4864
4865        let cc = &run.hir.items[run.complete_index];
4866        if run.and_or_index == 0 {
4867            self.vm.state.lineno = Self::line_number_for_offset(&run.input, cc.span.start as usize);
4868            self.maybe_write_verbose_input(&run.input, cc);
4869        }
4870        if self.is_set_option_enabled('n') {
4871            run.complete_index += 1;
4872            run.and_or_index = 0;
4873            return if run.is_done()
4874                || self.exec.exit_requested.is_some()
4875                || self.exec.resource_exhausted
4876            {
4877                ActiveRunStep::Done
4878            } else {
4879                ActiveRunStep::Pending
4880            };
4881        }
4882        let and_or = &cc.list[run.and_or_index];
4883        self.execute_and_or(and_or);
4884        self.handle_post_and_or(and_or);
4885
4886        run.and_or_index += 1;
4887        if run.and_or_index >= cc.list.len() {
4888            run.complete_index += 1;
4889            run.and_or_index = 0;
4890        }
4891
4892        if run.is_done() || self.exec.exit_requested.is_some() || self.exec.resource_exhausted {
4893            ActiveRunStep::Done
4894        } else {
4895            ActiveRunStep::Pending
4896        }
4897    }
4898
4899    fn drain_partial_run_events(&mut self) -> Vec<WorkerEvent> {
4900        let mut events = Vec::new();
4901        self.drain_io_events(&mut events);
4902        self.drain_diagnostic_events(&mut events);
4903        events
4904    }
4905
4906    fn current_run_exit_status(&self) -> i32 {
4907        if self.exec.resource_exhausted {
4908            match self.exec.stop_reason.as_ref() {
4909                Some(StopReason::Cancelled) => 130,
4910                _ => 128,
4911            }
4912        } else {
4913            self.exec
4914                .exit_requested
4915                .unwrap_or(self.vm.state.last_status)
4916        }
4917    }
4918
4919    fn mark_stop_reason(&mut self, reason: StopReason) {
4920        self.exec.resource_exhausted = true;
4921        self.exec.stop_reason = Some(reason);
4922    }
4923
4924    fn mark_budget_exhaustion(&mut self, reason: ExhaustionReason) {
4925        self.mark_stop_reason(StopReason::Exhausted(reason));
4926    }
4927
4928    fn ensure_stop_reason(&mut self) {
4929        if !self.exec.resource_exhausted || self.exec.stop_reason.is_some() {
4930            return;
4931        }
4932        if self.vm.cancellation_token().is_cancelled() {
4933            self.mark_stop_reason(StopReason::Cancelled);
4934            return;
4935        }
4936        if let Some(reason) = self.vm.stop_reason().cloned() {
4937            self.mark_stop_reason(reason);
4938            return;
4939        }
4940        let limit = self.vm.limits.output_byte_limit;
4941        if limit > 0 && self.vm.output_bytes > limit {
4942            self.mark_budget_exhaustion(ExhaustionReason {
4943                category: BudgetCategory::VisibleOutputBytes,
4944                used: self.vm.output_bytes,
4945                limit,
4946            });
4947        }
4948    }
4949
4950    fn sync_pipe_budget(&mut self, used: u64) {
4951        if self.exec.resource_exhausted {
4952            return;
4953        }
4954        let limit = self.vm.limits.pipe_byte_limit;
4955        if let Err(reason) = self.vm.budget.set_pipe_bytes(used, limit) {
4956            self.mark_budget_exhaustion(reason.clone());
4957            self.vm.emit_diagnostic(
4958                wasmsh_vm::DiagLevel::Error,
4959                wasmsh_vm::DiagCategory::Budget,
4960                reason.diagnostic_message(),
4961            );
4962        }
4963    }
4964
4965    pub fn set_output_byte_limit(&mut self, limit: u64) {
4966        self.config.output_byte_limit = limit;
4967        self.vm.limits.output_byte_limit = limit;
4968    }
4969
4970    pub fn set_pipe_byte_limit(&mut self, limit: u64) {
4971        self.config.pipe_byte_limit = limit;
4972        self.vm.limits.pipe_byte_limit = limit;
4973    }
4974
4975    pub fn set_recursion_limit(&mut self, limit: u32) {
4976        self.config.recursion_limit = limit;
4977        self.vm.limits.recursion_limit = limit;
4978    }
4979
4980    pub fn set_vm_subset_enabled(&mut self, enabled: bool) {
4981        self.config.vm_subset_enabled = enabled;
4982    }
4983
4984    fn execute_and_or(&mut self, and_or: &HirAndOr) {
4985        if let Ok(program) = self.lower_vm_subset_and_or(and_or) {
4986            self.run_debug_trap_if_needed();
4987            self.execute_ir_program(&program);
4988            return;
4989        }
4990        self.execute_pipeline_chain(and_or);
4991    }
4992
4993    fn execute_ir_program(&mut self, program: &IrProgram) {
4994        let mut executor = RuntimeVmExecutor {
4995            fs: &mut self.fs,
4996            builtins: &self.builtins,
4997            current_exec_io: &mut self.current_exec_io,
4998            proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
4999            exec: &mut self.exec,
5000        };
5001        let _ = self.vm.run_with_executor(program, &mut executor);
5002    }
5003
5004    fn lower_vm_subset_and_or(
5005        &self,
5006        and_or: &HirAndOr,
5007    ) -> Result<IrProgram, VmSubsetFallbackReason> {
5008        if !self.config.vm_subset_enabled {
5009            return Err(VmSubsetFallbackReason::Disabled);
5010        }
5011
5012        self.validate_vm_subset_and_or(and_or)?;
5013        lower_supported_and_or(and_or).map_err(VmSubsetFallbackReason::Lowering)
5014    }
5015
5016    fn validate_vm_subset_and_or(&self, and_or: &HirAndOr) -> Result<(), VmSubsetFallbackReason> {
5017        self.validate_vm_subset_pipeline(&and_or.first)?;
5018        for (_, pipeline) in &and_or.rest {
5019            self.validate_vm_subset_pipeline(pipeline)?;
5020        }
5021        Ok(())
5022    }
5023
5024    fn validate_vm_subset_pipeline(
5025        &self,
5026        pipeline: &HirPipeline,
5027    ) -> Result<(), VmSubsetFallbackReason> {
5028        if pipeline.timed || pipeline.time_posix || pipeline.negated || pipeline.commands.len() != 1
5029        {
5030            return Err(VmSubsetFallbackReason::Lowering(
5031                LoweringError::Unsupported("pipeline shape is outside the VM subset"),
5032            ));
5033        }
5034        self.validate_vm_subset_command(&pipeline.commands[0])
5035    }
5036
5037    fn validate_vm_subset_command(&self, cmd: &HirCommand) -> Result<(), VmSubsetFallbackReason> {
5038        match cmd {
5039            HirCommand::Assign(node) => Self::validate_vm_subset_assign(node),
5040            HirCommand::Exec(node) => self.validate_vm_subset_exec(node),
5041            _ => Err(VmSubsetFallbackReason::Lowering(
5042                LoweringError::Unsupported("command kind is outside the VM subset"),
5043            )),
5044        }
5045    }
5046
5047    fn validate_vm_subset_assign(
5048        node: &wasmsh_hir::HirAssign,
5049    ) -> Result<(), VmSubsetFallbackReason> {
5050        if !node.redirections.is_empty()
5051            || node
5052                .assignments
5053                .iter()
5054                .any(|a| !Self::vm_supported_assignment_name(&a.name))
5055            || node
5056                .assignments
5057                .iter()
5058                .filter_map(|a| a.value.as_ref())
5059                .any(|word| !Self::vm_supported_word(word))
5060        {
5061            return Err(VmSubsetFallbackReason::AssignmentShape);
5062        }
5063        Ok(())
5064    }
5065
5066    fn validate_vm_subset_exec(
5067        &self,
5068        node: &wasmsh_hir::HirExec,
5069    ) -> Result<(), VmSubsetFallbackReason> {
5070        if !node.env.is_empty() {
5071            return Err(VmSubsetFallbackReason::CommandEnvPrefixes);
5072        }
5073        if node.argv.is_empty() || node.argv.iter().any(|word| !Self::vm_supported_word(word)) {
5074            return Err(VmSubsetFallbackReason::UnsupportedWord);
5075        }
5076        if node
5077            .redirections
5078            .iter()
5079            .any(|redir| !Self::vm_supported_redirection(redir))
5080        {
5081            return Err(VmSubsetFallbackReason::UnsupportedRedirection);
5082        }
5083        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1")
5084            || node
5085                .argv
5086                .iter()
5087                .any(Self::vm_word_requires_full_shell_execution)
5088        {
5089            return Err(VmSubsetFallbackReason::ShellExpansion);
5090        }
5091        let Some(name) = Self::literal_word_text(&node.argv[0]) else {
5092            return Err(VmSubsetFallbackReason::UnsupportedWord);
5093        };
5094        if self.get_shopt_value("expand_aliases") && self.aliases.contains_key(name.as_str()) {
5095            return Err(VmSubsetFallbackReason::AliasExpansion);
5096        }
5097        let argv = vec![name.to_string()];
5098        if !matches!(
5099            self.resolve_command(name.as_str(), &argv),
5100            ResolvedCommand::Builtin
5101        ) {
5102            return Err(VmSubsetFallbackReason::NonBuiltinCommand);
5103        }
5104        Ok(())
5105    }
5106
5107    fn vm_supported_assignment_name(name: &smol_str::SmolStr) -> bool {
5108        !name.as_str().contains('[') && !name.as_str().ends_with('+')
5109    }
5110
5111    fn vm_supported_redirection(redirection: &HirRedirection) -> bool {
5112        matches!(
5113            redirection.op,
5114            RedirectionOp::Output | RedirectionOp::Append
5115        ) && redirection.fd.unwrap_or(1) == 1
5116            && redirection.here_doc_body.is_none()
5117            && Self::vm_supported_word(&redirection.target)
5118    }
5119
5120    fn vm_supported_word(word: &Word) -> bool {
5121        word.parts.iter().all(Self::vm_supported_word_part)
5122    }
5123
5124    fn vm_word_requires_full_shell_execution(word: &Word) -> bool {
5125        word.parts
5126            .iter()
5127            .any(Self::vm_word_part_requires_full_shell_execution)
5128    }
5129
5130    fn vm_word_part_requires_full_shell_execution(part: &WordPart) -> bool {
5131        match part {
5132            WordPart::Literal(text) => Self::text_has_brace_or_glob_literal(text),
5133            WordPart::SingleQuoted(_)
5134            | WordPart::DoubleQuoted(_)
5135            | WordPart::Parameter(_)
5136            | WordPart::Arithmetic(_) => false,
5137            WordPart::CommandSubstitution(_)
5138            | WordPart::ProcessSubstIn(_)
5139            | WordPart::ProcessSubstOut(_)
5140            | _ => true,
5141        }
5142    }
5143
5144    fn vm_supported_word_part(part: &WordPart) -> bool {
5145        match part {
5146            WordPart::Literal(_)
5147            | WordPart::SingleQuoted(_)
5148            | WordPart::Parameter(_)
5149            | WordPart::Arithmetic(_) => true,
5150            WordPart::DoubleQuoted(parts) => parts.iter().all(Self::vm_supported_word_part),
5151            WordPart::CommandSubstitution(_)
5152            | WordPart::ProcessSubstIn(_)
5153            | WordPart::ProcessSubstOut(_)
5154            | _ => false,
5155        }
5156    }
5157
5158    fn literal_word_text(word: &Word) -> Option<smol_str::SmolStr> {
5159        fn append_literal(part: &WordPart, out: &mut String) -> Option<()> {
5160            match part {
5161                WordPart::Literal(text) | WordPart::SingleQuoted(text) => {
5162                    out.push_str(text);
5163                    Some(())
5164                }
5165                WordPart::DoubleQuoted(parts) => {
5166                    for part in parts {
5167                        append_literal(part, out)?;
5168                    }
5169                    Some(())
5170                }
5171                _ => None,
5172            }
5173        }
5174
5175        let mut text = String::new();
5176        for part in &word.parts {
5177            append_literal(part, &mut text)?;
5178        }
5179        Some(text.into())
5180    }
5181
5182    fn line_number_for_offset(input: &str, offset: usize) -> u32 {
5183        input
5184            .as_bytes()
5185            .iter()
5186            .take(offset)
5187            .filter(|&&b| b == b'\n')
5188            .count() as u32
5189            + 1
5190    }
5191
5192    /// Execute input and return collected events (used by eval/source).
5193    fn execute_input_inner(&mut self, input: &str) -> Vec<WorkerEvent> {
5194        self.exec.recursion_depth += 1;
5195        if let Err(reason) = self
5196            .vm
5197            .budget
5198            .enter_recursion(self.vm.limits.recursion_limit)
5199        {
5200            self.exec.recursion_depth -= 1;
5201            self.mark_budget_exhaustion(reason);
5202            return vec![WorkerEvent::Stderr(
5203                b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
5204            )];
5205        }
5206        let result = self.execute_input_inner_impl(input);
5207        self.exec.recursion_depth -= 1;
5208        self.vm.budget.exit_recursion();
5209        result
5210    }
5211
5212    /// Inner implementation of `execute_input_inner` (after recursion check).
5213    fn execute_input_inner_impl(&mut self, input: &str) -> Vec<WorkerEvent> {
5214        let ast = match wasmsh_parse::parse(input) {
5215            Ok(ast) => ast,
5216            Err(e) => {
5217                self.vm.state.last_status = 2;
5218                return vec![WorkerEvent::Stderr(
5219                    format!("wasmsh: parse error: {e}\n").into_bytes(),
5220                )];
5221            }
5222        };
5223        let hir = wasmsh_hir::lower(&ast);
5224        for cc in &hir.items {
5225            if self.exec.exit_requested.is_some() {
5226                break;
5227            }
5228            // Update $LINENO from span position
5229            let line = input
5230                .as_bytes()
5231                .iter()
5232                .take(cc.span.start as usize)
5233                .filter(|&&b| b == b'\n')
5234                .count() as u32
5235                + 1;
5236            self.vm.state.lineno = line;
5237            self.maybe_write_verbose_input(input, cc);
5238            if self.is_set_option_enabled('n') {
5239                continue;
5240            }
5241            self.execute_complete_command(cc);
5242        }
5243        // Drain stdout/stderr into events
5244        let mut events = Vec::new();
5245        if !self.vm.stdout.is_empty() {
5246            events.push(WorkerEvent::Stdout(std::mem::take(&mut self.vm.stdout)));
5247        }
5248        if !self.vm.stderr.is_empty() {
5249            events.push(WorkerEvent::Stderr(std::mem::take(&mut self.vm.stderr)));
5250        }
5251        events
5252    }
5253
5254    fn run_exit_trap_if_needed(&mut self, events: &mut Vec<WorkerEvent>) {
5255        let Some(exit_code) = self.exec.exit_requested else {
5256            return;
5257        };
5258        let Some(handler_str) = self.trap_handler("_TRAP_EXIT", "_TRAP_IGNORE_EXIT") else {
5259            return;
5260        };
5261        if self.exec.trap_depth > 0 {
5262            return;
5263        }
5264        self.exec.trap_depth += 1;
5265        self.exec.exit_requested = None;
5266        self.vm.state.last_status = exit_code;
5267        events.extend(self.execute_input_inner(&handler_str));
5268        self.exec.trap_depth -= 1;
5269        if self.exec.exit_requested.is_none() {
5270            self.exec.exit_requested = Some(exit_code);
5271        }
5272        self.vm.state.last_status = self.exec.exit_requested.unwrap_or(exit_code);
5273    }
5274
5275    fn handle_post_and_or(&mut self, and_or: &HirAndOr) {
5276        self.run_err_trap_if_needed(and_or);
5277        if self.should_errexit(and_or) {
5278            self.exec.exit_requested = Some(self.vm.state.last_status);
5279        }
5280    }
5281
5282    fn should_run_err_trap(&self, and_or: &HirAndOr) -> bool {
5283        !self.exec.errexit_suppressed
5284            && (self.exec.nested_shell_depth == 0 || self.is_set_option_enabled('E'))
5285            && and_or.rest.is_empty()
5286            && !and_or.first.negated
5287            && self.vm.state.last_status != 0
5288            && self.exec.exit_requested.is_none()
5289            && self.exec.trap_depth == 0
5290    }
5291
5292    fn run_err_trap_if_needed(&mut self, and_or: &HirAndOr) {
5293        if !self.should_run_err_trap(and_or) {
5294            return;
5295        }
5296        self.run_trap_and_merge(
5297            "_TRAP_ERR",
5298            "_TRAP_IGNORE_ERR",
5299            self.vm.state.last_status,
5300            true,
5301        );
5302    }
5303
5304    fn run_debug_trap_if_needed(&mut self) {
5305        if self.exec.trap_depth > 0
5306            || self.exec.resource_exhausted
5307            || (self.exec.nested_shell_depth > 0 && !self.is_set_option_enabled('T'))
5308        {
5309            return;
5310        }
5311        self.run_trap_and_merge(
5312            "_TRAP_DEBUG",
5313            "_TRAP_IGNORE_DEBUG",
5314            self.vm.state.last_status,
5315            true,
5316        );
5317    }
5318
5319    fn run_return_trap_if_needed(&mut self) {
5320        if self.exec.trap_depth > 0
5321            || self.exec.resource_exhausted
5322            || (self.exec.nested_shell_depth > 0 && !self.is_set_option_enabled('T'))
5323        {
5324            return;
5325        }
5326        self.run_trap_and_merge(
5327            "_TRAP_RETURN",
5328            "_TRAP_IGNORE_RETURN",
5329            self.vm.state.last_status,
5330            true,
5331        );
5332    }
5333
5334    fn run_trap_and_merge(
5335        &mut self,
5336        handler_var: &str,
5337        ignore_var: &str,
5338        trigger_status: i32,
5339        restore_status: bool,
5340    ) {
5341        let Some(handler) = self.trap_handler(handler_var, ignore_var) else {
5342            return;
5343        };
5344        let saved_status = self.vm.state.last_status;
5345        let saved_exit_requested = self.exec.exit_requested;
5346        self.exec.trap_depth += 1;
5347        self.vm.state.last_status = trigger_status;
5348        let events = self.execute_input_inner(&handler);
5349        self.exec.trap_depth -= 1;
5350        self.merge_sub_events_with_diagnostics(events);
5351        if restore_status
5352            && !self.exec.resource_exhausted
5353            && self.exec.exit_requested == saved_exit_requested
5354        {
5355            self.vm.state.last_status = saved_status;
5356        }
5357    }
5358
5359    fn trap_handler(&self, handler_var: &str, ignore_var: &str) -> Option<String> {
5360        if self.exec.trap_depth > 0 || self.vm.state.get_var(ignore_var).as_deref() == Some("1") {
5361            return None;
5362        }
5363        let handler = self.vm.state.get_var(handler_var)?;
5364        if handler.is_empty() {
5365            return None;
5366        }
5367        Some(handler.to_string())
5368    }
5369
5370    fn signal_trap_handler(&self, spec: &RuntimeSignalSpec) -> Option<String> {
5371        if !spec.trappable {
5372            return None;
5373        }
5374        self.trap_handler(spec.handler_var, spec.ignore_var)
5375    }
5376
5377    fn run_signal_trap(&mut self, spec: &RuntimeSignalSpec, handler: &str) -> Vec<WorkerEvent> {
5378        let saved_status = self.vm.state.last_status;
5379        let saved_exit_requested = self.exec.exit_requested;
5380        let saved_exec_io = self.current_exec_io.take();
5381        let saved_output_captures = std::mem::take(&mut self.exec.output_captures);
5382        self.exec.trap_depth += 1;
5383        self.vm.state.last_status = 128 + spec.number;
5384        let events = self.execute_input_inner(handler);
5385        self.exec.trap_depth -= 1;
5386        self.current_exec_io = saved_exec_io;
5387        self.exec.output_captures = saved_output_captures;
5388        if !self.exec.resource_exhausted && self.exec.exit_requested == saved_exit_requested {
5389            self.vm.state.last_status = saved_status;
5390        }
5391        events
5392    }
5393
5394    fn with_nested_shell_scope<T>(&mut self, f: impl FnOnce(&mut Self) -> T) -> T {
5395        self.exec.nested_shell_depth += 1;
5396        let out = f(self);
5397        self.exec.nested_shell_depth -= 1;
5398        out
5399    }
5400
5401    fn drain_io_events(&mut self, events: &mut Vec<WorkerEvent>) {
5402        self.push_buffer_event(events, true);
5403        self.push_buffer_event(events, false);
5404    }
5405
5406    fn push_buffer_event(&mut self, events: &mut Vec<WorkerEvent>, stdout: bool) {
5407        let buffer = if stdout {
5408            &mut self.vm.stdout
5409        } else {
5410            &mut self.vm.stderr
5411        };
5412        if buffer.is_empty() {
5413            return;
5414        }
5415
5416        let data = std::mem::take(buffer);
5417        events.push(if stdout {
5418            WorkerEvent::Stdout(data)
5419        } else {
5420            WorkerEvent::Stderr(data)
5421        });
5422    }
5423
5424    fn push_output_capture(&mut self, capture_stdout: bool, capture_stderr: bool) {
5425        self.exec.output_captures.push(OutputCapture {
5426            capture_stdout,
5427            capture_stderr,
5428            ..OutputCapture::default()
5429        });
5430    }
5431
5432    fn pop_output_capture(&mut self) -> CapturedOutput {
5433        let capture = self
5434            .exec
5435            .output_captures
5436            .pop()
5437            .expect("output capture stack underflow");
5438        CapturedOutput {
5439            stdout: capture.stdout,
5440            stderr: capture.stderr,
5441        }
5442    }
5443
5444    fn with_output_capture<T>(
5445        &mut self,
5446        capture_stdout: bool,
5447        capture_stderr: bool,
5448        f: impl FnOnce(&mut Self) -> T,
5449    ) -> (T, CapturedOutput) {
5450        self.push_output_capture(capture_stdout, capture_stderr);
5451        let result = f(self);
5452        let captured = self.pop_output_capture();
5453        (result, captured)
5454    }
5455
5456    fn with_exec_io_scope<T>(
5457        &mut self,
5458        exec_io: Option<ExecIo>,
5459        f: impl FnOnce(&mut Self) -> T,
5460    ) -> T {
5461        if let Some(exec_io) = exec_io {
5462            let saved = self.current_exec_io.replace(exec_io);
5463            let result = f(self);
5464            let current = self.current_exec_io.take();
5465            self.current_exec_io = match (saved, current) {
5466                (Some(mut saved), Some(mut current)) => {
5467                    let stdin = current.take_stdin();
5468                    saved.fds_mut().set_input(stdin);
5469                    Some(saved)
5470                }
5471                (saved, _) => saved,
5472            };
5473            result
5474        } else {
5475            f(self)
5476        }
5477    }
5478
5479    fn append_visible_output_direct(&mut self, data: &[u8], stdout: bool) {
5480        if stdout {
5481            self.vm.stdout.extend_from_slice(data);
5482        } else {
5483            self.vm.stderr.extend_from_slice(data);
5484        }
5485    }
5486
5487    fn write_output_destination_direct(&mut self, destination: &OutputTarget, data: &[u8]) -> bool {
5488        match destination {
5489            OutputTarget::InheritStdout => {
5490                self.append_visible_output_direct(data, true);
5491                true
5492            }
5493            OutputTarget::InheritStderr => {
5494                self.append_visible_output_direct(data, false);
5495                true
5496            }
5497            OutputTarget::File { path, sink, .. } => {
5498                if let Err(err) = sink.borrow_mut().write(data) {
5499                    let msg = format!("wasmsh: write error: {err}\n");
5500                    self.emit_visible_stderr_direct(msg.as_bytes());
5501                    self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
5502                        level: wasmsh_vm::DiagLevel::Error,
5503                        category: wasmsh_vm::DiagCategory::Filesystem,
5504                        message: format!("write failed for {path}: {err}"),
5505                    });
5506                }
5507                false
5508            }
5509            OutputTarget::ProcessSubst { path } => {
5510                if let Some(sink) = self.process_subst_out_sink_mut(path) {
5511                    sink.write(data);
5512                } else {
5513                    let msg = format!("wasmsh: {path}: process substitution sink not found\n");
5514                    self.emit_visible_stderr_direct(msg.as_bytes());
5515                }
5516                false
5517            }
5518            OutputTarget::Pipe(pipe) => {
5519                pipe.borrow_mut().write_all(data);
5520                false
5521            }
5522            OutputTarget::Closed => false,
5523        }
5524    }
5525
5526    fn emit_visible_stderr_direct(&mut self, data: &[u8]) {
5527        self.append_visible_output_direct(data, false);
5528        self.account_output(data.len());
5529    }
5530
5531    fn route_output(&mut self, data: &[u8], stdout: bool) -> bool {
5532        let mut routed_stdout = stdout;
5533        if let Some(exec_io) = self.current_exec_io.as_ref() {
5534            let destination = exec_io.output_target(stdout);
5535            match destination {
5536                OutputTarget::InheritStdout => {
5537                    routed_stdout = true;
5538                }
5539                OutputTarget::InheritStderr => {
5540                    routed_stdout = false;
5541                }
5542                OutputTarget::File { .. }
5543                | OutputTarget::ProcessSubst { .. }
5544                | OutputTarget::Pipe(_)
5545                | OutputTarget::Closed => {
5546                    return self.write_output_destination_direct(&destination, data);
5547                }
5548            }
5549        }
5550
5551        for capture in self.exec.output_captures.iter_mut().rev() {
5552            let should_capture = if routed_stdout {
5553                capture.capture_stdout
5554            } else {
5555                capture.capture_stderr
5556            };
5557            if !should_capture {
5558                continue;
5559            }
5560            if routed_stdout {
5561                capture.stdout.extend_from_slice(data);
5562            } else {
5563                capture.stderr.extend_from_slice(data);
5564            }
5565            return false;
5566        }
5567
5568        if routed_stdout {
5569            self.vm.stdout.extend_from_slice(data);
5570        } else {
5571            self.vm.stderr.extend_from_slice(data);
5572        }
5573        true
5574    }
5575
5576    fn account_output(&mut self, bytes: usize) {
5577        self.vm.track_output(bytes as u64);
5578        self.flag_output_limit_if_needed();
5579    }
5580
5581    fn write_stdout(&mut self, data: &[u8]) {
5582        if self.route_output(data, true) {
5583            self.account_output(data.len());
5584        }
5585    }
5586
5587    fn write_stderr(&mut self, data: &[u8]) {
5588        if self.route_output(data, false) {
5589            self.account_output(data.len());
5590        }
5591    }
5592
5593    fn write_streams(&mut self, stdout: &[u8], stderr: &[u8]) {
5594        let visible_stdout = self.route_output(stdout, true);
5595        let visible_stderr = self.route_output(stderr, false);
5596        let visible_bytes =
5597            usize::from(visible_stdout) * stdout.len() + usize::from(visible_stderr) * stderr.len();
5598        if visible_bytes > 0 {
5599            self.account_output(visible_bytes);
5600        }
5601    }
5602
5603    fn flag_output_limit_if_needed(&mut self) {
5604        if self.exec.resource_exhausted {
5605            return;
5606        }
5607        if self.vm.check_output_limit().is_err() {
5608            self.exec.resource_exhausted = true;
5609        }
5610    }
5611
5612    fn drain_diagnostic_events(&mut self, events: &mut Vec<WorkerEvent>) {
5613        for diag in self.vm.diagnostics.drain(..) {
5614            events.push(WorkerEvent::Diagnostic(
5615                Self::to_protocol_diag_level(diag.level),
5616                diag.message,
5617            ));
5618        }
5619    }
5620
5621    fn to_protocol_diag_level(level: wasmsh_vm::DiagLevel) -> DiagnosticLevel {
5622        match level {
5623            wasmsh_vm::DiagLevel::Trace => DiagnosticLevel::Trace,
5624            wasmsh_vm::DiagLevel::Info => DiagnosticLevel::Info,
5625            wasmsh_vm::DiagLevel::Warning => DiagnosticLevel::Warning,
5626            wasmsh_vm::DiagLevel::Error => DiagnosticLevel::Error,
5627        }
5628    }
5629
5630    fn execute_pipeline_chain(&mut self, and_or: &HirAndOr) {
5631        self.execute_pipeline(&and_or.first);
5632        for (op, pipeline) in &and_or.rest {
5633            match op {
5634                HirAndOrOp::And => {
5635                    if self.vm.state.last_status == 0 {
5636                        self.execute_pipeline(pipeline);
5637                    }
5638                }
5639                HirAndOrOp::Or => {
5640                    if self.vm.state.last_status != 0 {
5641                        self.execute_pipeline(pipeline);
5642                    }
5643                }
5644            }
5645        }
5646    }
5647
5648    #[allow(clippy::let_unit_value)]
5649    fn execute_pipeline(&mut self, pipeline: &HirPipeline) {
5650        let started = pipeline_started_at();
5651        let cmds = &pipeline.commands;
5652        self.execute_scheduled_pipeline(cmds, pipeline);
5653        if pipeline.negated {
5654            self.vm.state.last_status = i32::from(self.vm.state.last_status == 0);
5655        }
5656        if pipeline.timed {
5657            self.emit_pipeline_timing(pipeline.time_posix, started_elapsed_seconds(started));
5658        }
5659    }
5660
5661    fn execute_scheduled_pipeline(&mut self, cmds: &[HirCommand], pipeline: &HirPipeline) {
5662        self.execute_scheduled_pipeline_with_source_reader(cmds, pipeline, None);
5663    }
5664
5665    fn execute_scheduled_pipeline_with_source_reader(
5666        &mut self,
5667        cmds: &[HirCommand],
5668        pipeline: &HirPipeline,
5669        source_reader: Option<Box<dyn Read>>,
5670    ) {
5671        let pipefail = self.vm.state.get_var("SHOPT_o_pipefail").as_deref() == Some("1");
5672        let (stages, stage_last_args) = self.compile_pipeline_stages(cmds, source_reader.is_none());
5673        if source_reader.is_none() && stages.len() == 1 {
5674            self.run_single_pipeline_stage(&cmds[0], &stages[0], stage_last_args[0].as_deref());
5675            return;
5676        }
5677        let stage_statuses = Self::seed_stage_statuses(&stages);
5678        let stage_stderr: Vec<Rc<RefCell<Vec<u8>>>> = stages
5679            .iter()
5680            .map(|_| Rc::new(RefCell::new(Vec::new())))
5681            .collect();
5682        let stage_pipe_stderr: Vec<bool> = (0..stages.len())
5683            .map(|idx| pipeline.pipe_stderr.get(idx).copied().unwrap_or(false))
5684            .collect();
5685
5686        self.execute_pipebuffer_streaming_pipeline(
5687            source_reader,
5688            &stages,
5689            &stage_pipe_stderr,
5690            &stage_statuses,
5691            &stage_stderr,
5692        );
5693
5694        let statuses: Vec<i32> = stage_statuses
5695            .iter()
5696            .map(|status| *status.borrow())
5697            .collect();
5698        if let Some(last_arg) = stage_last_args.iter().rev().flatten().next() {
5699            self.vm.state.set_last_argument(last_arg.as_str());
5700        }
5701        self.set_pipestatus(&statuses);
5702        if !self.exec.resource_exhausted {
5703            self.vm.state.last_status = Self::resolve_pipeline_exit_status(&statuses, pipefail);
5704        }
5705    }
5706
5707    fn compile_pipeline_stages(
5708        &mut self,
5709        cmds: &[HirCommand],
5710        no_source_reader: bool,
5711    ) -> (Vec<StreamingPipelineStage>, Vec<Option<String>>) {
5712        cmds.iter()
5713            .enumerate()
5714            .map(|(idx, cmd)| {
5715                self.compile_pipeline_stage_with_last_argument(cmd, idx == 0 && no_source_reader)
5716            })
5717            .unzip()
5718    }
5719
5720    fn run_single_pipeline_stage(
5721        &mut self,
5722        cmd: &HirCommand,
5723        stage: &StreamingPipelineStage,
5724        last_arg: Option<&str>,
5725    ) {
5726        if self.command_needs_full_single_stage_execution(cmd) {
5727            self.execute_command(cmd);
5728            let status = self.vm.state.last_status;
5729            self.set_pipestatus(&[status]);
5730            return;
5731        }
5732        if !matches!(stage, StreamingPipelineStage::BufferedCommand(_))
5733            && !Self::command_requires_runtime_expansion(cmd)
5734        {
5735            if let Some(argv) = self.resolve_streaming_pipeline_argv(cmd) {
5736                self.trace_command(&argv);
5737            }
5738        }
5739        let status = self.execute_scheduled_single_stage(stage);
5740        if let Some(last_arg) = last_arg {
5741            self.vm.state.set_last_argument(last_arg);
5742        }
5743        self.set_pipestatus(&[status]);
5744        if !self.exec.resource_exhausted {
5745            self.vm.state.last_status = status;
5746        }
5747    }
5748
5749    fn seed_stage_statuses(stages: &[StreamingPipelineStage]) -> Vec<Rc<RefCell<i32>>> {
5750        stages
5751            .iter()
5752            .map(|stage| {
5753                Rc::new(RefCell::new(i32::from(matches!(
5754                    stage,
5755                    StreamingPipelineStage::Grep(_)
5756                ))))
5757            })
5758            .collect()
5759    }
5760
5761    fn resolve_pipeline_exit_status(statuses: &[i32], pipefail: bool) -> i32 {
5762        if pipefail {
5763            statuses
5764                .iter()
5765                .rev()
5766                .copied()
5767                .find(|status| *status != 0)
5768                .unwrap_or(0)
5769        } else {
5770            statuses.last().copied().unwrap_or(0)
5771        }
5772    }
5773
5774    fn execute_scheduled_single_stage(&mut self, stage: &StreamingPipelineStage) -> i32 {
5775        match stage {
5776            StreamingPipelineStage::Literal(data) => {
5777                self.write_stdout(data);
5778                0
5779            }
5780            StreamingPipelineStage::File(path) => self.execute_single_stage_file(path),
5781            StreamingPipelineStage::Yes { line } => self.execute_single_stage_yes(line),
5782            StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(argv)) => {
5783                self.trace_command(argv);
5784                self.execute_argv_command(argv);
5785                self.vm.state.last_status
5786            }
5787            StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(cmd)) => {
5788                self.execute_command(cmd);
5789                self.vm.state.last_status
5790            }
5791            _ => {
5792                self.vm.state.last_status = 1;
5793                self.write_stderr(b"wasmsh: unsupported single-stage scheduler node\n");
5794                1
5795            }
5796        }
5797    }
5798
5799    fn execute_single_stage_file(&mut self, path: &str) -> i32 {
5800        let resolved = self.resolve_cwd_path(path);
5801        let Ok(mut reader) = self.open_streaming_file_reader(&resolved, "cat") else {
5802            return self.vm.state.last_status;
5803        };
5804        let mut buffer = [0u8; 4096];
5805        loop {
5806            match reader.read(&mut buffer) {
5807                Ok(0) => return 0,
5808                Ok(read) => {
5809                    self.write_stdout(&buffer[..read]);
5810                    if self.exec.resource_exhausted {
5811                        return 1;
5812                    }
5813                }
5814                Err(err) => {
5815                    self.write_stderr(format!("wasmsh: cat: stdin read error: {err}\n").as_bytes());
5816                    return 1;
5817                }
5818            }
5819        }
5820    }
5821
5822    fn execute_single_stage_yes(&mut self, line: &[u8]) -> i32 {
5823        for _ in 0..STREAMING_YES_MAX_LINES {
5824            self.write_stdout(line);
5825            if self.exec.resource_exhausted {
5826                return 1;
5827            }
5828        }
5829        0
5830    }
5831
5832    fn compile_pipeline_stage(
5833        &mut self,
5834        cmd: &HirCommand,
5835        is_first: bool,
5836    ) -> StreamingPipelineStage {
5837        let resolved_argv = self.resolve_streaming_pipeline_argv(cmd);
5838        self.compile_pipeline_stage_from_argv(cmd, is_first, resolved_argv)
5839    }
5840
5841    fn compile_pipeline_stage_with_last_argument(
5842        &mut self,
5843        cmd: &HirCommand,
5844        is_first: bool,
5845    ) -> (StreamingPipelineStage, Option<String>) {
5846        let resolved_argv = self.resolve_streaming_pipeline_argv(cmd);
5847        let last_arg = resolved_argv.as_ref().and_then(|argv| argv.last().cloned());
5848        (
5849            self.compile_pipeline_stage_from_argv(cmd, is_first, resolved_argv),
5850            last_arg,
5851        )
5852    }
5853
5854    fn compile_pipeline_stage_from_argv(
5855        &mut self,
5856        cmd: &HirCommand,
5857        is_first: bool,
5858        resolved_argv: Option<Vec<String>>,
5859    ) -> StreamingPipelineStage {
5860        if let Some(argv) = resolved_argv {
5861            if self.get_shopt_value("expand_aliases")
5862                && argv
5863                    .first()
5864                    .is_some_and(|name| self.aliases.contains_key(name))
5865            {
5866                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(
5867                    cmd.clone(),
5868                ));
5869            }
5870            if argv
5871                .first()
5872                .is_some_and(|name| self.functions.contains_key(name))
5873            {
5874                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(
5875                    argv,
5876                ));
5877            }
5878            if let Some(stage) = self.parse_streaming_stage(&argv, is_first) {
5879                if Self::uses_native_pipe_scheduler(&stage) {
5880                    return stage;
5881                }
5882                return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Argv(
5883                    argv,
5884                ));
5885            }
5886            return StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(
5887                cmd.clone(),
5888            ));
5889        }
5890        StreamingPipelineStage::BufferedCommand(BufferedPipelineCommand::Hir(cmd.clone()))
5891    }
5892
5893    fn uses_native_pipe_scheduler(stage: &StreamingPipelineStage) -> bool {
5894        !matches!(stage, StreamingPipelineStage::BufferedCommand(_))
5895    }
5896
5897    fn execute_pipebuffer_streaming_pipeline(
5898        &mut self,
5899        source_reader: Option<Box<dyn Read>>,
5900        stages: &[StreamingPipelineStage],
5901        stage_pipe_stderr: &[bool],
5902        stage_statuses: &[Rc<RefCell<i32>>],
5903        stage_stderr: &[Rc<RefCell<Vec<u8>>>],
5904    ) -> bool {
5905        let mut processes = Vec::new();
5906        let output_pipes: Vec<Rc<RefCell<PipeBuffer>>> = (0..stages.len())
5907            .map(|_| Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY))))
5908            .collect();
5909        let ctx = StreamingStageCtx {
5910            stages,
5911            stage_pipe_stderr,
5912            stage_statuses,
5913            stage_stderr,
5914            output_pipes: &output_pipes,
5915        };
5916
5917        if let Some(early) = self.setup_first_streaming_process(source_reader, &ctx, &mut processes)
5918        {
5919            return early;
5920        }
5921        for idx in 1..stages.len() {
5922            if !self.setup_later_streaming_stage(idx, &ctx, &mut processes) {
5923                return false;
5924            }
5925        }
5926
5927        let final_pipe = output_pipes
5928            .last()
5929            .cloned()
5930            .expect("final pipe missing for streaming pipeline");
5931        self.drive_streaming_pipeline(&mut processes, &output_pipes, &final_pipe);
5932
5933        for process in &mut processes {
5934            process.close(self);
5935        }
5936        self.drain_streaming_stage_stderr(stage_pipe_stderr, stage_stderr);
5937        true
5938    }
5939
5940    fn setup_first_streaming_process(
5941        &mut self,
5942        source_reader: Option<Box<dyn Read>>,
5943        ctx: &StreamingStageCtx<'_>,
5944        processes: &mut Vec<StreamingPipeProcess<'static>>,
5945    ) -> Option<bool> {
5946        if let Some(source_reader) = source_reader {
5947            self.setup_first_with_source(source_reader, ctx, processes)
5948        } else {
5949            self.setup_first_without_source(ctx, processes)
5950        }
5951    }
5952
5953    fn setup_first_with_source(
5954        &mut self,
5955        source_reader: Box<dyn Read>,
5956        ctx: &StreamingStageCtx<'_>,
5957        processes: &mut Vec<StreamingPipeProcess<'static>>,
5958    ) -> Option<bool> {
5959        let source_pipe = Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY)));
5960        let source_stderr = Rc::new(RefCell::new(Vec::new()));
5961        let source_status = Rc::new(RefCell::new(0));
5962        processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
5963            source_reader,
5964            source_pipe.clone(),
5965            source_stderr,
5966            source_status,
5967            "source",
5968            false,
5969        )));
5970        match &ctx.stages[0] {
5971            StreamingPipelineStage::Tee(stage) => {
5972                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
5973                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
5974                    reader,
5975                    ctx.output_pipes[0].clone(),
5976                    &mut self.fs,
5977                    self.vm.state.cwd.as_str(),
5978                    stage,
5979                    ctx.stage_stderr[0].clone(),
5980                    ctx.stage_statuses[0].clone(),
5981                    ctx.stage_pipe_stderr[0],
5982                )));
5983                None
5984            }
5985            StreamingPipelineStage::BufferedCommand(argv) => {
5986                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
5987                    Some(source_pipe),
5988                    ctx.output_pipes[0].clone(),
5989                    argv.clone(),
5990                    ctx.stage_pipe_stderr[0],
5991                    ctx.stage_stderr[0].clone(),
5992                    ctx.stage_statuses[0].clone(),
5993                )));
5994                None
5995            }
5996            _ => {
5997                let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
5998                let Some(stage_reader) = Self::wrap_non_tee_streaming_stage(
5999                    reader,
6000                    &ctx.stages[0],
6001                    0,
6002                    ctx.stage_statuses,
6003                ) else {
6004                    return Some(false);
6005                };
6006                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6007                    stage_reader,
6008                    ctx.output_pipes[0].clone(),
6009                    ctx.stage_stderr[0].clone(),
6010                    ctx.stage_statuses[0].clone(),
6011                    "stage",
6012                    ctx.stage_pipe_stderr[0],
6013                )));
6014                None
6015            }
6016        }
6017    }
6018
6019    fn setup_first_without_source(
6020        &mut self,
6021        ctx: &StreamingStageCtx<'_>,
6022        processes: &mut Vec<StreamingPipeProcess<'static>>,
6023    ) -> Option<bool> {
6024        match &ctx.stages[0] {
6025            StreamingPipelineStage::Literal(data) => {
6026                let first_reader: Box<dyn Read> = Box::new(Cursor::new(data.clone()));
6027                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6028                    first_reader,
6029                    ctx.output_pipes[0].clone(),
6030                    ctx.stage_stderr[0].clone(),
6031                    ctx.stage_statuses[0].clone(),
6032                    "source",
6033                    ctx.stage_pipe_stderr[0],
6034                )));
6035                None
6036            }
6037            StreamingPipelineStage::File(path) => {
6038                let resolved = self.resolve_cwd_path(path);
6039                let Ok(first_reader) = self.open_streaming_file_reader(&resolved, "cat") else {
6040                    *ctx.stage_statuses[0].borrow_mut() = self.vm.state.last_status;
6041                    return Some(true);
6042                };
6043                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6044                    first_reader,
6045                    ctx.output_pipes[0].clone(),
6046                    ctx.stage_stderr[0].clone(),
6047                    ctx.stage_statuses[0].clone(),
6048                    "source",
6049                    ctx.stage_pipe_stderr[0],
6050                )));
6051                None
6052            }
6053            StreamingPipelineStage::Yes { line } => {
6054                let first_reader: Box<dyn Read> =
6055                    Box::new(YesStreamReader::new(line.clone(), STREAMING_YES_MAX_LINES));
6056                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6057                    first_reader,
6058                    ctx.output_pipes[0].clone(),
6059                    ctx.stage_stderr[0].clone(),
6060                    ctx.stage_statuses[0].clone(),
6061                    "source",
6062                    ctx.stage_pipe_stderr[0],
6063                )));
6064                None
6065            }
6066            StreamingPipelineStage::BufferedCommand(argv) => {
6067                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
6068                    None,
6069                    ctx.output_pipes[0].clone(),
6070                    argv.clone(),
6071                    ctx.stage_pipe_stderr[0],
6072                    ctx.stage_stderr[0].clone(),
6073                    ctx.stage_statuses[0].clone(),
6074                )));
6075                None
6076            }
6077            _ => unreachable!("unexpected first pipeline stage"),
6078        }
6079    }
6080
6081    fn setup_later_streaming_stage(
6082        &mut self,
6083        idx: usize,
6084        ctx: &StreamingStageCtx<'_>,
6085        processes: &mut Vec<StreamingPipeProcess<'static>>,
6086    ) -> bool {
6087        match &ctx.stages[idx] {
6088            StreamingPipelineStage::Head(mode) => {
6089                processes.push(StreamingPipeProcess::Head(HeadPipeProcess::new(
6090                    ctx.output_pipes[idx - 1].clone(),
6091                    ctx.output_pipes[idx].clone(),
6092                    *mode,
6093                )));
6094            }
6095            StreamingPipelineStage::Tee(stage) => {
6096                let reader =
6097                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
6098                processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
6099                    reader,
6100                    ctx.output_pipes[idx].clone(),
6101                    &mut self.fs,
6102                    self.vm.state.cwd.as_str(),
6103                    stage,
6104                    ctx.stage_stderr[idx].clone(),
6105                    ctx.stage_statuses[idx].clone(),
6106                    ctx.stage_pipe_stderr[idx],
6107                )));
6108            }
6109            StreamingPipelineStage::BufferedCommand(argv) => {
6110                processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
6111                    Some(ctx.output_pipes[idx - 1].clone()),
6112                    ctx.output_pipes[idx].clone(),
6113                    argv.clone(),
6114                    ctx.stage_pipe_stderr[idx],
6115                    ctx.stage_stderr[idx].clone(),
6116                    ctx.stage_statuses[idx].clone(),
6117                )));
6118            }
6119            _ => {
6120                let reader =
6121                    Box::new(PipeReader::new(ctx.output_pipes[idx - 1].clone())) as Box<dyn Read>;
6122                let Some(stage_reader) = Self::wrap_non_tee_streaming_stage(
6123                    reader,
6124                    &ctx.stages[idx],
6125                    idx,
6126                    ctx.stage_statuses,
6127                ) else {
6128                    return false;
6129                };
6130                processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
6131                    stage_reader,
6132                    ctx.output_pipes[idx].clone(),
6133                    ctx.stage_stderr[idx].clone(),
6134                    ctx.stage_statuses[idx].clone(),
6135                    "stage",
6136                    ctx.stage_pipe_stderr[idx],
6137                )));
6138            }
6139        }
6140        true
6141    }
6142
6143    fn drive_streaming_pipeline(
6144        &mut self,
6145        processes: &mut [StreamingPipeProcess<'static>],
6146        output_pipes: &[Rc<RefCell<PipeBuffer>>],
6147        final_pipe: &Rc<RefCell<PipeBuffer>>,
6148    ) {
6149        let mut finished = vec![false; processes.len()];
6150        loop {
6151            if self.check_resource_limits() {
6152                final_pipe.borrow_mut().close_read();
6153                break;
6154            }
6155
6156            let mut progressed = self.poll_streaming_processes(processes, &mut finished);
6157
6158            let buffered_pipe_bytes = output_pipes
6159                .iter()
6160                .map(|pipe| pipe.borrow().len() as u64)
6161                .sum();
6162            self.sync_pipe_budget(buffered_pipe_bytes);
6163            if self.exec.resource_exhausted {
6164                final_pipe.borrow_mut().close_read();
6165                break;
6166            }
6167
6168            if self.drain_final_pipe_to_stdout(final_pipe, &mut progressed) {
6169                break;
6170            }
6171
6172            if self.exec.resource_exhausted || finished.iter().all(|done| *done) || !progressed {
6173                break;
6174            }
6175        }
6176    }
6177
6178    fn poll_streaming_processes(
6179        &mut self,
6180        processes: &mut [StreamingPipeProcess<'static>],
6181        finished: &mut [bool],
6182    ) -> bool {
6183        let mut progressed = false;
6184        for idx in (0..processes.len()).rev() {
6185            if finished[idx] {
6186                continue;
6187            }
6188            match processes[idx].poll(self) {
6189                PipeProcessPoll::Ready => progressed = true,
6190                PipeProcessPoll::PendingRead | PipeProcessPoll::PendingWrite => {}
6191                PipeProcessPoll::Exited => {
6192                    finished[idx] = true;
6193                    progressed = true;
6194                }
6195            }
6196        }
6197        progressed
6198    }
6199
6200    fn drain_final_pipe_to_stdout(
6201        &mut self,
6202        final_pipe: &Rc<RefCell<PipeBuffer>>,
6203        progressed: &mut bool,
6204    ) -> bool {
6205        loop {
6206            let mut buffer = [0u8; 4096];
6207            let read_result = {
6208                let mut pipe = final_pipe.borrow_mut();
6209                pipe.read(&mut buffer)
6210            };
6211            match read_result {
6212                ReadResult::Read(read) => {
6213                    self.write_stdout(&buffer[..read]);
6214                    *progressed = true;
6215                    if self.exec.resource_exhausted {
6216                        final_pipe.borrow_mut().close_read();
6217                        return true;
6218                    }
6219                }
6220                ReadResult::WouldBlock | ReadResult::Eof => return false,
6221            }
6222        }
6223    }
6224
6225    fn drain_streaming_stage_stderr(
6226        &mut self,
6227        stage_pipe_stderr: &[bool],
6228        stage_stderr: &[Rc<RefCell<Vec<u8>>>],
6229    ) {
6230        for (idx, stderr) in stage_stderr.iter().enumerate() {
6231            if stage_pipe_stderr[idx] {
6232                continue;
6233            }
6234            let data = stderr.borrow();
6235            if !data.is_empty() {
6236                self.write_stderr(&data);
6237            }
6238        }
6239    }
6240
6241    fn wrap_non_tee_streaming_stage<'a>(
6242        reader: Box<dyn Read + 'a>,
6243        stage: &StreamingPipelineStage,
6244        idx: usize,
6245        stage_statuses: &[Rc<RefCell<i32>>],
6246    ) -> Option<Box<dyn Read + 'a>> {
6247        match stage {
6248            StreamingPipelineStage::Cat => Some(reader),
6249            StreamingPipelineStage::Head(mode) => Some(match mode {
6250                StreamingHeadMode::Lines(limit) => Box::new(HeadStreamReader::new(
6251                    reader,
6252                    StreamingHeadMode::Lines(*limit),
6253                )),
6254                StreamingHeadMode::Bytes(limit) => Box::new(HeadStreamReader::new(
6255                    reader,
6256                    StreamingHeadMode::Bytes(*limit),
6257                )),
6258            }),
6259            StreamingPipelineStage::Tail(mode) => Some(match mode {
6260                StreamingTailMode::Lines(limit) => Box::new(TailStreamReader::new(
6261                    reader,
6262                    StreamingTailMode::Lines(*limit),
6263                )),
6264                StreamingTailMode::Bytes(limit) => Box::new(TailStreamReader::new(
6265                    reader,
6266                    StreamingTailMode::Bytes(*limit),
6267                )),
6268            }),
6269            StreamingPipelineStage::Bat(stage) => {
6270                Some(Box::new(BatStreamReader::new(reader, *stage)))
6271            }
6272            StreamingPipelineStage::Sed(stage) => {
6273                Some(Box::new(SedStreamReader::new(reader, stage.clone())))
6274            }
6275            StreamingPipelineStage::Paste(stage) => {
6276                Some(Box::new(PasteStreamReader::new(reader, stage.clone())))
6277            }
6278            StreamingPipelineStage::Column(_) => Some(Box::new(ColumnStreamReader::new(reader))),
6279            StreamingPipelineStage::Grep(stage) => Some(Box::new(GrepStreamReader::new(
6280                reader,
6281                stage.clone(),
6282                stage_statuses[idx].clone(),
6283            ))),
6284            StreamingPipelineStage::Uniq(flags) => {
6285                Some(Box::new(UniqStreamReader::new(reader, flags.clone())))
6286            }
6287            StreamingPipelineStage::Rev => Some(Box::new(RevStreamReader::new(reader))),
6288            StreamingPipelineStage::Cut(stage) => {
6289                Some(Box::new(CutStreamReader::new(reader, stage.clone())))
6290            }
6291            StreamingPipelineStage::Tr(stage) => {
6292                Some(Box::new(TrStreamReader::new(reader, stage.clone())))
6293            }
6294            StreamingPipelineStage::Wc(flags) => {
6295                Some(Box::new(WcStreamReader::new(reader, *flags)))
6296            }
6297            StreamingPipelineStage::Tee(_)
6298            | StreamingPipelineStage::Literal(_)
6299            | StreamingPipelineStage::File(_)
6300            | StreamingPipelineStage::Yes { .. }
6301            | StreamingPipelineStage::BufferedCommand(_) => None,
6302        }
6303    }
6304
6305    fn resolve_streaming_pipeline_argv(&mut self, cmd: &HirCommand) -> Option<Vec<String>> {
6306        let HirCommand::Exec(exec) = cmd else {
6307            return None;
6308        };
6309        if !exec.env.is_empty()
6310            || !exec.redirections.is_empty()
6311            || Self::command_requires_runtime_expansion(cmd)
6312        {
6313            return None;
6314        }
6315        let resolved = self.resolve_command_subst(&exec.argv);
6316        if self.exec.expansion_failed {
6317            return None;
6318        }
6319        let expanded = expand_words_argv(&resolved, &mut self.vm.state);
6320        if self.check_nounset_error() || expanded.is_empty() {
6321            return None;
6322        }
6323        let tagged: Vec<(String, bool)> = expanded
6324            .into_iter()
6325            .flat_map(|ew| {
6326                if ew.was_quoted {
6327                    vec![(ew.text, true)]
6328                } else {
6329                    wasmsh_expand::expand_braces(&ew.text)
6330                        .into_iter()
6331                        .map(|s| (s, false))
6332                        .collect()
6333                }
6334            })
6335            .collect();
6336        Some(self.expand_globs_tagged(tagged))
6337    }
6338
6339    fn parse_streaming_stage(
6340        &self,
6341        argv: &[String],
6342        is_first: bool,
6343    ) -> Option<StreamingPipelineStage> {
6344        let cmd_name = argv.first()?.as_str();
6345        if let Some(stage) = Self::parse_streaming_first_stage(cmd_name, argv, is_first) {
6346            return Some(stage);
6347        }
6348        if let Some(stage) = Self::parse_streaming_internal_stage(cmd_name, argv, is_first) {
6349            return Some(stage);
6350        }
6351        if self.is_buffered_stage_candidate(cmd_name) {
6352            return Some(StreamingPipelineStage::BufferedCommand(
6353                BufferedPipelineCommand::Argv(argv.to_vec()),
6354            ));
6355        }
6356        None
6357    }
6358
6359    fn parse_streaming_first_stage(
6360        cmd_name: &str,
6361        argv: &[String],
6362        is_first: bool,
6363    ) -> Option<StreamingPipelineStage> {
6364        if !is_first {
6365            return None;
6366        }
6367        match cmd_name {
6368            "echo" => Some(StreamingPipelineStage::Literal(Self::streaming_echo_bytes(
6369                &argv[1..],
6370            ))),
6371            "yes" => {
6372                let text = if argv.len() > 1 {
6373                    argv[1..].join(" ")
6374                } else {
6375                    "y".to_string()
6376                };
6377                Some(StreamingPipelineStage::Yes {
6378                    line: format!("{text}\n").into_bytes(),
6379                })
6380            }
6381            _ => None,
6382        }
6383    }
6384
6385    fn parse_streaming_internal_stage(
6386        cmd_name: &str,
6387        argv: &[String],
6388        is_first: bool,
6389    ) -> Option<StreamingPipelineStage> {
6390        if cmd_name == "cat" {
6391            return Self::parse_streaming_cat_stage(&argv[1..], is_first);
6392        }
6393        if is_first {
6394            return None;
6395        }
6396        match cmd_name {
6397            "head" => Self::parse_streaming_head_stage(&argv[1..]),
6398            "tail" => Self::parse_streaming_tail_stage(&argv[1..]),
6399            "bat" => Self::parse_streaming_bat_stage(&argv[1..]),
6400            "sed" => Self::parse_streaming_sed_stage(&argv[1..]),
6401            "tee" => Self::parse_streaming_tee_stage(&argv[1..]),
6402            "paste" => Self::parse_streaming_paste_stage(&argv[1..]),
6403            "column" => Self::parse_streaming_column_stage(&argv[1..]),
6404            "grep" => Self::parse_streaming_grep_stage(&argv[1..]),
6405            "uniq" => Self::parse_streaming_uniq_stage(&argv[1..]),
6406            "rev" => Self::parse_streaming_rev_stage(&argv[1..]),
6407            "cut" => Self::parse_streaming_cut_stage(&argv[1..]),
6408            "tr" => Self::parse_streaming_tr_stage(&argv[1..]),
6409            "wc" => Self::parse_streaming_wc_stage(&argv[1..]),
6410            _ => None,
6411        }
6412    }
6413
6414    fn is_buffered_stage_candidate(&self, cmd_name: &str) -> bool {
6415        cmd_name == "bash"
6416            || cmd_name == "sh"
6417            || cmd_name == "builtin"
6418            || self.functions.contains_key(cmd_name)
6419            || self.builtins.is_builtin(cmd_name)
6420            || self.utils.is_utility(cmd_name)
6421            || self.external_handler.is_some()
6422    }
6423
6424    fn streaming_echo_bytes(args: &[String]) -> Vec<u8> {
6425        let mut suppress_newline = false;
6426        let mut interpret_escapes = false;
6427        let mut start = 0usize;
6428
6429        for (i, arg) in args.iter().enumerate() {
6430            let bytes = arg.as_bytes();
6431            if bytes.first() != Some(&b'-') || bytes.len() < 2 {
6432                break;
6433            }
6434            if !bytes[1..].iter().all(|b| matches!(b, b'n' | b'e')) {
6435                break;
6436            }
6437            for &byte in &bytes[1..] {
6438                match byte {
6439                    b'n' => suppress_newline = true,
6440                    b'e' => interpret_escapes = true,
6441                    _ => {}
6442                }
6443            }
6444            start = i + 1;
6445        }
6446
6447        let text = args[start..].join(" ");
6448        let rendered = if interpret_escapes {
6449            Self::process_streaming_echo_escapes(&text)
6450        } else {
6451            text
6452        };
6453        let mut output = rendered.into_bytes();
6454        if !suppress_newline {
6455            output.push(b'\n');
6456        }
6457        output
6458    }
6459
6460    fn process_streaming_echo_escapes(text: &str) -> String {
6461        let bytes = text.as_bytes();
6462        let mut output = String::new();
6463        let mut i = 0usize;
6464        while i < bytes.len() {
6465            if bytes[i] == b'\\' && i + 1 < bytes.len() {
6466                match bytes[i + 1] {
6467                    b'n' => output.push('\n'),
6468                    b't' => output.push('\t'),
6469                    b'r' => output.push('\r'),
6470                    b'\\' => output.push('\\'),
6471                    other => {
6472                        output.push('\\');
6473                        output.push(other as char);
6474                    }
6475                }
6476                i += 2;
6477            } else {
6478                output.push(bytes[i] as char);
6479                i += 1;
6480            }
6481        }
6482        output
6483    }
6484
6485    fn parse_streaming_cat_stage(
6486        args: &[String],
6487        is_first: bool,
6488    ) -> Option<StreamingPipelineStage> {
6489        let non_separator: Vec<&String> = args.iter().filter(|arg| arg.as_str() != "--").collect();
6490        if non_separator.iter().any(|arg| arg.starts_with('-')) {
6491            return None;
6492        }
6493        if is_first {
6494            if non_separator.len() == 1 {
6495                return Some(StreamingPipelineStage::File(non_separator[0].clone()));
6496            }
6497            return None;
6498        }
6499        Some(StreamingPipelineStage::Cat)
6500    }
6501
6502    fn parse_streaming_head_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6503        let mut mode = StreamingHeadMode::Lines(10);
6504        let mut files = Vec::new();
6505        let mut i = 0usize;
6506        while i < args.len() {
6507            let arg = args[i].as_str();
6508            if arg == "-c" && i + 1 < args.len() {
6509                mode = StreamingHeadMode::Bytes(args[i + 1].parse().ok()?);
6510                i += 2;
6511            } else if arg == "-n" && i + 1 < args.len() {
6512                mode = StreamingHeadMode::Lines(args[i + 1].parse().ok()?);
6513                i += 2;
6514            } else if arg.starts_with('-') && arg.len() > 1 && arg != "--" {
6515                if let Ok(lines) = arg[1..].parse::<usize>() {
6516                    mode = StreamingHeadMode::Lines(lines);
6517                } else {
6518                    return None;
6519                }
6520                i += 1;
6521            } else if arg == "--" {
6522                i += 1;
6523            } else {
6524                files.push(arg);
6525                i += 1;
6526            }
6527        }
6528        if files.is_empty() {
6529            Some(StreamingPipelineStage::Head(mode))
6530        } else {
6531            None
6532        }
6533    }
6534
6535    fn parse_streaming_tail_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6536        let mut mode = StreamingTailMode::Lines(10);
6537        let mut files: Vec<&str> = Vec::new();
6538        let mut i = 0usize;
6539        while i < args.len() {
6540            i = Self::apply_streaming_tail_arg(args, i, &mut mode, &mut files)?;
6541        }
6542        files
6543            .is_empty()
6544            .then_some(StreamingPipelineStage::Tail(mode))
6545    }
6546
6547    fn apply_streaming_tail_arg<'a>(
6548        args: &'a [String],
6549        i: usize,
6550        mode: &mut StreamingTailMode,
6551        files: &mut Vec<&'a str>,
6552    ) -> Option<usize> {
6553        let arg = args[i].as_str();
6554        if arg == "-f" {
6555            return None;
6556        }
6557        if arg == "--" {
6558            return Some(i + 1);
6559        }
6560        if arg == "-c" && i + 1 < args.len() {
6561            *mode = StreamingTailMode::Bytes(args[i + 1].parse().ok()?);
6562            return Some(i + 2);
6563        }
6564        if arg == "-n" && i + 1 < args.len() {
6565            *mode = Self::parse_streaming_tail_lines_value(&args[i + 1])?;
6566            return Some(i + 2);
6567        }
6568        if arg.starts_with('-') && arg.len() > 1 {
6569            *mode = StreamingTailMode::Lines(arg[1..].parse().ok()?);
6570            return Some(i + 1);
6571        }
6572        files.push(arg);
6573        Some(i + 1)
6574    }
6575
6576    fn parse_streaming_tail_lines_value(value: &str) -> Option<StreamingTailMode> {
6577        if value.starts_with('+') {
6578            return None;
6579        }
6580        Some(StreamingTailMode::Lines(value.parse().ok()?))
6581    }
6582
6583    fn parse_streaming_bat_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6584        let mut stage = StreamingBatStage {
6585            show_numbers: true,
6586            show_header: true,
6587            line_range: None,
6588            show_all: false,
6589        };
6590        let mut i = 0usize;
6591        while i < args.len() {
6592            let advance = Self::apply_streaming_bat_arg(args, i, &mut stage)?;
6593            i += advance;
6594        }
6595        Some(StreamingPipelineStage::Bat(stage))
6596    }
6597
6598    fn apply_streaming_bat_arg(
6599        args: &[String],
6600        i: usize,
6601        stage: &mut StreamingBatStage,
6602    ) -> Option<usize> {
6603        let arg = args[i].as_str();
6604        match arg {
6605            "-n" | "--number" => {
6606                stage.show_numbers = true;
6607                Some(1)
6608            }
6609            "-p" | "--plain" | "--style=plain" => {
6610                stage.show_numbers = false;
6611                stage.show_header = false;
6612                Some(1)
6613            }
6614            "-A" | "--show-all" => {
6615                stage.show_all = true;
6616                Some(1)
6617            }
6618            "-r" | "--line-range" if i + 1 < args.len() => {
6619                stage.line_range = Self::parse_streaming_bat_range(&args[i + 1]);
6620                Some(2)
6621            }
6622            "-l" | "--language" | "--paging" if i + 1 < args.len() => Some(2),
6623            "--style=numbers" => {
6624                stage.show_numbers = true;
6625                stage.show_header = false;
6626                Some(1)
6627            }
6628            "--style=header" => {
6629                stage.show_numbers = false;
6630                stage.show_header = true;
6631                Some(1)
6632            }
6633            "--" => (i + 1 == args.len()).then_some(1),
6634            _ => Self::apply_streaming_bat_long_or_short(arg, stage),
6635        }
6636    }
6637
6638    fn apply_streaming_bat_long_or_short(
6639        value: &str,
6640        stage: &mut StreamingBatStage,
6641    ) -> Option<usize> {
6642        if value.starts_with("--style=") {
6643            stage.show_numbers = true;
6644            stage.show_header = true;
6645            return Some(1);
6646        }
6647        if let Some(range_spec) = value.strip_prefix("--line-range=") {
6648            stage.line_range = Self::parse_streaming_bat_range(range_spec);
6649            return Some(1);
6650        }
6651        if value.starts_with("--paging=") || value.starts_with("--language=") {
6652            return Some(1);
6653        }
6654        if value.starts_with('-') && value.len() > 1 && !value.starts_with("--") {
6655            Self::apply_streaming_bat_short_cluster(&value[1..], stage)?;
6656            return Some(1);
6657        }
6658        None
6659    }
6660
6661    fn apply_streaming_bat_short_cluster(flags: &str, stage: &mut StreamingBatStage) -> Option<()> {
6662        for ch in flags.chars() {
6663            match ch {
6664                'n' => stage.show_numbers = true,
6665                'p' => {
6666                    stage.show_numbers = false;
6667                    stage.show_header = false;
6668                }
6669                'A' => stage.show_all = true,
6670                _ => return None,
6671            }
6672        }
6673        Some(())
6674    }
6675
6676    fn parse_streaming_bat_range(s: &str) -> Option<(Option<usize>, Option<usize>)> {
6677        if let Some((start, end)) = s.split_once(':') {
6678            let start = if start.is_empty() {
6679                None
6680            } else {
6681                start.parse().ok()
6682            };
6683            let end = if end.is_empty() {
6684                None
6685            } else {
6686                end.parse().ok()
6687            };
6688            Some((start, end))
6689        } else {
6690            let n = s.parse().ok()?;
6691            Some((Some(n), Some(n)))
6692        }
6693    }
6694
6695    fn parse_streaming_sed_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6696        let mut suppress_print = false;
6697        let mut expressions = Vec::new();
6698        let mut i = 0usize;
6699        while i < args.len() {
6700            let step =
6701                Self::apply_streaming_sed_arg(args, i, &mut suppress_print, &mut expressions)?;
6702            match step {
6703                StreamingSedStep::Advance(n) => i += n,
6704                StreamingSedStep::Break => break,
6705            }
6706        }
6707        if expressions.is_empty() {
6708            return None;
6709        }
6710        let script = expressions.join(";");
6711        let instructions = parse_streaming_sed_script(&script);
6712        if instructions.is_empty() {
6713            return None;
6714        }
6715        Some(StreamingPipelineStage::Sed(StreamingSedStage {
6716            suppress_print,
6717            instructions,
6718        }))
6719    }
6720
6721    fn apply_streaming_sed_arg(
6722        args: &[String],
6723        i: usize,
6724        suppress_print: &mut bool,
6725        expressions: &mut Vec<String>,
6726    ) -> Option<StreamingSedStep> {
6727        let arg = args[i].as_str();
6728        if arg == "-n" {
6729            *suppress_print = true;
6730            return Some(StreamingSedStep::Advance(1));
6731        }
6732        if arg == "-e" && i + 1 < args.len() {
6733            expressions.push(args[i + 1].clone());
6734            return Some(StreamingSedStep::Advance(2));
6735        }
6736        if arg == "-E" || arg == "-r" {
6737            return Some(StreamingSedStep::Advance(1));
6738        }
6739        if Self::streaming_sed_arg_rejected(arg) {
6740            return None;
6741        }
6742        if arg == "--" {
6743            return Self::streaming_sed_handle_doubledash(args, i, expressions);
6744        }
6745        if expressions.is_empty() {
6746            expressions.push(args[i].clone());
6747            Some(StreamingSedStep::Advance(1))
6748        } else {
6749            None
6750        }
6751    }
6752
6753    fn streaming_sed_arg_rejected(arg: &str) -> bool {
6754        arg == "-f"
6755            || arg == "-i"
6756            || arg.starts_with("-i")
6757            || (arg.starts_with('-') && arg.len() > 1 && arg != "--")
6758    }
6759
6760    fn streaming_sed_handle_doubledash(
6761        args: &[String],
6762        i: usize,
6763        expressions: &mut Vec<String>,
6764    ) -> Option<StreamingSedStep> {
6765        if i + 1 >= args.len() {
6766            return Some(StreamingSedStep::Break);
6767        }
6768        if !expressions.is_empty() {
6769            return None;
6770        }
6771        expressions.push(args[i + 1].clone());
6772        Some(StreamingSedStep::Advance(2))
6773    }
6774
6775    fn parse_streaming_paste_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6776        let mut delimiter = "\t".to_string();
6777        let mut serial = false;
6778        let mut i = 0usize;
6779        while i < args.len() {
6780            i = Self::apply_streaming_paste_arg(args, i, &mut delimiter, &mut serial)?;
6781        }
6782        Some(StreamingPipelineStage::Paste(StreamingPasteStage {
6783            delimiter,
6784            serial,
6785        }))
6786    }
6787
6788    fn apply_streaming_paste_arg(
6789        args: &[String],
6790        i: usize,
6791        delimiter: &mut String,
6792        serial: &mut bool,
6793    ) -> Option<usize> {
6794        let arg = args[i].as_str();
6795        if arg == "-d" && i + 1 < args.len() {
6796            delimiter.clone_from(&args[i + 1]);
6797            return Some(i + 2);
6798        }
6799        if arg == "-s" {
6800            *serial = true;
6801            return Some(i + 1);
6802        }
6803        if arg == "--" {
6804            return (i + 1 == args.len()).then_some(i + 1);
6805        }
6806        if arg.starts_with('-') && arg.len() > 1 {
6807            let extra = Self::apply_streaming_paste_short_cluster(args, i, delimiter, serial)?;
6808            return Some(i + 1 + extra);
6809        }
6810        None
6811    }
6812
6813    fn apply_streaming_paste_short_cluster(
6814        args: &[String],
6815        i: usize,
6816        delimiter: &mut String,
6817        serial: &mut bool,
6818    ) -> Option<usize> {
6819        let arg = args[i].as_str();
6820        let mut extra = 0usize;
6821        for ch in arg[1..].chars() {
6822            match ch {
6823                's' => *serial = true,
6824                'd' if i + 1 < args.len() => {
6825                    delimiter.clone_from(&args[i + 1]);
6826                    extra = 1;
6827                }
6828                _ => return None,
6829            }
6830        }
6831        Some(extra)
6832    }
6833
6834    fn parse_streaming_tee_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6835        let mut append = false;
6836        let mut paths = Vec::new();
6837        let mut i = 0usize;
6838        while i < args.len() {
6839            let arg = args[i].as_str();
6840            if arg == "-a" {
6841                append = true;
6842                i += 1;
6843            } else if arg == "-i" {
6844                i += 1;
6845            } else if arg == "--" {
6846                paths.extend(args[i + 1..].iter().cloned());
6847                break;
6848            } else if arg.starts_with('-') && arg.len() > 1 {
6849                for ch in arg[1..].chars() {
6850                    match ch {
6851                        'a' => append = true,
6852                        'i' => {}
6853                        _ => return None,
6854                    }
6855                }
6856                i += 1;
6857            } else {
6858                paths.push(args[i].clone());
6859                i += 1;
6860            }
6861        }
6862        Some(StreamingPipelineStage::Tee(StreamingTeeStage {
6863            append,
6864            paths,
6865        }))
6866    }
6867
6868    fn parse_streaming_column_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6869        let mut i = 0usize;
6870        while i < args.len() {
6871            let arg = args[i].as_str();
6872            if arg == "-t" {
6873                return None;
6874            }
6875            if arg == "-s" && i + 1 < args.len() {
6876                return None;
6877            }
6878            if arg.starts_with('-') && arg.len() > 1 {
6879                i += 1;
6880            } else if arg == "--" {
6881                if i + 1 != args.len() {
6882                    return None;
6883                }
6884                i += 1;
6885            } else {
6886                return None;
6887            }
6888        }
6889        Some(StreamingPipelineStage::Column(StreamingColumnStage))
6890    }
6891
6892    fn parse_streaming_rev_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6893        if args.iter().all(|arg| arg == "--") {
6894            Some(StreamingPipelineStage::Rev)
6895        } else {
6896            None
6897        }
6898    }
6899
6900    fn parse_streaming_grep_stage(args: &[String]) -> Option<StreamingPipelineStage> {
6901        let mut flags = StreamingGrepFlags {
6902            ignore_case: false,
6903            invert: false,
6904            count_only: false,
6905            show_line_numbers: false,
6906            files_only: false,
6907            word_match: false,
6908            only_matching: false,
6909            quiet: false,
6910            extended: false,
6911            fixed: false,
6912            after_context: 0,
6913            before_context: 0,
6914            max_count: None,
6915            show_filename: None,
6916        };
6917        let mut patterns = Vec::new();
6918        let mut rest = Vec::new();
6919        let mut i = 0usize;
6920        while i < args.len() {
6921            let arg = args[i].as_str();
6922            if arg == "--" {
6923                rest.extend(args[i + 1..].iter().cloned());
6924                break;
6925            }
6926            if Self::streaming_grep_arg_rejected(arg) {
6927                return None;
6928            }
6929            match Self::parse_streaming_grep_value_flag(args, i, &mut flags, &mut patterns)? {
6930                StreamingGrepStep::Advance(delta) => {
6931                    i += delta;
6932                    continue;
6933                }
6934                StreamingGrepStep::NotMatched => {}
6935            }
6936            if arg.starts_with('-') && arg.len() > 1 {
6937                Self::apply_streaming_grep_short_flags(&arg[1..], &mut flags)?;
6938                i += 1;
6939            } else {
6940                rest.push(args[i].clone());
6941                i += 1;
6942            }
6943        }
6944
6945        let (patterns, file_args) = if patterns.is_empty() {
6946            let first = rest.first()?.clone();
6947            (vec![first], rest[1..].to_vec())
6948        } else {
6949            (patterns, rest)
6950        };
6951        if !file_args.is_empty() {
6952            return None;
6953        }
6954        Some(StreamingPipelineStage::Grep(StreamingGrepStage {
6955            flags,
6956            patterns,
6957        }))
6958    }
6959
6960    fn streaming_grep_arg_rejected(arg: &str) -> bool {
6961        arg.starts_with("--include=")
6962            || arg.starts_with("--exclude=")
6963            || arg == "--color"
6964            || arg.starts_with("--color=")
6965            || arg == "-r"
6966            || arg == "-R"
6967            || arg == "--recursive"
6968    }
6969
6970    fn parse_streaming_grep_value_flag(
6971        args: &[String],
6972        i: usize,
6973        flags: &mut StreamingGrepFlags,
6974        patterns: &mut Vec<String>,
6975    ) -> Option<StreamingGrepStep> {
6976        let arg = args[i].as_str();
6977        let has_next = i + 1 < args.len();
6978        if !has_next {
6979            return Some(StreamingGrepStep::NotMatched);
6980        }
6981        match arg {
6982            "-e" => {
6983                patterns.push(args[i + 1].clone());
6984                Some(StreamingGrepStep::Advance(2))
6985            }
6986            "-f" => None,
6987            "-A" => {
6988                flags.after_context = args[i + 1].parse().ok()?;
6989                Some(StreamingGrepStep::Advance(2))
6990            }
6991            "-B" => {
6992                flags.before_context = args[i + 1].parse().ok()?;
6993                Some(StreamingGrepStep::Advance(2))
6994            }
6995            "-C" => {
6996                let n = args[i + 1].parse().ok()?;
6997                flags.before_context = n;
6998                flags.after_context = n;
6999                Some(StreamingGrepStep::Advance(2))
7000            }
7001            "-m" => {
7002                flags.max_count = args[i + 1].parse().ok();
7003                Some(StreamingGrepStep::Advance(2))
7004            }
7005            _ => Some(StreamingGrepStep::NotMatched),
7006        }
7007    }
7008
7009    fn apply_streaming_grep_short_flags(
7010        short_flags: &str,
7011        flags: &mut StreamingGrepFlags,
7012    ) -> Option<()> {
7013        for ch in short_flags.chars() {
7014            match ch {
7015                'i' => flags.ignore_case = true,
7016                'v' => flags.invert = true,
7017                'c' => flags.count_only = true,
7018                'n' => flags.show_line_numbers = true,
7019                'l' => flags.files_only = true,
7020                'E' | 'P' => flags.extended = true,
7021                'F' => flags.fixed = true,
7022                'w' => flags.word_match = true,
7023                'o' => flags.only_matching = true,
7024                'q' => flags.quiet = true,
7025                'h' => flags.show_filename = Some(false),
7026                'H' => flags.show_filename = Some(true),
7027                'z' => {}
7028                _ => return None,
7029            }
7030        }
7031        Some(())
7032    }
7033
7034    fn parse_streaming_uniq_stage(args: &[String]) -> Option<StreamingPipelineStage> {
7035        let mut flags = StreamingUniqFlags {
7036            count: false,
7037            duplicates_only: false,
7038            unique_only: false,
7039            ignore_case: false,
7040            skip_fields: 0,
7041            skip_chars: 0,
7042            compare_chars: None,
7043        };
7044        let mut i = 0usize;
7045        while i < args.len() {
7046            i = Self::apply_streaming_uniq_arg(args, i, &mut flags)?;
7047        }
7048        Some(StreamingPipelineStage::Uniq(flags))
7049    }
7050
7051    fn apply_streaming_uniq_arg(
7052        args: &[String],
7053        i: usize,
7054        flags: &mut StreamingUniqFlags,
7055    ) -> Option<usize> {
7056        let arg = args[i].as_str();
7057        if arg == "--" {
7058            return Some(i + 1);
7059        }
7060        if i + 1 < args.len() {
7061            match arg {
7062                "-f" => {
7063                    flags.skip_fields = args[i + 1].parse().ok()?;
7064                    return Some(i + 2);
7065                }
7066                "-s" => {
7067                    flags.skip_chars = args[i + 1].parse().ok()?;
7068                    return Some(i + 2);
7069                }
7070                "-w" => {
7071                    flags.compare_chars = args[i + 1].parse().ok();
7072                    return Some(i + 2);
7073                }
7074                _ => {}
7075            }
7076        }
7077        if arg.starts_with('-') && arg.len() > 1 {
7078            Self::apply_streaming_uniq_short_cluster(&arg[1..], flags)?;
7079            return Some(i + 1);
7080        }
7081        None
7082    }
7083
7084    fn apply_streaming_uniq_short_cluster(
7085        short_flags: &str,
7086        flags: &mut StreamingUniqFlags,
7087    ) -> Option<()> {
7088        for ch in short_flags.chars() {
7089            match ch {
7090                'c' => flags.count = true,
7091                'd' => flags.duplicates_only = true,
7092                'u' => flags.unique_only = true,
7093                'i' => flags.ignore_case = true,
7094                'z' => {}
7095                _ => return None,
7096            }
7097        }
7098        Some(())
7099    }
7100
7101    fn parse_streaming_cut_ranges(spec: &str) -> Vec<StreamingCutRange> {
7102        spec.split(',')
7103            .filter_map(|part| {
7104                if let Some((start, end)) = part.split_once('-') {
7105                    Some(StreamingCutRange {
7106                        start: if start.is_empty() {
7107                            None
7108                        } else {
7109                            start.parse().ok()
7110                        },
7111                        end: if end.is_empty() {
7112                            None
7113                        } else {
7114                            end.parse().ok()
7115                        },
7116                    })
7117                } else {
7118                    let n: usize = part.parse().ok()?;
7119                    Some(StreamingCutRange {
7120                        start: Some(n),
7121                        end: Some(n),
7122                    })
7123                }
7124            })
7125            .collect()
7126    }
7127
7128    fn parse_streaming_cut_stage(args: &[String]) -> Option<StreamingPipelineStage> {
7129        let mut state = StreamingCutParseState {
7130            delim: '\t',
7131            mode: None,
7132            complement: false,
7133            only_delimited: false,
7134            output_delim: None,
7135        };
7136        let mut i = 0usize;
7137        while i < args.len() {
7138            i = Self::apply_streaming_cut_arg(args, i, &mut state)?;
7139        }
7140        Some(StreamingPipelineStage::Cut(StreamingCutStage {
7141            mode: state.mode?,
7142            delim: state.delim,
7143            complement: state.complement,
7144            only_delimited: state.only_delimited,
7145            output_delim: state
7146                .output_delim
7147                .unwrap_or_else(|| state.delim.to_string()),
7148        }))
7149    }
7150
7151    fn apply_streaming_cut_arg(
7152        args: &[String],
7153        i: usize,
7154        state: &mut StreamingCutParseState,
7155    ) -> Option<usize> {
7156        let arg = args[i].as_str();
7157        if let Some(advance) = Self::streaming_cut_try_mode_flag(args, i, &mut state.mode) {
7158            return Some(advance);
7159        }
7160        if let Some(advance) = Self::streaming_cut_try_delim_flag(args, i, &mut state.delim) {
7161            return Some(advance);
7162        }
7163        match arg {
7164            "--complement" => {
7165                state.complement = true;
7166                Some(i + 1)
7167            }
7168            "-s" => {
7169                state.only_delimited = true;
7170                Some(i + 1)
7171            }
7172            "-z" | "--" => Some(i + 1),
7173            _ => {
7174                if let Some(out) = arg.strip_prefix("--output-delimiter=") {
7175                    state.output_delim = Some(out.to_string());
7176                    Some(i + 1)
7177                } else {
7178                    None
7179                }
7180            }
7181        }
7182    }
7183
7184    fn streaming_cut_try_mode_flag(
7185        args: &[String],
7186        i: usize,
7187        mode: &mut Option<StreamingCutMode>,
7188    ) -> Option<usize> {
7189        let arg = args[i].as_str();
7190        let (flag, wrap): (&str, fn(Vec<StreamingCutRange>) -> StreamingCutMode) =
7191            if arg == "-f" || arg.starts_with("-f") {
7192                ("-f", StreamingCutMode::Fields)
7193            } else if arg == "-c" || arg.starts_with("-c") {
7194                ("-c", StreamingCutMode::Chars)
7195            } else if arg == "-b" || arg.starts_with("-b") {
7196                ("-b", StreamingCutMode::Bytes)
7197            } else {
7198                return None;
7199            };
7200        if arg == flag && i + 1 < args.len() {
7201            *mode = Some(wrap(Self::parse_streaming_cut_ranges(&args[i + 1])));
7202            return Some(i + 2);
7203        }
7204        if let Some(spec) = arg.strip_prefix(flag) {
7205            if !spec.is_empty() {
7206                *mode = Some(wrap(Self::parse_streaming_cut_ranges(spec)));
7207                return Some(i + 1);
7208            }
7209        }
7210        None
7211    }
7212
7213    fn streaming_cut_try_delim_flag(args: &[String], i: usize, delim: &mut char) -> Option<usize> {
7214        let arg = args[i].as_str();
7215        if arg == "-d" && i + 1 < args.len() {
7216            *delim = args[i + 1].chars().next().unwrap_or('\t');
7217            return Some(i + 2);
7218        }
7219        if arg.starts_with("-d") && arg.len() > 2 {
7220            *delim = arg[2..].chars().next().unwrap_or('\t');
7221            return Some(i + 1);
7222        }
7223        None
7224    }
7225
7226    fn parse_streaming_tr_stage(args: &[String]) -> Option<StreamingPipelineStage> {
7227        let mut delete = false;
7228        let mut squeeze = false;
7229        let mut complement = false;
7230        let mut set_args = Vec::new();
7231        for arg in args {
7232            if arg.starts_with('-') && arg.len() > 1 {
7233                Self::apply_streaming_tr_flags(
7234                    &arg[1..],
7235                    &mut delete,
7236                    &mut squeeze,
7237                    &mut complement,
7238                )?;
7239            } else {
7240                set_args.push(arg.as_str());
7241            }
7242        }
7243        let from_chars = streaming_tr_expand_set(set_args.first()?);
7244        let to_chars = Self::streaming_tr_resolve_to_chars(&set_args, delete, squeeze)?;
7245        Some(StreamingPipelineStage::Tr(StreamingTrStage {
7246            delete,
7247            squeeze,
7248            complement,
7249            from_chars,
7250            to_chars,
7251        }))
7252    }
7253
7254    fn apply_streaming_tr_flags(
7255        flags: &str,
7256        delete: &mut bool,
7257        squeeze: &mut bool,
7258        complement: &mut bool,
7259    ) -> Option<()> {
7260        for ch in flags.chars() {
7261            match ch {
7262                'd' => *delete = true,
7263                's' => *squeeze = true,
7264                'c' | 'C' => *complement = true,
7265                't' => {}
7266                _ => return None,
7267            }
7268        }
7269        Some(())
7270    }
7271
7272    fn streaming_tr_resolve_to_chars(
7273        set_args: &[&str],
7274        delete: bool,
7275        squeeze: bool,
7276    ) -> Option<Vec<char>> {
7277        if delete {
7278            let to = if squeeze && set_args.len() >= 2 {
7279                streaming_tr_expand_set(set_args[1])
7280            } else {
7281                Vec::new()
7282            };
7283            return Some(to);
7284        }
7285        if squeeze && set_args.len() < 2 {
7286            return Some(Vec::new());
7287        }
7288        if set_args.len() < 2 {
7289            return None;
7290        }
7291        Some(streaming_tr_expand_set(set_args[1]))
7292    }
7293
7294    fn parse_streaming_wc_stage(args: &[String]) -> Option<StreamingPipelineStage> {
7295        let mut flags = StreamingWcFlags {
7296            lines: false,
7297            words: false,
7298            bytes: false,
7299            max_line_length: false,
7300        };
7301        let mut parsing_flags = true;
7302        for arg in args {
7303            if !Self::apply_streaming_wc_arg(arg, &mut flags, &mut parsing_flags)? {
7304                return None;
7305            }
7306        }
7307        if !flags.lines && !flags.words && !flags.bytes && !flags.max_line_length {
7308            flags.lines = true;
7309            flags.words = true;
7310            flags.bytes = true;
7311        }
7312        Some(StreamingPipelineStage::Wc(flags))
7313    }
7314
7315    fn apply_streaming_wc_arg(
7316        arg: &str,
7317        flags: &mut StreamingWcFlags,
7318        parsing_flags: &mut bool,
7319    ) -> Option<bool> {
7320        if *parsing_flags && arg == "--" {
7321            *parsing_flags = false;
7322            return Some(true);
7323        }
7324        if *parsing_flags && arg.starts_with('-') && arg.len() > 1 {
7325            Self::apply_streaming_wc_short_cluster(&arg[1..], flags)?;
7326            return Some(true);
7327        }
7328        Some(false)
7329    }
7330
7331    fn apply_streaming_wc_short_cluster(short: &str, flags: &mut StreamingWcFlags) -> Option<()> {
7332        for ch in short.chars() {
7333            match ch {
7334                'l' => flags.lines = true,
7335                'w' => flags.words = true,
7336                'c' | 'm' => flags.bytes = true,
7337                'L' => flags.max_line_length = true,
7338                _ => return None,
7339            }
7340        }
7341        Some(())
7342    }
7343
7344    fn set_pipestatus(&mut self, statuses: &[i32]) {
7345        let status_key = smol_str::SmolStr::from("PIPESTATUS");
7346        self.vm.state.init_indexed_array(status_key.clone());
7347        for (i, s) in statuses.iter().enumerate() {
7348            self.vm.state.set_array_element(
7349                status_key.clone(),
7350                &i.to_string(),
7351                smol_str::SmolStr::from(s.to_string()),
7352            );
7353        }
7354    }
7355
7356    fn open_streaming_file_reader(
7357        &mut self,
7358        path: &str,
7359        cmd_name: &str,
7360    ) -> Result<Box<dyn Read>, ()> {
7361        let resolved = self.resolve_cwd_path(path);
7362        match Self::open_streaming_file_reader_in_fs(&mut self.fs, &resolved) {
7363            Ok(reader) => Ok(reader),
7364            Err(err) => {
7365                let msg =
7366                    format!("wasmsh: {cmd_name}: failed to open stdin source {resolved}: {err}\n");
7367                self.write_stderr(msg.as_bytes());
7368                self.vm.state.last_status = 1;
7369                Err(())
7370            }
7371        }
7372    }
7373
7374    fn open_streaming_file_reader_in_fs(
7375        fs: &mut BackendFs,
7376        resolved: &str,
7377    ) -> Result<Box<dyn Read>, String> {
7378        let handle = fs
7379            .open(resolved, OpenOptions::read())
7380            .map_err(|err| err.to_string())?;
7381        let reader_result = fs.stream_file(handle).map_err(|err| err.to_string());
7382        fs.close(handle);
7383        reader_result
7384    }
7385
7386    fn execute_inner_capture_stdout(&mut self, input: &str) -> Vec<u8> {
7387        let events = self.execute_isolated_input_events(input, None);
7388        let mut stdout = Vec::new();
7389        for event in events {
7390            match event {
7391                WorkerEvent::Stdout(data) => stdout.extend_from_slice(&data),
7392                WorkerEvent::Stderr(data) => self.write_stderr(&data),
7393                WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
7394                    convert_diag_level(level),
7395                    wasmsh_vm::DiagCategory::Runtime,
7396                    msg,
7397                ),
7398                _ => {}
7399            }
7400        }
7401        stdout
7402    }
7403
7404    fn execute_isolated_input_events(
7405        &mut self,
7406        input: &str,
7407        pending_input: Option<InputTarget>,
7408    ) -> Vec<WorkerEvent> {
7409        let saved_state = self.vm.state.clone();
7410        let saved_functions = self.functions.clone();
7411        let saved_aliases = self.aliases.clone();
7412        let saved_exec = self.exec.clone();
7413        let saved_exec_io = self.current_exec_io.take();
7414        let saved_stdout = std::mem::take(&mut self.vm.stdout);
7415        let saved_stderr = std::mem::take(&mut self.vm.stderr);
7416        let saved_diagnostics = std::mem::take(&mut self.vm.diagnostics);
7417        let saved_output_bytes = self.vm.output_bytes;
7418        let saved_proc_subst_out_scopes = std::mem::take(&mut self.proc_subst_out_scopes);
7419        let saved_proc_subst_in_scopes = std::mem::take(&mut self.proc_subst_in_scopes);
7420
7421        self.current_exec_io = pending_input.map(|target| {
7422            let mut exec_io = ExecIo::default();
7423            exec_io.fds_mut().set_input(target);
7424            exec_io
7425        });
7426        let (mut inner_events, captured) = self.with_output_capture(true, true, |runtime| {
7427            runtime.with_nested_shell_scope(|nested| nested.execute_input_inner(input))
7428        });
7429        let inner_resource_exhausted = self.exec.resource_exhausted;
7430        let inner_diagnostics = self
7431            .vm
7432            .diagnostics
7433            .drain(..)
7434            .map(|diag| {
7435                WorkerEvent::Diagnostic(Self::to_protocol_diag_level(diag.level), diag.message)
7436            })
7437            .collect::<Vec<_>>();
7438        self.clear_pending_input();
7439        for scope in self.proc_subst_out_scopes.drain(..) {
7440            for sink in scope {
7441                let _ = self.fs.remove_file(&sink.path);
7442            }
7443        }
7444        for scope in self.proc_subst_in_scopes.drain(..) {
7445            for sink in scope {
7446                let _ = self.fs.remove_file(&sink.path);
7447            }
7448        }
7449
7450        self.vm.state = saved_state;
7451        self.functions = saved_functions;
7452        self.aliases = saved_aliases;
7453        self.exec = saved_exec;
7454        self.exec.resource_exhausted |= inner_resource_exhausted;
7455        self.current_exec_io = saved_exec_io;
7456        self.vm.stdout = saved_stdout;
7457        self.vm.stderr = saved_stderr;
7458        self.vm.diagnostics = saved_diagnostics;
7459        self.vm.output_bytes = saved_output_bytes;
7460        self.vm.budget.visible_output_bytes = saved_output_bytes;
7461        self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
7462        self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
7463
7464        let mut events = Self::seed_isolated_events_from_capture(captured);
7465        Self::merge_isolated_inner_events(&mut events, inner_events.drain(..));
7466        events.extend(inner_diagnostics);
7467        events
7468    }
7469
7470    fn seed_isolated_events_from_capture(capture: CapturedOutput) -> Vec<WorkerEvent> {
7471        let mut events = Vec::new();
7472        if !capture.stdout.is_empty() {
7473            events.push(WorkerEvent::Stdout(capture.stdout));
7474        }
7475        if !capture.stderr.is_empty() {
7476            events.push(WorkerEvent::Stderr(capture.stderr));
7477        }
7478        events
7479    }
7480
7481    fn merge_isolated_inner_events(
7482        events: &mut Vec<WorkerEvent>,
7483        inner_events: impl IntoIterator<Item = WorkerEvent>,
7484    ) {
7485        for event in inner_events {
7486            match &event {
7487                WorkerEvent::Stdout(_)
7488                    if !events.iter().any(|e| matches!(e, WorkerEvent::Stdout(_))) =>
7489                {
7490                    events.push(event);
7491                }
7492                WorkerEvent::Stderr(_)
7493                    if !events.iter().any(|e| matches!(e, WorkerEvent::Stderr(_))) =>
7494                {
7495                    events.push(event);
7496                }
7497                WorkerEvent::Stdout(_) | WorkerEvent::Stderr(_) => {}
7498                _ => events.push(event),
7499            }
7500        }
7501    }
7502
7503    fn execute_isolated_scheduled_pipeline_events_from_reader(
7504        &mut self,
7505        pipeline: &HirPipeline,
7506        reader: Box<dyn Read>,
7507    ) -> Vec<WorkerEvent> {
7508        let saved_state = self.vm.state.clone();
7509        let saved_functions = self.functions.clone();
7510        let saved_aliases = self.aliases.clone();
7511        let saved_exec = self.exec.clone();
7512        let saved_exec_io = self.current_exec_io.take();
7513        let saved_stdout = std::mem::take(&mut self.vm.stdout);
7514        let saved_stderr = std::mem::take(&mut self.vm.stderr);
7515        let saved_diagnostics = std::mem::take(&mut self.vm.diagnostics);
7516        let saved_output_bytes = self.vm.output_bytes;
7517        let saved_proc_subst_out_scopes = std::mem::take(&mut self.proc_subst_out_scopes);
7518        let saved_proc_subst_in_scopes = std::mem::take(&mut self.proc_subst_in_scopes);
7519
7520        self.current_exec_io = None;
7521        self.proc_subst_out_scopes.clear();
7522        self.proc_subst_in_scopes.clear();
7523        self.exec.recursion_depth += 1;
7524        if let Err(reason) = self
7525            .vm
7526            .budget
7527            .enter_recursion(self.vm.limits.recursion_limit)
7528        {
7529            self.exec.recursion_depth -= 1;
7530            self.vm.state = saved_state;
7531            self.functions = saved_functions;
7532            self.aliases = saved_aliases;
7533            self.exec = saved_exec;
7534            self.current_exec_io = saved_exec_io;
7535            self.vm.stdout = saved_stdout;
7536            self.vm.stderr = saved_stderr;
7537            self.vm.diagnostics = saved_diagnostics;
7538            self.vm.output_bytes = saved_output_bytes;
7539            self.vm.budget.visible_output_bytes = saved_output_bytes;
7540            self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
7541            self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
7542            self.mark_budget_exhaustion(reason);
7543            return vec![WorkerEvent::Stderr(
7544                b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
7545            )];
7546        }
7547
7548        let ((), captured) = self.with_output_capture(true, true, |runtime| {
7549            runtime.with_nested_shell_scope(|nested| {
7550                nested.execute_scheduled_pipeline_with_source_reader(
7551                    &pipeline.commands,
7552                    pipeline,
7553                    Some(reader),
7554                );
7555            });
7556        });
7557        self.exec.recursion_depth -= 1;
7558        self.vm.budget.exit_recursion();
7559        let inner_resource_exhausted = self.exec.resource_exhausted;
7560        let inner_diagnostics = self
7561            .vm
7562            .diagnostics
7563            .drain(..)
7564            .map(|diag| {
7565                WorkerEvent::Diagnostic(Self::to_protocol_diag_level(diag.level), diag.message)
7566            })
7567            .collect::<Vec<_>>();
7568        self.clear_pending_input();
7569        let pending_scopes: Vec<Vec<PendingProcessSubstOut>> =
7570            self.proc_subst_out_scopes.drain(..).collect();
7571        for scope in pending_scopes {
7572            for sink in scope {
7573                self.flush_process_subst_out(sink);
7574            }
7575        }
7576        let pending_in_scopes: Vec<Vec<PendingProcessSubstIn>> =
7577            self.proc_subst_in_scopes.drain(..).collect();
7578        for scope in pending_in_scopes {
7579            self.flush_process_subst_in_scope(scope);
7580        }
7581
7582        self.vm.state = saved_state;
7583        self.functions = saved_functions;
7584        self.aliases = saved_aliases;
7585        self.exec = saved_exec;
7586        self.exec.resource_exhausted |= inner_resource_exhausted;
7587        self.current_exec_io = saved_exec_io;
7588        self.vm.stdout = saved_stdout;
7589        self.vm.stderr = saved_stderr;
7590        self.vm.diagnostics = saved_diagnostics;
7591        self.vm.output_bytes = saved_output_bytes;
7592        self.vm.budget.visible_output_bytes = saved_output_bytes;
7593        self.proc_subst_out_scopes = saved_proc_subst_out_scopes;
7594        self.proc_subst_in_scopes = saved_proc_subst_in_scopes;
7595
7596        let mut events = Vec::new();
7597        if !captured.stdout.is_empty() {
7598            events.push(WorkerEvent::Stdout(captured.stdout));
7599        }
7600        if !captured.stderr.is_empty() {
7601            events.push(WorkerEvent::Stderr(captured.stderr));
7602        }
7603        events.extend(inner_diagnostics);
7604        events
7605    }
7606
7607    /// Execute a command substitution and return the trimmed output.
7608    fn execute_subst(&mut self, inner: &str) -> smol_str::SmolStr {
7609        let stdout = self.execute_inner_capture_stdout(inner);
7610        let result = String::from_utf8_lossy(&stdout).to_string();
7611        smol_str::SmolStr::from(result.trim_end_matches('\n'))
7612    }
7613
7614    fn word_parts_require_runtime_expansion(parts: &[WordPart]) -> bool {
7615        parts.iter().any(|part| match part {
7616            WordPart::Literal(_) | WordPart::SingleQuoted(_) => false,
7617            WordPart::DoubleQuoted(inner) => Self::word_parts_require_runtime_expansion(inner),
7618            WordPart::Parameter(_)
7619            | WordPart::Arithmetic(_)
7620            | WordPart::CommandSubstitution(_)
7621            | WordPart::ProcessSubstIn(_)
7622            | WordPart::ProcessSubstOut(_)
7623            | _ => true,
7624        })
7625    }
7626
7627    fn command_requires_runtime_expansion(cmd: &HirCommand) -> bool {
7628        let HirCommand::Exec(exec) = cmd else {
7629            return false;
7630        };
7631        exec.argv
7632            .iter()
7633            .any(|word| Self::word_parts_require_runtime_expansion(&word.parts))
7634    }
7635
7636    fn command_needs_full_single_stage_execution(&self, cmd: &HirCommand) -> bool {
7637        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
7638            return true;
7639        }
7640        let HirCommand::Exec(exec) = cmd else {
7641            return false;
7642        };
7643        exec.argv.iter().any(Self::word_has_brace_or_glob_literal)
7644    }
7645
7646    fn word_has_brace_or_glob_literal(word: &Word) -> bool {
7647        word.parts
7648            .iter()
7649            .any(Self::word_part_has_brace_or_glob_literal)
7650    }
7651
7652    fn word_part_has_brace_or_glob_literal(part: &WordPart) -> bool {
7653        match part {
7654            WordPart::Literal(text) | WordPart::SingleQuoted(text) | WordPart::Parameter(text) => {
7655                Self::text_has_brace_or_glob_literal(text)
7656            }
7657            WordPart::DoubleQuoted(parts) => {
7658                parts.iter().any(Self::word_part_has_brace_or_glob_literal)
7659            }
7660            WordPart::Arithmetic(_) => false,
7661            WordPart::CommandSubstitution(_)
7662            | WordPart::ProcessSubstIn(_)
7663            | WordPart::ProcessSubstOut(_)
7664            | _ => true,
7665        }
7666    }
7667
7668    fn text_has_brace_or_glob_literal(text: &str) -> bool {
7669        text.contains('{')
7670            || text.contains('}')
7671            || text.contains('*')
7672            || text.contains('?')
7673            || text.contains('[')
7674    }
7675
7676    fn parse_single_pipeline_input(input: &str) -> Option<HirPipeline> {
7677        let ast = wasmsh_parse::parse(input).ok()?;
7678        let hir = wasmsh_hir::lower(&ast);
7679        let cc = hir.items.first()?;
7680        if hir.items.len() != 1 || cc.list.len() != 1 {
7681            return None;
7682        }
7683        let and_or = cc.list.first()?;
7684        if !and_or.rest.is_empty() {
7685            return None;
7686        }
7687        Some(and_or.first.clone())
7688    }
7689
7690    /// Counter for generating unique temp file paths for process substitution.
7691    fn next_proc_subst_id() -> u64 {
7692        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
7693        COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
7694    }
7695
7696    fn next_pending_input_id() -> u64 {
7697        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
7698        COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
7699    }
7700
7701    fn set_pending_input_bytes(&mut self, data: Vec<u8>) {
7702        self.current_exec_io
7703            .get_or_insert_with(ExecIo::default)
7704            .fds_mut()
7705            .set_input(InputTarget::Bytes(data));
7706    }
7707
7708    fn set_pending_input_file(&mut self, path: String, remove_after_read: bool) {
7709        self.current_exec_io
7710            .get_or_insert_with(ExecIo::default)
7711            .fds_mut()
7712            .set_input(InputTarget::File {
7713                path,
7714                remove_after_read,
7715            });
7716    }
7717
7718    fn clear_pending_input(&mut self) {
7719        let Some(exec_io) = self.current_exec_io.as_mut() else {
7720            return;
7721        };
7722        if let InputTarget::File {
7723            path,
7724            remove_after_read: true,
7725        } = exec_io.take_stdin()
7726        {
7727            let _ = self.fs.remove_file(&path);
7728        }
7729    }
7730
7731    fn take_pending_input_reader(&mut self, cmd_name: &str) -> Result<Option<Box<dyn Read>>, ()> {
7732        let Some(exec_io) = self.current_exec_io.as_mut() else {
7733            return Ok(None);
7734        };
7735        match exec_io.take_stdin() {
7736            InputTarget::Inherit | InputTarget::Closed => Ok(None),
7737            InputTarget::Bytes(data) => Ok(Some(Box::new(Cursor::new(data)))),
7738            InputTarget::File {
7739                path,
7740                remove_after_read,
7741            } => {
7742                let reader_result = self.open_streaming_file_reader(&path, cmd_name);
7743                if remove_after_read {
7744                    let _ = self.fs.remove_file(&path);
7745                }
7746                reader_result.map(Some)
7747            }
7748            InputTarget::Pipe(pipe) => Ok(Some(Box::new(PipeReader::new(pipe)))),
7749        }
7750    }
7751
7752    fn take_builtin_stdin(
7753        &mut self,
7754        cmd_name: &str,
7755    ) -> Result<Option<wasmsh_builtins::BuiltinStdin<'static>>, ()> {
7756        let reader = self.take_pending_input_reader(cmd_name)?;
7757        Ok(reader.map(wasmsh_builtins::BuiltinStdin::from_reader))
7758    }
7759
7760    fn take_util_stdin(
7761        &mut self,
7762        cmd_name: &str,
7763    ) -> Result<Option<wasmsh_utils::UtilStdin<'static>>, ()> {
7764        let reader = self.take_pending_input_reader(cmd_name)?;
7765        Ok(reader.map(wasmsh_utils::UtilStdin::from_reader))
7766    }
7767
7768    fn take_external_stdin(
7769        &mut self,
7770        cmd_name: &str,
7771    ) -> Result<Option<ExternalCommandStdin<'static>>, ()> {
7772        let reader = self.take_pending_input_reader(cmd_name)?;
7773        Ok(reader.map(ExternalCommandStdin::from_reader))
7774    }
7775
7776    fn can_use_isolated_process_subst_runtime(&self) -> bool {
7777        self.external_handler.is_none() && self.network.is_none()
7778    }
7779
7780    fn clone_for_isolated_process_subst(&self) -> Option<Self> {
7781        if !self.can_use_isolated_process_subst_runtime() {
7782            return None;
7783        }
7784        let mut exec = ExecState::new();
7785        exec.recursion_depth = self.exec.recursion_depth;
7786        Some(Self {
7787            config: self.config.clone(),
7788            vm: Vm::with_limits(self.vm.state.clone(), self.vm.limits.clone()),
7789            fs: self.fs.clone(),
7790            utils: UtilRegistry::new(),
7791            builtins: wasmsh_builtins::BuiltinRegistry::new(),
7792            initialized: self.initialized,
7793            current_exec_io: None,
7794            proc_subst_out_scopes: Vec::new(),
7795            proc_subst_in_scopes: Vec::new(),
7796            functions: self.functions.clone(),
7797            exec,
7798            aliases: self.aliases.clone(),
7799            external_handler: None,
7800            network: None,
7801            active_run: None,
7802            pending_signals: VecDeque::new(),
7803        })
7804    }
7805
7806    fn build_live_process_subst_pipeline(
7807        &mut self,
7808        pipeline: &HirPipeline,
7809        source_pipe: Option<Rc<RefCell<PipeBuffer>>>,
7810    ) -> Option<(
7811        Vec<StreamingPipeProcess<'static>>,
7812        Vec<Rc<RefCell<Vec<u8>>>>,
7813        Vec<bool>,
7814        Rc<RefCell<PipeBuffer>>,
7815        Vec<Rc<RefCell<i32>>>,
7816    )> {
7817        let stages: Vec<StreamingPipelineStage> = pipeline
7818            .commands
7819            .iter()
7820            .enumerate()
7821            .map(|(idx, cmd)| self.compile_pipeline_stage(cmd, idx == 0 && source_pipe.is_none()))
7822            .collect();
7823        let stage_statuses: Vec<Rc<RefCell<i32>>> = stages
7824            .iter()
7825            .map(|stage| {
7826                Rc::new(RefCell::new(i32::from(matches!(
7827                    stage,
7828                    StreamingPipelineStage::Grep(_)
7829                ))))
7830            })
7831            .collect();
7832        let stage_stderr: Vec<Rc<RefCell<Vec<u8>>>> = stages
7833            .iter()
7834            .map(|_| Rc::new(RefCell::new(Vec::new())))
7835            .collect();
7836        let stage_pipe_stderr = vec![false; stages.len()];
7837        let output_pipes: Vec<Rc<RefCell<PipeBuffer>>> = (0..stages.len())
7838            .map(|_| Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY))))
7839            .collect();
7840        let mut processes = Vec::new();
7841
7842        if let Some(source_pipe) = source_pipe {
7843            match &stages[0] {
7844                StreamingPipelineStage::Tee(stage) => {
7845                    let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
7846                    processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
7847                        reader,
7848                        output_pipes[0].clone(),
7849                        &mut self.fs,
7850                        self.vm.state.cwd.as_str(),
7851                        stage,
7852                        stage_stderr[0].clone(),
7853                        stage_statuses[0].clone(),
7854                        false,
7855                    )));
7856                }
7857                StreamingPipelineStage::BufferedCommand(argv) => {
7858                    processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
7859                        Some(source_pipe),
7860                        output_pipes[0].clone(),
7861                        argv.clone(),
7862                        false,
7863                        stage_stderr[0].clone(),
7864                        stage_statuses[0].clone(),
7865                    )));
7866                }
7867                _ => {
7868                    let reader = Box::new(PipeReader::new(source_pipe)) as Box<dyn Read>;
7869                    let stage_reader =
7870                        Self::wrap_non_tee_streaming_stage(reader, &stages[0], 0, &stage_statuses)?;
7871                    processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
7872                        stage_reader,
7873                        output_pipes[0].clone(),
7874                        stage_stderr[0].clone(),
7875                        stage_statuses[0].clone(),
7876                        "process-subst",
7877                        false,
7878                    )));
7879                }
7880            }
7881        } else {
7882            match &stages[0] {
7883                StreamingPipelineStage::Literal(data) => {
7884                    let reader: Box<dyn Read> = Box::new(Cursor::new(data.clone()));
7885                    processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
7886                        reader,
7887                        output_pipes[0].clone(),
7888                        stage_stderr[0].clone(),
7889                        stage_statuses[0].clone(),
7890                        "process-subst",
7891                        false,
7892                    )));
7893                }
7894                StreamingPipelineStage::File(path) => {
7895                    let resolved = self.resolve_cwd_path(path);
7896                    let reader = self.open_streaming_file_reader(&resolved, "cat").ok()?;
7897                    processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
7898                        reader,
7899                        output_pipes[0].clone(),
7900                        stage_stderr[0].clone(),
7901                        stage_statuses[0].clone(),
7902                        "process-subst",
7903                        false,
7904                    )));
7905                }
7906                StreamingPipelineStage::Yes { line } => {
7907                    let reader: Box<dyn Read> =
7908                        Box::new(YesStreamReader::new(line.clone(), STREAMING_YES_MAX_LINES));
7909                    processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
7910                        reader,
7911                        output_pipes[0].clone(),
7912                        stage_stderr[0].clone(),
7913                        stage_statuses[0].clone(),
7914                        "process-subst",
7915                        false,
7916                    )));
7917                }
7918                StreamingPipelineStage::BufferedCommand(argv) => {
7919                    processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
7920                        None,
7921                        output_pipes[0].clone(),
7922                        argv.clone(),
7923                        false,
7924                        stage_stderr[0].clone(),
7925                        stage_statuses[0].clone(),
7926                    )));
7927                }
7928                _ => return None,
7929            }
7930        }
7931
7932        for idx in 1..stages.len() {
7933            match &stages[idx] {
7934                StreamingPipelineStage::Head(mode) => {
7935                    processes.push(StreamingPipeProcess::Head(HeadPipeProcess::new(
7936                        output_pipes[idx - 1].clone(),
7937                        output_pipes[idx].clone(),
7938                        *mode,
7939                    )));
7940                }
7941                StreamingPipelineStage::Tee(stage) => {
7942                    let reader =
7943                        Box::new(PipeReader::new(output_pipes[idx - 1].clone())) as Box<dyn Read>;
7944                    processes.push(StreamingPipeProcess::Tee(TeePipeProcess::new(
7945                        reader,
7946                        output_pipes[idx].clone(),
7947                        &mut self.fs,
7948                        self.vm.state.cwd.as_str(),
7949                        stage,
7950                        stage_stderr[idx].clone(),
7951                        stage_statuses[idx].clone(),
7952                        false,
7953                    )));
7954                }
7955                StreamingPipelineStage::BufferedCommand(argv) => {
7956                    processes.push(StreamingPipeProcess::Buffered(BufferedPipeProcess::new(
7957                        Some(output_pipes[idx - 1].clone()),
7958                        output_pipes[idx].clone(),
7959                        argv.clone(),
7960                        false,
7961                        stage_stderr[idx].clone(),
7962                        stage_statuses[idx].clone(),
7963                    )));
7964                }
7965                _ => {
7966                    let reader =
7967                        Box::new(PipeReader::new(output_pipes[idx - 1].clone())) as Box<dyn Read>;
7968                    let stage_reader = Self::wrap_non_tee_streaming_stage(
7969                        reader,
7970                        &stages[idx],
7971                        idx,
7972                        &stage_statuses,
7973                    )?;
7974                    processes.push(StreamingPipeProcess::Read(PipeReadProcess::new(
7975                        stage_reader,
7976                        output_pipes[idx].clone(),
7977                        stage_stderr[idx].clone(),
7978                        stage_statuses[idx].clone(),
7979                        "process-subst",
7980                        false,
7981                    )));
7982                }
7983            }
7984        }
7985
7986        Some((
7987            processes,
7988            stage_stderr,
7989            stage_pipe_stderr,
7990            output_pipes.last().cloned()?,
7991            stage_statuses,
7992        ))
7993    }
7994
7995    fn try_build_live_process_subst_in_reader(
7996        &mut self,
7997        inner: &str,
7998    ) -> Option<(
7999        Box<dyn Read>,
8000        Rc<RefCell<Vec<u8>>>,
8001        Rc<RefCell<Vec<wasmsh_vm::DiagnosticEvent>>>,
8002    )> {
8003        let pipeline = Self::parse_single_pipeline_input(inner)?;
8004        let requires_runtime = pipeline.commands.iter().enumerate().any(|(idx, cmd)| {
8005            matches!(
8006                self.compile_pipeline_stage(cmd, idx == 0),
8007                StreamingPipelineStage::BufferedCommand(_)
8008            )
8009        });
8010        let mut isolated_runtime = if requires_runtime {
8011            self.clone_for_isolated_process_subst().map(Box::new)
8012        } else {
8013            None
8014        };
8015        let (processes, stage_stderr, stage_pipe_stderr, final_pipe, _) =
8016            if let Some(runtime) = isolated_runtime.as_mut() {
8017                runtime.build_live_process_subst_pipeline(&pipeline, None)?
8018            } else {
8019                if requires_runtime {
8020                    return None;
8021                }
8022                self.build_live_process_subst_pipeline(&pipeline, None)?
8023            };
8024
8025        let flushed_stderr = Rc::new(RefCell::new(Vec::new()));
8026        let flushed_diagnostics = Rc::new(RefCell::new(Vec::new()));
8027        let reader = LiveProcessSubstInReader {
8028            isolated_runtime,
8029            processes,
8030            finished: vec![false; stage_stderr.len()],
8031            final_pipe,
8032            stage_stderr,
8033            stage_pipe_stderr,
8034            flushed_stderr: flushed_stderr.clone(),
8035            flushed_diagnostics: flushed_diagnostics.clone(),
8036            done: false,
8037        };
8038        Some((Box::new(reader), flushed_stderr, flushed_diagnostics))
8039    }
8040
8041    /// Execute `<(cmd)` by registering a command-scoped readable path.
8042    fn execute_process_subst_in(&mut self, inner: &str) -> smol_str::SmolStr {
8043        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
8044        if self.proc_subst_in_scopes.is_empty() {
8045            self.proc_subst_in_scopes.push(Vec::new());
8046        }
8047
8048        if let Some((reader, stderr, diagnostics)) =
8049            self.try_build_live_process_subst_in_reader(inner)
8050        {
8051            if self.fs.install_stream_reader(&path, reader).is_ok() {
8052                self.proc_subst_in_scopes
8053                    .last_mut()
8054                    .expect("process substitution input scope stack is empty")
8055                    .push(PendingProcessSubstIn {
8056                        path: path.clone(),
8057                        stderr: Some(stderr),
8058                        diagnostics: Some(diagnostics),
8059                    });
8060                return smol_str::SmolStr::from(path);
8061            }
8062        }
8063
8064        let output = self.execute_inner_capture_stdout(inner);
8065        if let Ok(h) = self.fs.open(&path, OpenOptions::write()) {
8066            let _ = self.fs.write_file(h, &output);
8067            self.fs.close(h);
8068        }
8069        self.proc_subst_in_scopes
8070            .last_mut()
8071            .expect("process substitution input scope stack is empty")
8072            .push(PendingProcessSubstIn {
8073                path: path.clone(),
8074                stderr: None,
8075                diagnostics: None,
8076            });
8077        smol_str::SmolStr::from(path)
8078    }
8079
8080    fn try_build_live_process_subst_runner(
8081        &mut self,
8082        inner: &str,
8083    ) -> Option<LiveProcessSubstRunner> {
8084        let pipeline = Self::parse_single_pipeline_input(inner)?;
8085        let source_pipe = Rc::new(RefCell::new(PipeBuffer::new(PIPEBUFFER_STREAMING_CAPACITY)));
8086        let mut isolated_runtime = self.clone_for_isolated_process_subst();
8087        let (processes, stage_stderr, stage_pipe_stderr, final_pipe, _) =
8088            if let Some(runtime) = isolated_runtime.as_mut() {
8089                runtime.build_live_process_subst_pipeline(&pipeline, Some(source_pipe.clone()))?
8090            } else {
8091                self.build_live_process_subst_pipeline(&pipeline, Some(source_pipe.clone()))?
8092            };
8093
8094        Some(LiveProcessSubstRunner {
8095            isolated_runtime: isolated_runtime.map(Box::new),
8096            source_pipe,
8097            processes,
8098            finished: vec![false; stage_stderr.len()],
8099            final_pipe,
8100            stage_stderr,
8101            stage_pipe_stderr,
8102            captured_stdout: Vec::new(),
8103            captured_stderr: Vec::new(),
8104            captured_diagnostics: Vec::new(),
8105            done: false,
8106            synced_steps: self.vm.steps,
8107        })
8108    }
8109
8110    fn register_process_subst_out(&mut self, inner: &str) -> String {
8111        if self.proc_subst_out_scopes.is_empty() {
8112            self.proc_subst_out_scopes.push(Vec::new());
8113        }
8114        let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
8115        let mode = if let Some(runner) = self.try_build_live_process_subst_runner(inner) {
8116            PendingProcessSubstOutMode::Live { runner }
8117        } else {
8118            PendingProcessSubstOutMode::Buffered { data: Vec::new() }
8119        };
8120        self.proc_subst_out_scopes
8121            .last_mut()
8122            .expect("process substitution scope stack is empty")
8123            .push(PendingProcessSubstOut {
8124                path: path.clone(),
8125                inner: inner.to_string(),
8126                mode,
8127            });
8128        path
8129    }
8130
8131    fn flush_process_subst_out_scope(&mut self, scope: Vec<PendingProcessSubstOut>) {
8132        for sink in scope {
8133            self.flush_process_subst_out(sink);
8134        }
8135    }
8136
8137    fn flush_process_subst_in_scope(&mut self, scope: Vec<PendingProcessSubstIn>) {
8138        for sink in scope {
8139            if let Some(stderr) = sink.stderr {
8140                let data = stderr.borrow();
8141                if !data.is_empty() {
8142                    self.write_stderr(&data);
8143                }
8144            }
8145            if let Some(diagnostics) = sink.diagnostics {
8146                let mut diagnostics = diagnostics.borrow_mut();
8147                for event in diagnostics.drain(..) {
8148                    self.vm
8149                        .emit_diagnostic(event.level, event.category, event.message);
8150                }
8151            }
8152            let _ = self.fs.remove_file(&sink.path);
8153        }
8154    }
8155
8156    fn flush_process_subst_out(&mut self, sink: PendingProcessSubstOut) {
8157        let saved_status = self.vm.state.last_status;
8158        match sink.mode {
8159            PendingProcessSubstOutMode::Buffered { data } => {
8160                self.flush_buffered_process_subst_out(&sink.inner, data);
8161            }
8162            PendingProcessSubstOutMode::Live { runner } => {
8163                self.flush_live_process_subst_out(runner);
8164            }
8165        }
8166        self.vm.state.last_status = saved_status;
8167    }
8168
8169    fn flush_buffered_process_subst_out(&mut self, inner: &str, data: Vec<u8>) {
8170        let events = if let Some(pipeline) = Self::parse_single_pipeline_input(inner) {
8171            self.execute_isolated_scheduled_pipeline_events_from_reader(
8172                &pipeline,
8173                Box::new(Cursor::new(data.clone())),
8174            )
8175        } else {
8176            self.execute_isolated_input_events(inner, Some(InputTarget::Bytes(data)))
8177        };
8178        for event in events {
8179            self.apply_isolated_flush_event(event);
8180        }
8181    }
8182
8183    fn apply_isolated_flush_event(&mut self, event: WorkerEvent) {
8184        match event {
8185            WorkerEvent::Stdout(data) => self.write_stdout(&data),
8186            WorkerEvent::Stderr(data) => self.write_stderr(&data),
8187            WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
8188                convert_diag_level(level),
8189                wasmsh_vm::DiagCategory::Runtime,
8190                msg,
8191            ),
8192            _ => {}
8193        }
8194    }
8195
8196    fn flush_live_process_subst_out(&mut self, mut runner: LiveProcessSubstRunner) {
8197        if runner.isolated_runtime.is_some() {
8198            runner.finish_with_parent(self);
8199        } else {
8200            runner.finish();
8201        }
8202        if !runner.captured_stdout.is_empty() {
8203            self.write_stdout(&runner.captured_stdout);
8204        }
8205        if !runner.captured_stderr.is_empty() {
8206            self.write_stderr(&runner.captured_stderr);
8207        }
8208        for diag in runner.captured_diagnostics {
8209            self.vm
8210                .emit_diagnostic(diag.level, diag.category, diag.message);
8211        }
8212    }
8213
8214    /// Execute `>(cmd)` by creating a writable temp path and scheduling the
8215    /// consumer command to run once the enclosing command finishes writing to it.
8216    fn execute_process_subst_out(&mut self, inner: &str) -> smol_str::SmolStr {
8217        smol_str::SmolStr::from(self.register_process_subst_out(inner))
8218    }
8219
8220    /// Resolve command substitutions in a list of words by executing them.
8221    fn resolve_command_subst(&mut self, words: &[Word]) -> Vec<Word> {
8222        words
8223            .iter()
8224            .map(|w| {
8225                let parts: Vec<WordPart> = w
8226                    .parts
8227                    .iter()
8228                    .map(|p| match p {
8229                        WordPart::CommandSubstitution(inner) => {
8230                            WordPart::Literal(self.execute_subst(inner))
8231                        }
8232                        WordPart::ProcessSubstIn(inner) => {
8233                            WordPart::Literal(self.execute_process_subst_in(inner))
8234                        }
8235                        WordPart::ProcessSubstOut(inner) => {
8236                            WordPart::Literal(self.execute_process_subst_out(inner))
8237                        }
8238                        WordPart::DoubleQuoted(inner_parts) => {
8239                            let resolved: Vec<WordPart> = inner_parts
8240                                .iter()
8241                                .map(|ip| match ip {
8242                                    WordPart::CommandSubstitution(inner) => {
8243                                        WordPart::Literal(self.execute_subst(inner))
8244                                    }
8245                                    WordPart::ProcessSubstIn(inner) => {
8246                                        WordPart::Literal(self.execute_process_subst_in(inner))
8247                                    }
8248                                    WordPart::ProcessSubstOut(inner) => {
8249                                        WordPart::Literal(self.execute_process_subst_out(inner))
8250                                    }
8251                                    other => other.clone(),
8252                                })
8253                                .collect();
8254                            WordPart::DoubleQuoted(resolved)
8255                        }
8256                        other => other.clone(),
8257                    })
8258                    .collect();
8259                Word {
8260                    parts,
8261                    span: w.span,
8262                }
8263            })
8264            .collect()
8265    }
8266
8267    fn execute_command(&mut self, cmd: &HirCommand) {
8268        self.run_debug_trap_if_needed();
8269        self.proc_subst_out_scopes.push(Vec::new());
8270        self.proc_subst_in_scopes.push(Vec::new());
8271        self.execute_command_body(cmd);
8272        let in_scope = self
8273            .proc_subst_in_scopes
8274            .pop()
8275            .expect("process substitution input scope stack underflow");
8276        let scope = self
8277            .proc_subst_out_scopes
8278            .pop()
8279            .expect("process substitution scope stack underflow");
8280        self.flush_process_subst_out_scope(scope);
8281        self.flush_process_subst_in_scope(in_scope);
8282    }
8283
8284    fn execute_command_body(&mut self, cmd: &HirCommand) {
8285        match cmd {
8286            HirCommand::Exec(exec) => self.execute_exec(exec),
8287            HirCommand::Assign(assign) => {
8288                for a in &assign.assignments {
8289                    self.execute_assignment(&a.name, a.value.as_ref());
8290                }
8291                let stdout_before = self.current_stdout_len();
8292                self.apply_redirections(&assign.redirections, stdout_before);
8293                self.vm.state.last_status = 0;
8294            }
8295            HirCommand::If(if_cmd) => self.execute_if(if_cmd),
8296            HirCommand::While(loop_cmd) => self.execute_while_loop(loop_cmd),
8297            HirCommand::Until(loop_cmd) => self.execute_until_loop(loop_cmd),
8298            HirCommand::For(for_cmd) => self.execute_for_loop(for_cmd),
8299            HirCommand::Group(block) => self.execute_body(&block.body),
8300            HirCommand::Subshell(block) => {
8301                self.vm.state.env.push_scope();
8302                self.execute_body(&block.body);
8303                self.vm.state.env.pop_scope();
8304            }
8305            HirCommand::Case(case_cmd) => self.execute_case(case_cmd),
8306            HirCommand::FunctionDef(fd) => {
8307                self.functions
8308                    .insert(fd.name.to_string(), (*fd.body).clone());
8309                self.vm.state.last_status = 0;
8310            }
8311            HirCommand::RedirectOnly(ro) => {
8312                let stdout_before = self.current_stdout_len();
8313                self.apply_redirections(&ro.redirections, stdout_before);
8314                self.vm.state.last_status = 0;
8315            }
8316            HirCommand::DoubleBracket(db) => {
8317                let result = self.eval_double_bracket(&db.words);
8318                self.vm.state.last_status = i32::from(!result);
8319            }
8320            HirCommand::ArithCommand(ac) => {
8321                let result = wasmsh_expand::eval_arithmetic(&ac.expr, &mut self.vm.state);
8322                self.vm.state.last_status = i32::from(result == 0);
8323            }
8324            HirCommand::ArithFor(af) => self.execute_arith_for(af),
8325            HirCommand::Select(sel) => self.execute_select(sel),
8326            _ => {}
8327        }
8328    }
8329
8330    /// Execute a simple command (`HirCommand::Exec`).
8331    fn execute_exec(&mut self, exec: &wasmsh_hir::HirExec) {
8332        let resolved = self.resolve_command_subst(&exec.argv);
8333        if self.exec.expansion_failed {
8334            return;
8335        }
8336        let expanded = expand_words_argv(&resolved, &mut self.vm.state);
8337
8338        if self.check_nounset_error() {
8339            return;
8340        }
8341        if expanded.is_empty() {
8342            return;
8343        }
8344
8345        // Brace and glob expansion must be suppressed for quoted words (POSIX + bash).
8346        let tagged: Vec<(String, bool)> = expanded
8347            .into_iter()
8348            .flat_map(|ew| {
8349                if ew.was_quoted {
8350                    vec![(ew.text, true)]
8351                } else {
8352                    wasmsh_expand::expand_braces(&ew.text)
8353                        .into_iter()
8354                        .map(|s| (s, false))
8355                        .collect()
8356                }
8357            })
8358            .collect();
8359        let argv = self.expand_globs_tagged(tagged);
8360
8361        for assignment in &exec.env {
8362            self.execute_assignment(&assignment.name, assignment.value.as_ref());
8363        }
8364
8365        if self.try_alias_expansion(&argv) {
8366            return;
8367        }
8368
8369        let Ok(exec_io) = self.prepare_exec_io(&exec.redirections) else {
8370            return;
8371        };
8372        self.with_exec_io_scope(exec_io, |runtime| {
8373            runtime.trace_command(&argv);
8374            runtime.execute_argv_command(&argv);
8375        });
8376    }
8377
8378    /// Drain a pending nounset error from parameter expansion and report it
8379    /// through the fallback interpreter's stderr sink.
8380    fn check_nounset_error(&mut self) -> bool {
8381        let Some(var_name) = self.vm.state.take_nounset_error() else {
8382            return false;
8383        };
8384        let msg = format!("wasmsh: {var_name}: unbound variable\n");
8385        self.write_stderr(msg.as_bytes());
8386        self.vm.state.last_status = 1;
8387        true
8388    }
8389
8390    /// Collect stdin from here-doc bodies or input redirections. Returns true if
8391    /// an error occurred and execution should stop.
8392    fn collect_stdin_from_redirections(&mut self, redirections: &[HirRedirection]) -> bool {
8393        for redir in redirections {
8394            if self.collect_stdin_from_redir(redir) {
8395                return true;
8396            }
8397        }
8398        false
8399    }
8400
8401    fn collect_stdin_from_redir(&mut self, redir: &HirRedirection) -> bool {
8402        match redir.op {
8403            RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
8404                self.collect_stdin_heredoc(redir);
8405                false
8406            }
8407            RedirectionOp::HereString => {
8408                self.collect_stdin_herestring(redir);
8409                false
8410            }
8411            RedirectionOp::Input => self.collect_stdin_input(redir),
8412            _ => false,
8413        }
8414    }
8415
8416    fn collect_stdin_heredoc(&mut self, redir: &HirRedirection) {
8417        if let Some(body) = &redir.here_doc_body {
8418            let expanded = wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
8419            self.set_pending_input_bytes(expanded.into_bytes());
8420        }
8421    }
8422
8423    fn collect_stdin_herestring(&mut self, redir: &HirRedirection) {
8424        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
8425        let resolved_target = resolved.first().unwrap_or(&redir.target);
8426        let content = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
8427        let mut data = content.into_bytes();
8428        data.push(b'\n');
8429        self.set_pending_input_bytes(data);
8430    }
8431
8432    fn collect_stdin_input(&mut self, redir: &HirRedirection) -> bool {
8433        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
8434        let resolved_target = resolved.first().unwrap_or(&redir.target);
8435        let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
8436        let path = self.resolve_cwd_path(&target);
8437        match self.fs.stat(&path) {
8438            Ok(metadata) if !metadata.is_dir => {
8439                self.set_pending_input_file(path, false);
8440                false
8441            }
8442            Ok(_) => self.fail_stdin_input(&target, "Is a directory"),
8443            Err(_) => self.fail_stdin_input(&target, "No such file or directory"),
8444        }
8445    }
8446
8447    fn fail_stdin_input(&mut self, target: &str, reason: &str) -> bool {
8448        let msg = format!("wasmsh: {target}: {reason}\n");
8449        self.write_stderr(msg.as_bytes());
8450        self.vm.state.last_status = 1;
8451        true
8452    }
8453
8454    fn read_pending_input_bytes(&mut self, cmd_name: &str) -> Result<Option<Vec<u8>>, ()> {
8455        let Some(mut reader) = self.take_pending_input_reader(cmd_name)? else {
8456            return Ok(None);
8457        };
8458        let mut data = Vec::new();
8459        match reader.read_to_end(&mut data) {
8460            Ok(_) => Ok(Some(data)),
8461            Err(err) => {
8462                let msg = format!("wasmsh: {cmd_name}: stdin read error: {err}\n");
8463                self.write_stderr(msg.as_bytes());
8464                self.vm.state.last_status = 1;
8465                Err(())
8466            }
8467        }
8468    }
8469
8470    /// Try alias expansion for the command. Returns true if an alias was expanded.
8471    fn try_alias_expansion(&mut self, argv: &[String]) -> bool {
8472        if !self.get_shopt_value("expand_aliases") {
8473            return false;
8474        }
8475        if let Some(alias_val) = self.aliases.get(&argv[0]).cloned() {
8476            let rest = if argv.len() > 1 {
8477                format!(" {}", argv[1..].join(" "))
8478            } else {
8479                String::new()
8480            };
8481            let expanded = format!("{alias_val}{rest}");
8482            let sub_events = self.execute_input_inner(&expanded);
8483            self.merge_sub_events(sub_events);
8484            return true;
8485        }
8486        false
8487    }
8488
8489    /// Print xtrace output if enabled.
8490    fn trace_command(&mut self, argv: &[String]) {
8491        if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
8492            let ps4 = self
8493                .vm
8494                .state
8495                .get_var("PS4")
8496                .unwrap_or_else(|| smol_str::SmolStr::from("+ "));
8497            let trace_line = format!("{}{}\n", ps4, argv.join(" "));
8498            self.write_stderr(trace_line.as_bytes());
8499        }
8500    }
8501
8502    fn resolve_runtime_command(cmd_name: &str) -> Option<RuntimeCommandKind> {
8503        match cmd_name {
8504            CMD_LOCAL => Some(RuntimeCommandKind::Local),
8505            CMD_BREAK => Some(RuntimeCommandKind::Break),
8506            CMD_CONTINUE => Some(RuntimeCommandKind::Continue),
8507            CMD_EXIT => Some(RuntimeCommandKind::Exit),
8508            CMD_EVAL => Some(RuntimeCommandKind::Eval),
8509            CMD_SOURCE | CMD_DOT => Some(RuntimeCommandKind::Source),
8510            CMD_DECLARE | CMD_TYPESET => Some(RuntimeCommandKind::Declare),
8511            CMD_LET => Some(RuntimeCommandKind::Let),
8512            CMD_SHOPT => Some(RuntimeCommandKind::Shopt),
8513            CMD_ALIAS => Some(RuntimeCommandKind::Alias),
8514            CMD_UNALIAS => Some(RuntimeCommandKind::Unalias),
8515            CMD_BUILTIN => Some(RuntimeCommandKind::BuiltinKeyword),
8516            CMD_MAPFILE | CMD_READARRAY => Some(RuntimeCommandKind::Mapfile),
8517            CMD_TYPE => Some(RuntimeCommandKind::Type),
8518            CMD_COMMAND => Some(RuntimeCommandKind::CommandKeyword),
8519            CMD_EXEC => Some(RuntimeCommandKind::ExecKeyword),
8520            CMD_HASH => Some(RuntimeCommandKind::Hash),
8521            CMD_TIMES => Some(RuntimeCommandKind::Times),
8522            CMD_DIRS => Some(RuntimeCommandKind::Dirs),
8523            CMD_PUSHD => Some(RuntimeCommandKind::Pushd),
8524            CMD_POPD => Some(RuntimeCommandKind::Popd),
8525            CMD_UMASK => Some(RuntimeCommandKind::Umask),
8526            CMD_WAIT => Some(RuntimeCommandKind::Wait),
8527            CMD_ULIMIT => Some(RuntimeCommandKind::Ulimit),
8528            _ => None,
8529        }
8530    }
8531
8532    fn resolve_command(&self, cmd_name: &str, argv: &[String]) -> ResolvedCommand {
8533        if let Some(kind) = Self::resolve_runtime_command(cmd_name) {
8534            return ResolvedCommand::Runtime(kind);
8535        }
8536        if cmd_name == "bash" || cmd_name == "sh" {
8537            return ResolvedCommand::ShellScript;
8538        }
8539        if let Some(body) = self.functions.get(cmd_name).cloned() {
8540            return ResolvedCommand::Function(body);
8541        }
8542        if self.builtins.is_builtin(cmd_name) {
8543            return ResolvedCommand::Builtin;
8544        }
8545        if self.utils.is_utility(cmd_name) {
8546            let kind = if cmd_name == "find" && argv.iter().any(|arg| arg == "-exec") {
8547                UtilityCommandKind::FindWithExec
8548            } else if cmd_name == "xargs" {
8549                UtilityCommandKind::Xargs
8550            } else {
8551                UtilityCommandKind::Plain
8552            };
8553            return ResolvedCommand::Utility(kind);
8554        }
8555        ResolvedCommand::External
8556    }
8557
8558    fn resolve_command_without_functions(
8559        &self,
8560        cmd_name: &str,
8561        argv: &[String],
8562    ) -> ResolvedCommand {
8563        if let Some(kind) = Self::resolve_runtime_command(cmd_name) {
8564            return ResolvedCommand::Runtime(kind);
8565        }
8566        if cmd_name == "bash" || cmd_name == "sh" {
8567            return ResolvedCommand::ShellScript;
8568        }
8569        if self.builtins.is_builtin(cmd_name) {
8570            return ResolvedCommand::Builtin;
8571        }
8572        if self.utils.is_utility(cmd_name) {
8573            let kind = if cmd_name == "find" && argv.iter().any(|arg| arg == "-exec") {
8574                UtilityCommandKind::FindWithExec
8575            } else if cmd_name == "xargs" {
8576                UtilityCommandKind::Xargs
8577            } else {
8578                UtilityCommandKind::Plain
8579            };
8580            return ResolvedCommand::Utility(kind);
8581        }
8582        ResolvedCommand::External
8583    }
8584
8585    fn find_command_path(&self, name: &str) -> Option<String> {
8586        if name.contains('/') {
8587            let path = self.resolve_cwd_path(name);
8588            self.fs.stat(&path).ok().map(|_| path)
8589        } else {
8590            self.search_path_for_file(name)
8591        }
8592    }
8593
8594    fn command_lookups(
8595        &self,
8596        name: &str,
8597        skip_functions: bool,
8598        force_path: bool,
8599    ) -> Vec<CommandLookup> {
8600        let mut lookups = Vec::new();
8601
8602        if !force_path {
8603            if let Some(value) = self.aliases.get(name) {
8604                lookups.push(CommandLookup {
8605                    kind: CommandLookupKind::Alias,
8606                    name: name.to_string(),
8607                    detail: value.clone(),
8608                });
8609            }
8610            if !skip_functions && self.functions.contains_key(name) {
8611                lookups.push(CommandLookup {
8612                    kind: CommandLookupKind::Function,
8613                    name: name.to_string(),
8614                    detail: name.to_string(),
8615                });
8616            }
8617            if self.builtins.is_builtin(name) {
8618                lookups.push(CommandLookup {
8619                    kind: CommandLookupKind::Builtin,
8620                    name: name.to_string(),
8621                    detail: name.to_string(),
8622                });
8623            }
8624        }
8625
8626        if let Some(path) = self.find_command_path(name) {
8627            lookups.push(CommandLookup {
8628                kind: CommandLookupKind::File,
8629                name: name.to_string(),
8630                detail: path,
8631            });
8632        }
8633
8634        lookups
8635    }
8636
8637    fn execute_argv_command(&mut self, argv: &[String]) {
8638        if self.check_resource_limits() || argv.is_empty() {
8639            return;
8640        }
8641        if let Some(last) = argv.last() {
8642            self.vm.state.set_last_argument(last.as_str());
8643        }
8644        let mut resolved = self.resolve_command(&argv[0], argv);
8645        // If the command would be dispatched externally and the path
8646        // contains a `/`, check whether the file has a shell shebang
8647        // so we can execute it natively instead of forwarding to the
8648        // external handler (which may not exist).
8649        if matches!(resolved, ResolvedCommand::External) && argv[0].contains('/') {
8650            if let Some(interp) = self.detect_shell_shebang(&argv[0]) {
8651                if interp == "bash"
8652                    || interp == "sh"
8653                    || interp == "/bin/bash"
8654                    || interp == "/bin/sh"
8655                    || interp.ends_with("/bash")
8656                    || interp.ends_with("/sh")
8657                {
8658                    resolved = ResolvedCommand::ShebangScript;
8659                }
8660            }
8661        }
8662        self.execute_resolved_command(resolved, argv);
8663    }
8664
8665    fn execute_resolved_command(&mut self, resolved: ResolvedCommand, argv: &[String]) {
8666        match resolved {
8667            ResolvedCommand::Runtime(kind) => self.execute_runtime_command(kind, argv),
8668            ResolvedCommand::ShellScript => self.call_shell_script(argv),
8669            ResolvedCommand::ShebangScript => self.call_shebang_script(argv),
8670            ResolvedCommand::Function(body) => self.call_shell_function(&argv[0], argv, &body),
8671            ResolvedCommand::Builtin => self.call_builtin(&argv[0], argv),
8672            ResolvedCommand::Utility(kind) => match kind {
8673                UtilityCommandKind::Plain => self.call_utility(&argv[0], argv),
8674                UtilityCommandKind::FindWithExec => self.call_find_with_exec(argv),
8675                UtilityCommandKind::Xargs => self.call_xargs_with_exec(argv),
8676            },
8677            ResolvedCommand::External => self.call_external(argv),
8678        }
8679    }
8680
8681    fn execute_runtime_command(&mut self, kind: RuntimeCommandKind, argv: &[String]) {
8682        match kind {
8683            RuntimeCommandKind::Local => self.execute_local(argv),
8684            RuntimeCommandKind::Break => {
8685                self.exec.break_depth = argv.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
8686                self.vm.state.last_status = 0;
8687            }
8688            RuntimeCommandKind::Continue => {
8689                self.exec.loop_continue = true;
8690                self.vm.state.last_status = 0;
8691            }
8692            RuntimeCommandKind::Exit => {
8693                let code = argv
8694                    .get(1)
8695                    .and_then(|s| s.parse().ok())
8696                    .unwrap_or(self.vm.state.last_status);
8697                self.exec.exit_requested = Some(code);
8698                self.vm.state.last_status = code;
8699            }
8700            RuntimeCommandKind::Eval => {
8701                let code = argv[1..].join(" ");
8702                let sub_events = self.execute_input_inner(&code);
8703                self.merge_sub_events_with_diagnostics(sub_events);
8704            }
8705            RuntimeCommandKind::Source => self.execute_source(argv),
8706            RuntimeCommandKind::Declare => self.execute_declare(argv),
8707            RuntimeCommandKind::Let => self.execute_let(argv),
8708            RuntimeCommandKind::Shopt => self.execute_shopt(argv),
8709            RuntimeCommandKind::Alias => self.execute_alias(argv),
8710            RuntimeCommandKind::Unalias => self.execute_unalias(argv),
8711            RuntimeCommandKind::BuiltinKeyword => self.execute_builtin_keyword(argv),
8712            RuntimeCommandKind::Mapfile => self.execute_mapfile(argv),
8713            RuntimeCommandKind::Type => self.execute_type(argv),
8714            RuntimeCommandKind::CommandKeyword => self.execute_command_keyword(argv),
8715            RuntimeCommandKind::ExecKeyword => self.execute_exec_keyword(argv),
8716            RuntimeCommandKind::Hash => self.execute_hash(argv),
8717            RuntimeCommandKind::Times => self.execute_times(),
8718            RuntimeCommandKind::Dirs => self.execute_dirs(),
8719            RuntimeCommandKind::Pushd => self.execute_pushd(argv),
8720            RuntimeCommandKind::Popd => self.execute_popd(),
8721            RuntimeCommandKind::Umask => self.execute_umask(argv),
8722            RuntimeCommandKind::Wait => self.execute_wait(argv),
8723            RuntimeCommandKind::Ulimit => self.execute_ulimit(argv),
8724        }
8725    }
8726
8727    /// Execute `local` — save old variable values and set new ones.
8728    fn execute_local(&mut self, argv: &[String]) {
8729        for arg in &argv[1..] {
8730            let (name, value) = if let Some(eq) = arg.find('=') {
8731                (&arg[..eq], Some(&arg[eq + 1..]))
8732            } else {
8733                (arg.as_str(), None)
8734            };
8735            let old = self.vm.state.get_var(name);
8736            self.exec
8737                .local_save_stack
8738                .push((smol_str::SmolStr::from(name), old));
8739            let val = value.map_or(smol_str::SmolStr::default(), smol_str::SmolStr::from);
8740            self.vm.state.set_var(smol_str::SmolStr::from(name), val);
8741        }
8742        self.vm.state.last_status = 0;
8743    }
8744
8745    /// Execute `source`/`.` — read and execute a file.
8746    fn execute_source(&mut self, argv: &[String]) {
8747        let Some(path) = argv.get(1) else { return };
8748        let resolved = if path.contains('/') {
8749            Some(self.resolve_cwd_path(path))
8750        } else {
8751            let direct = self.resolve_cwd_path(path);
8752            if self.fs.stat(&direct).is_ok() {
8753                Some(direct)
8754            } else if self.get_shopt_value("sourcepath") {
8755                self.search_path_for_file(path)
8756            } else {
8757                None
8758            }
8759        };
8760        let Some(full) = resolved else {
8761            let msg = format!("source: {path}: not found\n");
8762            self.write_stderr(msg.as_bytes());
8763            self.vm.state.last_status = 1;
8764            return;
8765        };
8766        let Ok(h) = self.fs.open(&full, OpenOptions::read()) else {
8767            let msg = format!("source: {path}: not found\n");
8768            self.write_stderr(msg.as_bytes());
8769            self.vm.state.last_status = 1;
8770            return;
8771        };
8772        match self.fs.read_file(h) {
8773            Ok(data) => {
8774                self.fs.close(h);
8775                self.vm
8776                    .state
8777                    .source_stack
8778                    .push(smol_str::SmolStr::from(full.as_str()));
8779                let code = String::from_utf8_lossy(&data).to_string();
8780                self.with_nested_shell_scope(|runtime| {
8781                    let sub_events = runtime.execute_input_inner(&code);
8782                    runtime.merge_sub_events_with_diagnostics(sub_events);
8783                    runtime.run_return_trap_if_needed();
8784                });
8785                self.vm.state.source_stack.pop();
8786            }
8787            Err(e) => {
8788                self.fs.close(h);
8789                let msg = format!("source: {path}: read error: {e}\n");
8790                self.write_stderr(msg.as_bytes());
8791                self.vm.state.last_status = 1;
8792            }
8793        }
8794    }
8795
8796    /// Merge sub-events (stdout/stderr only) into the current VM buffers.
8797    fn merge_sub_events(&mut self, events: Vec<WorkerEvent>) {
8798        for e in events {
8799            match e {
8800                WorkerEvent::Stdout(d) => self.write_stdout(&d),
8801                WorkerEvent::Stderr(d) => self.write_stderr(&d),
8802                _ => {}
8803            }
8804        }
8805    }
8806
8807    /// Merge sub-events including diagnostics into the current VM buffers.
8808    fn merge_sub_events_with_diagnostics(&mut self, events: Vec<WorkerEvent>) {
8809        for e in events {
8810            match e {
8811                WorkerEvent::Stdout(d) => self.write_stdout(&d),
8812                WorkerEvent::Stderr(d) => self.write_stderr(&d),
8813                WorkerEvent::Diagnostic(level, msg) => self.vm.emit_diagnostic(
8814                    convert_diag_level(level),
8815                    wasmsh_vm::DiagCategory::Runtime,
8816                    msg,
8817                ),
8818                _ => {}
8819            }
8820        }
8821    }
8822
8823    /// Handle `bash`/`sh` commands by reading the script and executing it.
8824    fn call_shell_script(&mut self, argv: &[String]) {
8825        if argv.len() < 2 {
8826            // Interactive shell not supported — just return
8827            return;
8828        }
8829
8830        // Check for -c flag (inline script)
8831        // bash -c 'script' [name [args...]]
8832        // $0 = name (argv[3]), $1.. = args (argv[4..])
8833        if argv[1] == "-c" {
8834            if let Some(script) = argv.get(2) {
8835                let old_positional = std::mem::take(&mut self.vm.state.positional);
8836                let old_script_name = self.vm.state.script_name.take();
8837                if let Some(name) = argv.get(3) {
8838                    self.vm.state.script_name = Some(smol_str::SmolStr::from(name.as_str()));
8839                }
8840                self.vm.state.positional = argv
8841                    .get(4..)
8842                    .unwrap_or_default()
8843                    .iter()
8844                    .map(|s| smol_str::SmolStr::from(s.as_str()))
8845                    .collect();
8846                self.with_nested_shell_scope(|runtime| {
8847                    let sub_events = runtime.execute_input_inner(script);
8848                    runtime.merge_sub_events_with_diagnostics(sub_events);
8849                });
8850                self.vm.state.positional = old_positional;
8851                self.vm.state.script_name = old_script_name;
8852            }
8853            return;
8854        }
8855
8856        // Read script file from VFS
8857        let path = if argv[1].starts_with('/') {
8858            argv[1].clone()
8859        } else {
8860            format!("{}/{}", self.vm.state.cwd, argv[1])
8861        };
8862        let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
8863            let msg = format!("{}: {}: No such file or directory\n", argv[0], argv[1]);
8864            self.write_stderr(msg.as_bytes());
8865            self.vm.state.last_status = 127;
8866            return;
8867        };
8868        let data = self.fs.read_file(h).unwrap_or_default();
8869        self.fs.close(h);
8870        let content = String::from_utf8_lossy(&data).to_string();
8871
8872        // Set $0 to the script path, positional parameters from argv[2..]
8873        let old_positional = std::mem::take(&mut self.vm.state.positional);
8874        let old_script_name = self.vm.state.script_name.take();
8875        self.vm.state.script_name = Some(smol_str::SmolStr::from(argv[1].as_str()));
8876        self.vm.state.positional = argv[2..]
8877            .iter()
8878            .map(|s| smol_str::SmolStr::from(s.as_str()))
8879            .collect();
8880
8881        self.vm
8882            .state
8883            .source_stack
8884            .push(smol_str::SmolStr::from(path.as_str()));
8885        let sub_events =
8886            self.with_nested_shell_scope(|runtime| runtime.execute_input_inner(&content));
8887        self.vm.state.source_stack.pop();
8888        self.merge_sub_events_with_diagnostics(sub_events);
8889
8890        self.vm.state.positional = old_positional;
8891        self.vm.state.script_name = old_script_name;
8892    }
8893
8894    /// Detect a shell shebang at the start of a file.
8895    /// Returns the interpreter command (e.g. "bash", "/bin/sh") if found.
8896    fn detect_shell_shebang(&mut self, cmd_name: &str) -> Option<String> {
8897        let path = if cmd_name.starts_with('/') {
8898            cmd_name.to_string()
8899        } else {
8900            format!("{}/{cmd_name}", self.vm.state.cwd)
8901        };
8902        let h = self.fs.open(&path, OpenOptions::read()).ok()?;
8903        let data = self.fs.read_file(h).unwrap_or_default();
8904        self.fs.close(h);
8905        if data.len() < 3 || data[0] != b'#' || data[1] != b'!' {
8906            return None;
8907        }
8908        let end = data.iter().position(|&b| b == b'\n').unwrap_or(data.len());
8909        let line = String::from_utf8_lossy(&data[2..end]).trim().to_string();
8910        // Handle "#!/usr/bin/env bash" → "bash"
8911        if let Some(rest) = line.strip_prefix("/usr/bin/env ") {
8912            Some(rest.trim().to_string())
8913        } else {
8914            // e.g. "/bin/bash" → extract basename for matching
8915            Some(line.clone())
8916        }
8917    }
8918
8919    /// Execute a script file that was invoked directly by path (e.g. `/workspace/script.sh`).
8920    /// The shebang has already been validated as a shell interpreter.
8921    fn call_shebang_script(&mut self, argv: &[String]) {
8922        let cmd_name = &argv[0];
8923        let path = if cmd_name.starts_with('/') {
8924            cmd_name.clone()
8925        } else {
8926            format!("{}/{cmd_name}", self.vm.state.cwd)
8927        };
8928        let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
8929            let msg = format!("wasmsh: {cmd_name}: No such file or directory\n");
8930            self.write_stderr(msg.as_bytes());
8931            self.vm.state.last_status = 127;
8932            return;
8933        };
8934        let data = self.fs.read_file(h).unwrap_or_default();
8935        self.fs.close(h);
8936        let content = String::from_utf8_lossy(&data).to_string();
8937
8938        // Set $0 to the script path, positional parameters from argv[1..]
8939        let old_positional = std::mem::take(&mut self.vm.state.positional);
8940        let old_script_name = self.vm.state.script_name.take();
8941        self.vm.state.script_name = Some(smol_str::SmolStr::from(cmd_name.as_str()));
8942        self.vm.state.positional = argv[1..]
8943            .iter()
8944            .map(|s| smol_str::SmolStr::from(s.as_str()))
8945            .collect();
8946
8947        self.vm
8948            .state
8949            .source_stack
8950            .push(smol_str::SmolStr::from(path.as_str()));
8951        let sub_events =
8952            self.with_nested_shell_scope(|runtime| runtime.execute_input_inner(&content));
8953        self.vm.state.source_stack.pop();
8954        self.merge_sub_events_with_diagnostics(sub_events);
8955
8956        self.vm.state.positional = old_positional;
8957        self.vm.state.script_name = old_script_name;
8958    }
8959
8960    fn call_external(&mut self, argv: &[String]) {
8961        let cmd_name = &argv[0];
8962        let Ok(stdin) = self.take_external_stdin(cmd_name) else {
8963            return;
8964        };
8965        if let Some(ref mut handler) = self.external_handler {
8966            if let Some(result) = handler(cmd_name, argv, stdin) {
8967                self.write_streams(&result.stdout, &result.stderr);
8968                self.vm.state.last_status = result.status;
8969            } else {
8970                let msg = format!("wasmsh: {cmd_name}: command not found\n");
8971                self.write_stderr(msg.as_bytes());
8972                self.vm.state.last_status = 127;
8973            }
8974        } else {
8975            let msg = format!("wasmsh: {cmd_name}: command not found\n");
8976            self.write_stderr(msg.as_bytes());
8977            self.vm.state.last_status = 127;
8978        }
8979    }
8980
8981    /// Invoke a shell function.
8982    fn call_shell_function(&mut self, cmd_name: &str, argv: &[String], body: &HirCommand) {
8983        self.exec.recursion_depth += 1;
8984        if let Err(reason) = self
8985            .vm
8986            .budget
8987            .enter_recursion(self.vm.limits.recursion_limit)
8988        {
8989            self.exec.recursion_depth -= 1;
8990            self.mark_budget_exhaustion(reason);
8991            self.write_stderr(b"wasmsh: maximum recursion depth exceeded\n");
8992            self.vm.state.last_status = 1;
8993            return;
8994        }
8995        let old_positional = std::mem::take(&mut self.vm.state.positional);
8996        self.vm.state.positional = argv[1..]
8997            .iter()
8998            .map(|s| smol_str::SmolStr::from(s.as_str()))
8999            .collect();
9000        self.vm
9001            .state
9002            .func_stack
9003            .push(smol_str::SmolStr::from(cmd_name));
9004        let locals_before = self.exec.local_save_stack.len();
9005        self.with_nested_shell_scope(|runtime| {
9006            runtime.execute_command(body);
9007            runtime.run_return_trap_if_needed();
9008        });
9009        let new_locals: Vec<_> = self.exec.local_save_stack.drain(locals_before..).collect();
9010        for (name, old_val) in new_locals.into_iter().rev() {
9011            if let Some(val) = old_val {
9012                self.vm.state.set_var(name, val);
9013            } else {
9014                self.vm.state.unset_var(&name).ok();
9015            }
9016        }
9017        self.vm.state.func_stack.pop();
9018        self.vm.state.positional = old_positional;
9019        self.vm.budget.exit_recursion();
9020        self.exec.recursion_depth -= 1;
9021    }
9022
9023    /// Invoke a builtin command.
9024    fn call_builtin(&mut self, cmd_name: &str, argv: &[String]) {
9025        let builtin_fn = self.builtins.get(cmd_name).unwrap();
9026        let Ok(stdin) = self.take_builtin_stdin(cmd_name) else {
9027            return;
9028        };
9029        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
9030        let status = {
9031            let mut router = RuntimeOutputRouter {
9032                exec: &mut self.exec,
9033                exec_io: self.current_exec_io.as_mut(),
9034                proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
9035                vm_stdout: &mut self.vm.stdout,
9036                vm_stderr: &mut self.vm.stderr,
9037                vm_output_bytes: &mut self.vm.output_bytes,
9038                vm_output_limit: self.vm.limits.output_byte_limit,
9039                vm_diagnostics: &mut self.vm.diagnostics,
9040            };
9041            let mut sink = RuntimeBuiltinSink {
9042                router: &mut router,
9043            };
9044            let mut ctx = wasmsh_builtins::BuiltinContext {
9045                state: &mut self.vm.state,
9046                output: &mut sink,
9047                fs: Some(&self.fs),
9048                stdin,
9049            };
9050            builtin_fn(&mut ctx, &argv_refs)
9051        };
9052        self.vm.state.last_status = status;
9053    }
9054
9055    /// Extract `-exec CMD [args...] {} \;` from find argv.
9056    /// Returns `(exec_template, cleaned_argv)` or `None` if no `-exec` present.
9057    fn extract_find_exec(argv: &[String]) -> Option<(Vec<String>, Vec<String>)> {
9058        let exec_pos = argv.iter().position(|a| a == "-exec")?;
9059        // Find the terminator: \; or ;
9060        let term_pos = argv[exec_pos + 1..]
9061            .iter()
9062            .position(|a| a == "\\;" || a == ";")
9063            .map(|p| p + exec_pos + 1)?;
9064        let template: Vec<String> = argv[exec_pos + 1..term_pos].to_vec();
9065        if template.is_empty() {
9066            return None;
9067        }
9068        let mut cleaned: Vec<String> = argv[..exec_pos].to_vec();
9069        cleaned.extend_from_slice(&argv[term_pos + 1..]);
9070        Some((template, cleaned))
9071    }
9072
9073    /// Shell-quote a path for safe interpolation into a command string.
9074    fn shell_quote(s: &str) -> String {
9075        if s.chars()
9076            .all(|c| c.is_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'))
9077        {
9078            s.to_string()
9079        } else {
9080            format!("'{}'", s.replace('\'', "'\\''"))
9081        }
9082    }
9083
9084    /// Handle `find ... -exec CMD {} \;` by running find for paths, then executing
9085    /// the command for each matched path via the shell.
9086    fn call_find_with_exec(&mut self, argv: &[String]) {
9087        let Some((template, cleaned_argv)) = Self::extract_find_exec(argv) else {
9088            // Malformed -exec (missing \;), fall through to normal find
9089            self.call_utility("find", argv);
9090            return;
9091        };
9092
9093        // Phase 1: run find with cleaned argv, capturing stdout
9094        let ((), captured) = self.with_output_capture(true, false, |runtime| {
9095            runtime.call_utility("find", &cleaned_argv);
9096        });
9097        let find_output = captured.stdout;
9098
9099        // Phase 2: parse matched paths
9100        let paths_str = String::from_utf8_lossy(&find_output);
9101        let paths: Vec<&str> = paths_str.lines().filter(|l| !l.is_empty()).collect();
9102
9103        // Phase 3: execute the command for each path
9104        let mut last_status = 0i32;
9105        for path in paths {
9106            let cmd_line: String = template
9107                .iter()
9108                .map(|t| {
9109                    if t == "{}" {
9110                        Self::shell_quote(path)
9111                    } else {
9112                        t.clone()
9113                    }
9114                })
9115                .collect::<Vec<_>>()
9116                .join(" ");
9117            let sub_events = self.execute_input_inner(&cmd_line);
9118            self.merge_sub_events(sub_events);
9119            if self.vm.state.last_status != 0 {
9120                last_status = self.vm.state.last_status;
9121            }
9122        }
9123        self.vm.state.last_status = last_status;
9124    }
9125
9126    /// Handle `xargs` with actual command execution for non-echo commands.
9127    /// The existing xargs utility already formats correct command lines for
9128    /// non-echo; we capture those and execute them via the shell.
9129    fn call_xargs_with_exec(&mut self, argv: &[String]) {
9130        // Determine if xargs has a non-echo command by scanning past flags
9131        let mut has_non_echo = false;
9132        let mut i = 1;
9133        while i < argv.len() {
9134            let arg = &argv[i];
9135            if matches!(arg.as_str(), "-I" | "-n" | "-d" | "-P" | "-L") && i + 1 < argv.len() {
9136                i += 2;
9137            } else if matches!(arg.as_str(), "-0" | "--null" | "-t" | "-p") || arg.starts_with('-')
9138            {
9139                i += 1;
9140            } else {
9141                // First non-flag arg is the command
9142                if arg != "echo" {
9143                    has_non_echo = true;
9144                }
9145                break;
9146            }
9147        }
9148
9149        if !has_non_echo {
9150            self.call_utility("xargs", argv);
9151            return;
9152        }
9153
9154        // Run xargs utility — it outputs formatted command lines for non-echo
9155        let ((), captured) = self.with_output_capture(true, false, |runtime| {
9156            runtime.call_utility("xargs", argv);
9157        });
9158        let xargs_output = captured.stdout;
9159
9160        // Execute each output line as a command
9161        let output_str = String::from_utf8_lossy(&xargs_output);
9162        let mut last_status = 0i32;
9163        for line in output_str.lines().filter(|l| !l.is_empty()) {
9164            let sub_events = self.execute_input_inner(line);
9165            self.merge_sub_events(sub_events);
9166            if self.vm.state.last_status != 0 {
9167                last_status = self.vm.state.last_status;
9168            }
9169        }
9170        self.vm.state.last_status = last_status;
9171    }
9172
9173    /// Invoke a utility command.
9174    fn call_utility(&mut self, cmd_name: &str, argv: &[String]) {
9175        let Ok(stdin) = self.take_util_stdin(cmd_name) else {
9176            return;
9177        };
9178        let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
9179        let cwd = self.vm.state.cwd.clone();
9180        let status = {
9181            let mut router = RuntimeOutputRouter {
9182                exec: &mut self.exec,
9183                exec_io: self.current_exec_io.as_mut(),
9184                proc_subst_out_scopes: &mut self.proc_subst_out_scopes,
9185                vm_stdout: &mut self.vm.stdout,
9186                vm_stderr: &mut self.vm.stderr,
9187                vm_output_bytes: &mut self.vm.output_bytes,
9188                vm_output_limit: self.vm.limits.output_byte_limit,
9189                vm_diagnostics: &mut self.vm.diagnostics,
9190            };
9191            let mut output = RuntimeUtilSink {
9192                router: &mut router,
9193            };
9194            let util_fn = self.utils.get(cmd_name).unwrap();
9195            let mut ctx = UtilContext {
9196                fs: &mut self.fs,
9197                output: &mut output,
9198                cwd: &cwd,
9199                stdin,
9200                state: Some(&self.vm.state),
9201                network: self.network.as_deref(),
9202            };
9203            util_fn(&mut ctx, &argv_refs)
9204        };
9205        self.vm.state.last_status = status;
9206    }
9207
9208    /// Execute an `if` command.
9209    fn execute_if(&mut self, if_cmd: &wasmsh_hir::HirIf) {
9210        let saved_suppress = self.exec.errexit_suppressed;
9211        self.exec.errexit_suppressed = true;
9212        self.execute_body(&if_cmd.condition);
9213        self.exec.errexit_suppressed = saved_suppress;
9214        if self.vm.state.last_status == 0 {
9215            self.execute_body(&if_cmd.then_body);
9216            return;
9217        }
9218        for elif in &if_cmd.elifs {
9219            let saved = self.exec.errexit_suppressed;
9220            self.exec.errexit_suppressed = true;
9221            self.execute_body(&elif.condition);
9222            self.exec.errexit_suppressed = saved;
9223            if self.vm.state.last_status == 0 {
9224                self.execute_body(&elif.then_body);
9225                return;
9226            }
9227        }
9228        if let Some(else_body) = &if_cmd.else_body {
9229            self.execute_body(else_body);
9230        }
9231    }
9232
9233    /// Execute a `while` loop.
9234    fn execute_while_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
9235        loop {
9236            if self.check_resource_limits() {
9237                break;
9238            }
9239            let saved = self.exec.errexit_suppressed;
9240            self.exec.errexit_suppressed = true;
9241            self.execute_body(&loop_cmd.condition);
9242            self.exec.errexit_suppressed = saved;
9243            if self.vm.state.last_status != 0 {
9244                break;
9245            }
9246            self.execute_body(&loop_cmd.body);
9247            if self.handle_loop_control() {
9248                break;
9249            }
9250        }
9251    }
9252
9253    /// Execute an `until` loop.
9254    fn execute_until_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
9255        loop {
9256            if self.check_resource_limits() {
9257                break;
9258            }
9259            let saved = self.exec.errexit_suppressed;
9260            self.exec.errexit_suppressed = true;
9261            self.execute_body(&loop_cmd.condition);
9262            self.exec.errexit_suppressed = saved;
9263            if self.vm.state.last_status == 0 {
9264                break;
9265            }
9266            self.execute_body(&loop_cmd.body);
9267            if self.handle_loop_control() {
9268                break;
9269            }
9270        }
9271    }
9272
9273    /// Handle loop control flow (break/continue/exit). Returns true if the loop should break.
9274    fn handle_loop_control(&mut self) -> bool {
9275        if self.exec.break_depth > 0 {
9276            self.exec.break_depth -= 1;
9277            return true;
9278        }
9279        if self.exec.loop_continue {
9280            self.exec.loop_continue = false;
9281        }
9282        self.exec.exit_requested.is_some()
9283    }
9284
9285    /// Execute a `for` loop.
9286    fn execute_for_loop(&mut self, for_cmd: &wasmsh_hir::HirFor) {
9287        let words = self.expand_for_words(for_cmd.words.as_deref());
9288        for word in words {
9289            if self.check_resource_limits() {
9290                break;
9291            }
9292            self.vm.state.set_var(for_cmd.var_name.clone(), word.into());
9293            self.execute_body(&for_cmd.body);
9294            if self.exec.break_depth > 0 {
9295                self.exec.break_depth -= 1;
9296                break;
9297            }
9298            if self.exec.loop_continue {
9299                self.exec.loop_continue = false;
9300                continue;
9301            }
9302            if self.exec.exit_requested.is_some() {
9303                break;
9304            }
9305        }
9306    }
9307
9308    /// Expand word list for `for` and `select` commands.
9309    fn expand_for_words(&mut self, words: Option<&[Word]>) -> Vec<String> {
9310        if let Some(ws) = words {
9311            let resolved = self.resolve_command_subst(ws);
9312            let mut result = Vec::new();
9313            for w in &resolved {
9314                let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
9315                result.extend(expanded.fields);
9316            }
9317            let result: Vec<String> = result
9318                .into_iter()
9319                .flat_map(|arg| wasmsh_expand::expand_braces(&arg))
9320                .collect();
9321            self.expand_globs(result)
9322        } else {
9323            self.vm
9324                .state
9325                .positional
9326                .iter()
9327                .map(ToString::to_string)
9328                .collect()
9329        }
9330    }
9331
9332    /// Execute a `case` command.
9333    fn execute_case(&mut self, case_cmd: &wasmsh_hir::HirCase) {
9334        let nocasematch = self.vm.state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
9335        let value = wasmsh_expand::expand_word(&case_cmd.word, &mut self.vm.state);
9336        let mut i = 0;
9337        let mut fallthrough = false;
9338        while i < case_cmd.items.len() {
9339            let item = &case_cmd.items[i];
9340            let pattern_matched = if fallthrough {
9341                true
9342            } else {
9343                item.patterns.iter().any(|pattern| {
9344                    let pat = wasmsh_expand::expand_word(pattern, &mut self.vm.state);
9345                    if nocasematch {
9346                        glob_match_inner(
9347                            pat.to_lowercase().as_bytes(),
9348                            value.to_lowercase().as_bytes(),
9349                        )
9350                    } else {
9351                        glob_match_inner(pat.as_bytes(), value.as_bytes())
9352                    }
9353                })
9354            };
9355            if pattern_matched {
9356                self.execute_body(&item.body);
9357                match item.terminator {
9358                    CaseTerminator::Break => break,
9359                    CaseTerminator::Fallthrough => {
9360                        fallthrough = true;
9361                        i += 1;
9362                    }
9363                    CaseTerminator::ContinueTesting => {
9364                        fallthrough = false;
9365                        i += 1;
9366                    }
9367                }
9368            } else {
9369                fallthrough = false;
9370                i += 1;
9371            }
9372        }
9373    }
9374
9375    /// Execute a C-style `for (( init; cond; step ))` loop.
9376    fn execute_arith_for(&mut self, af: &wasmsh_hir::HirArithFor) {
9377        if !af.init.is_empty() {
9378            wasmsh_expand::eval_arithmetic(&af.init, &mut self.vm.state);
9379        }
9380        loop {
9381            if self.check_resource_limits() {
9382                break;
9383            }
9384            if !af.cond.is_empty() {
9385                let cond_val = wasmsh_expand::eval_arithmetic(&af.cond, &mut self.vm.state);
9386                if cond_val == 0 {
9387                    break;
9388                }
9389            }
9390            self.execute_body(&af.body);
9391            if self.handle_loop_control() {
9392                break;
9393            }
9394            if !af.step.is_empty() {
9395                wasmsh_expand::eval_arithmetic(&af.step, &mut self.vm.state);
9396            }
9397        }
9398    }
9399
9400    /// Execute a `select` command.
9401    fn execute_select(&mut self, sel: &wasmsh_hir::HirSelect) {
9402        if self.collect_stdin_from_redirections(&sel.redirections) {
9403            return;
9404        }
9405
9406        let words = self.expand_for_words(sel.words.as_deref());
9407        if words.is_empty() {
9408            return;
9409        }
9410
9411        self.print_select_menu(&words);
9412
9413        let Ok(input) = self.read_pending_input_bytes("select") else {
9414            return;
9415        };
9416        let input = String::from_utf8_lossy(&input.unwrap_or_default()).into_owned();
9417
9418        for line in input.lines() {
9419            let reply = line.trim();
9420            self.vm
9421                .state
9422                .set_var(smol_str::SmolStr::from("REPLY"), reply.into());
9423
9424            let selected = reply.parse::<usize>().ok().and_then(|n| {
9425                if n >= 1 && n <= words.len() {
9426                    Some(words[n - 1].clone())
9427                } else {
9428                    None
9429                }
9430            });
9431
9432            self.vm
9433                .state
9434                .set_var(sel.var_name.clone(), selected.unwrap_or_default().into());
9435
9436            self.execute_body(&sel.body);
9437            if self.exec.break_depth > 0 {
9438                self.exec.break_depth -= 1;
9439                break;
9440            }
9441            if self.exec.loop_continue {
9442                self.exec.loop_continue = false;
9443            }
9444            if self.exec.exit_requested.is_some() {
9445                break;
9446            }
9447            if reply.is_empty() {
9448                self.print_select_menu(&words);
9449            }
9450        }
9451    }
9452
9453    fn print_select_menu(&mut self, words: &[String]) {
9454        for (idx, word) in words.iter().enumerate() {
9455            let line = format!("{}) {word}\n", idx + 1);
9456            self.write_stderr(line.as_bytes());
9457        }
9458    }
9459
9460    // ---- [[ ]] extended test evaluation ----
9461
9462    /// Expand a word inside `[[ ]]` — no word splitting or glob expansion.
9463    fn dbl_bracket_expand(&mut self, word: &Word) -> String {
9464        let resolved = self.resolve_command_subst(std::slice::from_ref(word));
9465        wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
9466    }
9467
9468    /// Evaluate a `[[ expression ]]` command. Returns true for exit-status 0.
9469    fn eval_double_bracket(&mut self, words: &[Word]) -> bool {
9470        // Expand all words (no splitting/globbing) into string tokens for the evaluator
9471        let tokens: Vec<String> = words.iter().map(|w| self.dbl_bracket_expand(w)).collect();
9472        let mut pos = 0;
9473        dbl_bracket_eval_or(&tokens, &mut pos, &self.fs, &mut self.vm.state)
9474    }
9475
9476    fn resolve_cwd_path(&self, path: &str) -> String {
9477        if path.starts_with('/') {
9478            wasmsh_fs::normalize_path(path)
9479        } else {
9480            wasmsh_fs::normalize_path(&format!("{}/{}", self.vm.state.cwd, path))
9481        }
9482    }
9483
9484    /// Execute `alias [name[='value'] ...]`.
9485    fn execute_alias(&mut self, argv: &[String]) {
9486        let args = &argv[1..];
9487        if args.is_empty() {
9488            // List all aliases
9489            let alias_lines: Vec<String> = self
9490                .aliases
9491                .iter()
9492                .map(|(name, value)| format!("alias {name}='{value}'\n"))
9493                .collect();
9494            for line in alias_lines {
9495                self.write_stdout(line.as_bytes());
9496            }
9497            self.vm.state.last_status = 0;
9498            return;
9499        }
9500        for arg in args {
9501            if let Some(eq_pos) = arg.find('=') {
9502                let name = &arg[..eq_pos];
9503                let value = &arg[eq_pos + 1..];
9504                self.aliases.insert(name.to_string(), value.to_string());
9505            } else {
9506                // Show specific alias
9507                if let Some(value) = self.aliases.get(arg.as_str()) {
9508                    let line = format!("alias {arg}='{value}'\n");
9509                    self.write_stdout(line.as_bytes());
9510                } else {
9511                    let msg = format!("alias: {arg}: not found\n");
9512                    self.write_stderr(msg.as_bytes());
9513                    self.vm.state.last_status = 1;
9514                    return;
9515                }
9516            }
9517        }
9518        self.vm.state.last_status = 0;
9519    }
9520
9521    /// Execute `unalias [-a] name ...`.
9522    fn execute_unalias(&mut self, argv: &[String]) {
9523        let args = &argv[1..];
9524        if args.is_empty() {
9525            self.write_stderr(b"unalias: usage: unalias [-a] name ...\n");
9526            self.vm.state.last_status = 1;
9527            return;
9528        }
9529        for arg in args {
9530            if arg == "-a" {
9531                self.aliases.clear();
9532            } else if self.aliases.shift_remove(arg.as_str()).is_none() {
9533                let msg = format!("unalias: {arg}: not found\n");
9534                self.write_stderr(msg.as_bytes());
9535                self.vm.state.last_status = 1;
9536                return;
9537            }
9538        }
9539        self.vm.state.last_status = 0;
9540    }
9541
9542    /// Execute `type name ...` — report how each name would be interpreted.
9543    /// Checks aliases, functions, builtins, and utilities in that order.
9544    fn execute_type(&mut self, argv: &[String]) {
9545        let (flags, names) = Self::parse_type_args(&argv[1..]);
9546        let mut status = 0;
9547        for name in names {
9548            if !self.render_type_name(name, &flags) {
9549                status = 1;
9550            }
9551        }
9552        self.vm.state.last_status = status;
9553    }
9554
9555    fn parse_type_args<'a>(args: &'a [String]) -> (TypeFlags, Vec<&'a str>) {
9556        let mut flags = TypeFlags::default();
9557        let mut names = Vec::new();
9558        for arg in args {
9559            if arg.starts_with('-') && arg.len() > 1 {
9560                Self::apply_type_short_flags(&arg[1..], &mut flags);
9561            } else {
9562                names.push(arg.as_str());
9563            }
9564        }
9565        (flags, names)
9566    }
9567
9568    fn apply_type_short_flags(short: &str, flags: &mut TypeFlags) {
9569        for ch in short.chars() {
9570            match ch {
9571                'a' => flags.all = true,
9572                'f' => flags.skip_functions = true,
9573                'p' => flags.path_only = true,
9574                'P' => {
9575                    flags.path_only = true;
9576                    flags.force_path = true;
9577                }
9578                't' => flags.type_only = true,
9579                _ => {}
9580            }
9581        }
9582    }
9583
9584    fn render_type_name(&mut self, name: &str, flags: &TypeFlags) -> bool {
9585        let mut lookups = self.command_lookups(name, flags.skip_functions, flags.force_path);
9586        if flags.path_only {
9587            lookups.retain(|lookup| matches!(lookup.kind, CommandLookupKind::File));
9588        }
9589        if lookups.is_empty() {
9590            let msg = format!("wasmsh: type: {name}: not found\n");
9591            self.write_stderr(msg.as_bytes());
9592            return false;
9593        }
9594        let limit = if flags.all { usize::MAX } else { 1 };
9595        for lookup in lookups.into_iter().take(limit) {
9596            let line = format_type_lookup(&lookup, flags.type_only, flags.path_only);
9597            self.write_stdout(format!("{line}\n").as_bytes());
9598        }
9599        true
9600    }
9601
9602    /// Execute `builtin name [args...]` — skip alias and function lookup,
9603    /// invoke the named builtin directly.
9604    fn execute_builtin_keyword(&mut self, argv: &[String]) {
9605        if argv.len() < 2 {
9606            self.vm.state.last_status = 0;
9607            return;
9608        }
9609        let builtin_argv: Vec<String> = argv[1..].to_vec();
9610        let cmd_name = &builtin_argv[0];
9611        if self.builtins.is_builtin(cmd_name) {
9612            self.execute_resolved_command(ResolvedCommand::Builtin, &builtin_argv);
9613        } else {
9614            let msg = format!("builtin: {cmd_name}: not a shell builtin\n");
9615            self.write_stderr(msg.as_bytes());
9616            self.vm.state.last_status = 1;
9617        }
9618    }
9619
9620    fn execute_command_keyword(&mut self, argv: &[String]) {
9621        let mut use_default_path = false;
9622        let mut verbose = false;
9623        let mut describe = false;
9624        let mut index = 1usize;
9625
9626        while let Some(arg) = argv.get(index) {
9627            match arg.as_str() {
9628                "-p" => use_default_path = true,
9629                "-v" => verbose = true,
9630                "-V" => describe = true,
9631                _ if arg.starts_with('-') && arg.len() > 1 => {}
9632                _ => break,
9633            }
9634            index += 1;
9635        }
9636
9637        let args = &argv[index..];
9638        if verbose || describe {
9639            let mut status = 0;
9640            for name in args {
9641                let lookups = self.command_lookups(name, true, use_default_path);
9642                let Some(lookup) = lookups.first() else {
9643                    status = 1;
9644                    continue;
9645                };
9646                let line = if verbose {
9647                    format_command_verbose(lookup)
9648                } else {
9649                    format_type_lookup(lookup, false, false)
9650                };
9651                self.write_stdout(format!("{line}\n").as_bytes());
9652            }
9653            self.vm.state.last_status = status;
9654            return;
9655        }
9656
9657        if args.is_empty() {
9658            self.vm.state.last_status = 0;
9659            return;
9660        }
9661
9662        let resolved = self.resolve_command_without_functions(&args[0], args);
9663        self.execute_resolved_command(resolved, args);
9664    }
9665
9666    fn execute_exec_keyword(&mut self, argv: &[String]) {
9667        if argv.len() <= 1 {
9668            self.vm.state.last_status = 0;
9669            return;
9670        }
9671        let args = &argv[1..];
9672        let resolved = self.resolve_command_without_functions(&args[0], args);
9673        self.execute_resolved_command(resolved, args);
9674    }
9675
9676    fn execute_hash(&mut self, argv: &[String]) {
9677        let mut print_paths = false;
9678        let mut status = 0;
9679
9680        for arg in &argv[1..] {
9681            match arg.as_str() {
9682                "-r" => {}
9683                "-t" => print_paths = true,
9684                name => {
9685                    let lookups = self.command_lookups(name, true, true);
9686                    let Some(lookup) = lookups
9687                        .iter()
9688                        .find(|lookup| matches!(lookup.kind, CommandLookupKind::File))
9689                    else {
9690                        status = 1;
9691                        continue;
9692                    };
9693                    if print_paths {
9694                        self.write_stdout(format!("{}\n", lookup.detail).as_bytes());
9695                    }
9696                }
9697            }
9698        }
9699
9700        self.vm.state.last_status = status;
9701    }
9702
9703    fn execute_times(&mut self) {
9704        self.write_stdout(b"0m0.000s 0m0.000s\n0m0.000s 0m0.000s\n");
9705        self.vm.state.last_status = 0;
9706    }
9707
9708    fn emit_pipeline_timing(&mut self, posix_format: bool, elapsed_seconds: f64) {
9709        let output = if posix_format {
9710            format!("real {elapsed_seconds:.3}\nuser 0.000\nsys 0.000\n")
9711        } else {
9712            let minutes = (elapsed_seconds / 60.0).floor() as u64;
9713            let seconds = elapsed_seconds - (minutes as f64 * 60.0);
9714            format!("real\t{minutes}m{seconds:.3}s\nuser\t0m0.000s\nsys\t0m0.000s\n")
9715        };
9716        self.write_stderr(output.as_bytes());
9717    }
9718
9719    fn execute_dirs(&mut self) {
9720        let mut dirs = vec![self.vm.state.cwd.clone()];
9721        dirs.extend(self.vm.state.dir_stack.iter().map(ToString::to_string));
9722        self.write_stdout(format!("{}\n", dirs.join(" ")).as_bytes());
9723        self.vm.state.last_status = 0;
9724    }
9725
9726    fn execute_pushd(&mut self, argv: &[String]) {
9727        let target = if let Some(path) = argv.get(1) {
9728            path.clone()
9729        } else if let Some(path) = self.vm.state.dir_stack.first() {
9730            path.to_string()
9731        } else {
9732            self.write_stderr(b"pushd: no other directory\n");
9733            self.vm.state.last_status = 1;
9734            return;
9735        };
9736
9737        let old_cwd = self.vm.state.cwd.clone();
9738        if !self.change_directory(&target) {
9739            return;
9740        }
9741        self.vm
9742            .state
9743            .dir_stack
9744            .insert(0, smol_str::SmolStr::from(old_cwd.as_str()));
9745        self.execute_dirs();
9746    }
9747
9748    fn execute_popd(&mut self) {
9749        let Some(target) = self.vm.state.dir_stack.first().cloned() else {
9750            self.write_stderr(b"popd: directory stack empty\n");
9751            self.vm.state.last_status = 1;
9752            return;
9753        };
9754        self.vm.state.dir_stack.remove(0);
9755        if !self.change_directory(&target) {
9756            return;
9757        }
9758        self.execute_dirs();
9759    }
9760
9761    fn execute_umask(&mut self, argv: &[String]) {
9762        if argv.len() <= 1 {
9763            self.write_stdout(format!("{:03o}\n", self.vm.state.umask).as_bytes());
9764            self.vm.state.last_status = 0;
9765            return;
9766        }
9767
9768        let value = argv[1].trim_start_matches('0');
9769        let value = if value.is_empty() { "0" } else { value };
9770        if let Ok(value) = u32::from_str_radix(value, 8) {
9771            self.vm.state.umask = value;
9772            self.vm.state.last_status = 0;
9773        } else {
9774            self.write_stderr(b"umask: invalid mode\n");
9775            self.vm.state.last_status = 1;
9776        }
9777    }
9778
9779    fn execute_wait(&mut self, argv: &[String]) {
9780        if argv.len() <= 1 {
9781            self.vm.state.last_status = 0;
9782            return;
9783        }
9784
9785        let mut status = 0;
9786        for arg in &argv[1..] {
9787            let Ok(pid) = arg.parse::<u32>() else {
9788                self.write_stderr(format!("wait: {arg}: not a pid or valid job spec\n").as_bytes());
9789                status = 1;
9790                continue;
9791            };
9792            if self.vm.state.last_background_pid != Some(pid) {
9793                self.write_stderr(
9794                    format!("wait: pid {pid} is not a child of this shell\n").as_bytes(),
9795                );
9796                status = 127;
9797            }
9798        }
9799        self.vm.state.last_status = status;
9800    }
9801
9802    fn execute_ulimit(&mut self, argv: &[String]) {
9803        if argv.len() <= 1 || argv.get(1).is_some_and(|arg| arg == "-a") {
9804            self.write_stdout(b"unlimited\n");
9805        }
9806        self.vm.state.last_status = 0;
9807    }
9808
9809    /// Execute `mapfile`/`readarray` — read stdin lines into an indexed array.
9810    /// Supports the common Bash flags needed by scripts in the sandbox model.
9811    fn execute_mapfile(&mut self, argv: &[String]) {
9812        let Ok(opts) = Self::parse_mapfile_args(&argv[1..]) else {
9813            self.vm.state.last_status = 1;
9814            return;
9815        };
9816        if opts.fd != 0 {
9817            self.write_stderr(b"wasmsh: mapfile: only file descriptor 0 is supported\n");
9818            self.vm.state.last_status = 1;
9819            return;
9820        }
9821
9822        let name_key = smol_str::SmolStr::from(opts.array_name.as_str());
9823        if opts.origin == 0
9824            || !matches!(
9825                self.vm
9826                    .state
9827                    .env
9828                    .get(name_key.as_str())
9829                    .map(|var| &var.value),
9830                Some(wasmsh_state::VarValue::IndexedArray(_))
9831            )
9832        {
9833            self.vm.state.init_indexed_array(name_key.clone());
9834        }
9835
9836        let Ok(bytes) = self.read_pending_input_bytes("mapfile") else {
9837            return;
9838        };
9839        self.populate_mapfile_array(&name_key, &bytes.unwrap_or_default(), &opts);
9840        self.vm.state.last_status = 0;
9841    }
9842
9843    fn parse_mapfile_args(args: &[String]) -> Result<MapfileOptions, ()> {
9844        let mut opts = MapfileOptions {
9845            strip_delimiter: false,
9846            delimiter: b'\n',
9847            count: None,
9848            origin: 0,
9849            skip: 0,
9850            fd: 0,
9851            array_name: "MAPFILE".to_string(),
9852        };
9853        let mut i = 0usize;
9854        while i < args.len() {
9855            match args[i].as_str() {
9856                "-t" => opts.strip_delimiter = true,
9857                "-d" => {
9858                    i += 1;
9859                    let Some(value) = args.get(i) else {
9860                        return Err(());
9861                    };
9862                    opts.delimiter = value.as_bytes().first().copied().unwrap_or(0);
9863                }
9864                "-n" => {
9865                    i += 1;
9866                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
9867                        return Err(());
9868                    };
9869                    opts.count = Some(value);
9870                }
9871                "-O" => {
9872                    i += 1;
9873                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
9874                        return Err(());
9875                    };
9876                    opts.origin = value;
9877                }
9878                "-s" => {
9879                    i += 1;
9880                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<usize>().ok()) else {
9881                        return Err(());
9882                    };
9883                    opts.skip = value;
9884                }
9885                "-u" => {
9886                    i += 1;
9887                    let Some(value) = args.get(i).and_then(|arg| arg.parse::<u32>().ok()) else {
9888                        return Err(());
9889                    };
9890                    opts.fd = value;
9891                }
9892                "-C" | "-c" => {
9893                    i += 1;
9894                    if args.get(i).is_none() {
9895                        return Err(());
9896                    }
9897                }
9898                value if value.starts_with('-') && value.len() > 1 => {}
9899                value => opts.array_name = value.to_string(),
9900            }
9901            i += 1;
9902        }
9903        Ok(opts)
9904    }
9905
9906    fn populate_mapfile_array(
9907        &mut self,
9908        name_key: &smol_str::SmolStr,
9909        text: &[u8],
9910        opts: &MapfileOptions,
9911    ) {
9912        let mut records = Vec::new();
9913        let mut current = Vec::new();
9914        for &byte in text {
9915            if byte == opts.delimiter {
9916                if !opts.strip_delimiter {
9917                    current.push(byte);
9918                }
9919                records.push(std::mem::take(&mut current));
9920            } else {
9921                current.push(byte);
9922            }
9923        }
9924        if !current.is_empty() {
9925            records.push(current);
9926        }
9927
9928        for (offset, record) in records
9929            .into_iter()
9930            .skip(opts.skip)
9931            .take(opts.count.unwrap_or(usize::MAX))
9932            .enumerate()
9933        {
9934            let value = String::from_utf8_lossy(&record).to_string();
9935            self.vm.state.set_array_element(
9936                name_key.clone(),
9937                &(opts.origin + offset).to_string(),
9938                smol_str::SmolStr::from(value.as_str()),
9939            );
9940        }
9941    }
9942
9943    fn change_directory(&mut self, target: &str) -> bool {
9944        let path = self.resolve_cwd_path(target);
9945        match self.fs.stat(&path) {
9946            Ok(meta) if meta.is_dir => {
9947                let old_pwd = self.vm.state.cwd.clone();
9948                self.vm.state.cwd.clone_from(&path);
9949                self.vm
9950                    .state
9951                    .set_var("OLDPWD".into(), smol_str::SmolStr::from(old_pwd.as_str()));
9952                self.vm
9953                    .state
9954                    .set_var("PWD".into(), smol_str::SmolStr::from(path.as_str()));
9955                self.vm.state.last_status = 0;
9956                true
9957            }
9958            Ok(_) => {
9959                self.write_stderr(format!("wasmsh: {target}: Not a directory\n").as_bytes());
9960                self.vm.state.last_status = 1;
9961                false
9962            }
9963            Err(_) => {
9964                self.write_stderr(
9965                    format!("wasmsh: {target}: No such file or directory\n").as_bytes(),
9966                );
9967                self.vm.state.last_status = 1;
9968                false
9969            }
9970        }
9971    }
9972
9973    /// Search `$PATH` directories in the VFS for a file. Returns the first match.
9974    fn search_path_for_file(&self, filename: &str) -> Option<String> {
9975        let path_var = self.vm.state.get_var("PATH")?;
9976        for dir in path_var.split(':') {
9977            if dir.is_empty() {
9978                continue;
9979            }
9980            let candidate = format!("{dir}/{filename}");
9981            let full = self.resolve_cwd_path(&candidate);
9982            if self.fs.stat(&full).is_ok() {
9983                return Some(full);
9984            }
9985        }
9986        None
9987    }
9988
9989    fn should_errexit(&self, and_or: &HirAndOr) -> bool {
9990        !self.exec.errexit_suppressed
9991            && and_or.rest.is_empty()
9992            && !and_or.first.negated
9993            && self.vm.state.get_var("SHOPT_e").as_deref() == Some("1")
9994            && self.vm.state.last_status != 0
9995            && self.exec.exit_requested.is_none()
9996    }
9997
9998    /// Execute `let expr1 expr2 ...` — evaluate each as arithmetic.
9999    /// Exit status: 0 if the last expression is non-zero, 1 if zero.
10000    fn execute_let(&mut self, argv: &[String]) {
10001        if argv.len() < 2 {
10002            self.vm
10003                .stderr
10004                .extend_from_slice(b"let: expression expected\n");
10005            self.vm.state.last_status = 1;
10006            return;
10007        }
10008        let mut last_val: i64 = 0;
10009        for expr in &argv[1..] {
10010            last_val = wasmsh_expand::eval_arithmetic(expr, &mut self.vm.state);
10011        }
10012        self.vm.state.last_status = i32::from(last_val == 0);
10013    }
10014
10015    /// Known `shopt` option names.
10016    const SHOPT_OPTIONS: &'static [&'static str] = &[
10017        "extglob",
10018        "nullglob",
10019        "dotglob",
10020        "globstar",
10021        "nocasematch",
10022        "nocaseglob",
10023        "failglob",
10024        "lastpipe",
10025        "expand_aliases",
10026        "sourcepath",
10027    ];
10028
10029    /// Execute `shopt [-s|-u] [optname ...]`.
10030    fn execute_shopt(&mut self, argv: &[String]) {
10031        let (set_mode, names) = Self::parse_shopt_args(&argv[1..]);
10032        if let Some(enable) = set_mode {
10033            self.shopt_set_options(&names, enable);
10034        } else {
10035            self.shopt_print_options(&names);
10036        }
10037    }
10038
10039    fn parse_shopt_args(args: &[String]) -> (Option<bool>, Vec<&str>) {
10040        let mut set_mode = None;
10041        let mut names = Vec::new();
10042
10043        for arg in args {
10044            match arg.as_str() {
10045                "-s" => set_mode = Some(true),
10046                "-u" => set_mode = Some(false),
10047                _ => names.push(arg.as_str()),
10048            }
10049        }
10050
10051        (set_mode, names)
10052    }
10053
10054    /// Set shopt options (`-s` or `-u`).
10055    fn shopt_set_options(&mut self, names: &[&str], enable: bool) {
10056        if names.is_empty() {
10057            self.vm
10058                .stderr
10059                .extend_from_slice(b"shopt: option name required\n");
10060            self.vm.state.last_status = 1;
10061            return;
10062        }
10063        let val = if enable { "1" } else { "0" };
10064        for name in names {
10065            if self.reject_invalid_shopt_name(name) {
10066                return;
10067            }
10068            self.set_shopt_value(name, val);
10069        }
10070        self.vm.state.last_status = 0;
10071    }
10072
10073    /// Print shopt option statuses. If `names` is empty, print all.
10074    fn shopt_print_options(&mut self, names: &[&str]) {
10075        let options_to_print: Vec<&str> = if names.is_empty() {
10076            Self::SHOPT_OPTIONS.to_vec()
10077        } else {
10078            names.to_vec()
10079        };
10080        for name in &options_to_print {
10081            if self.reject_invalid_shopt_name(name) {
10082                return;
10083            }
10084            let enabled = self.get_shopt_value(name);
10085            let status_str = if enabled { "on" } else { "off" };
10086            let line = format!("{name}\t{status_str}\n");
10087            self.write_stdout(line.as_bytes());
10088        }
10089        self.vm.state.last_status = 0;
10090    }
10091
10092    fn reject_invalid_shopt_name(&mut self, name: &str) -> bool {
10093        if Self::SHOPT_OPTIONS.contains(&name) {
10094            return false;
10095        }
10096
10097        let msg = format!("shopt: {name}: invalid shell option name\n");
10098        self.write_stderr(msg.as_bytes());
10099        self.vm.state.last_status = 1;
10100        true
10101    }
10102
10103    fn shopt_var_name(name: &str) -> String {
10104        format!("SHOPT_{name}")
10105    }
10106
10107    fn set_shopt_value(&mut self, name: &str, value: &str) {
10108        let var = Self::shopt_var_name(name);
10109        self.vm.state.set_var(
10110            smol_str::SmolStr::from(var.as_str()),
10111            smol_str::SmolStr::from(value),
10112        );
10113    }
10114
10115    fn get_shopt_value(&self, name: &str) -> bool {
10116        let var = Self::shopt_var_name(name);
10117        self.vm.state.get_var(&var).as_deref() == Some("1")
10118    }
10119
10120    fn is_set_option_enabled(&self, flag: char) -> bool {
10121        let var = format!("SHOPT_{flag}");
10122        self.vm.state.get_var(&var).as_deref() == Some("1")
10123    }
10124
10125    fn maybe_write_verbose_input(&mut self, input: &str, cc: &HirCompleteCommand) {
10126        if !self.is_set_option_enabled('v') {
10127            return;
10128        }
10129        let start = cc.span.start as usize;
10130        let end = cc.span.end as usize;
10131        let Some(snippet) = input.get(start..end) else {
10132            return;
10133        };
10134        if snippet.is_empty() {
10135            return;
10136        }
10137        self.write_stderr(snippet.as_bytes());
10138        if !snippet.ends_with('\n') {
10139            self.write_stderr(b"\n");
10140        }
10141    }
10142
10143    /// Execute `declare`/`typeset` with flag parsing.
10144    /// Supports: -i, -a, -A, -x, -r, -l, -u, -p, -n, name=value.
10145    fn execute_declare(&mut self, argv: &[String]) {
10146        let (flags, names) = parse_declare_flags(argv);
10147
10148        if flags.is_print || flags.is_functions || flags.is_function_names {
10149            self.declare_print(argv, &names);
10150            return;
10151        }
10152
10153        for &idx in &names {
10154            self.declare_one_name(argv, idx, &flags);
10155        }
10156        self.vm.state.last_status = 0;
10157    }
10158
10159    /// Handle `declare -p` printing.
10160    fn declare_print(&mut self, argv: &[String], names: &[usize]) {
10161        let (flags, _) = parse_declare_flags(argv);
10162        if flags.is_functions || flags.is_function_names {
10163            self.declare_print_functions(argv, names, flags.is_function_names);
10164            return;
10165        }
10166        self.declare_print_vars(argv, names);
10167    }
10168
10169    fn declare_print_functions(&mut self, argv: &[String], names: &[usize], names_only: bool) {
10170        let function_names: Vec<String> = if names.is_empty() {
10171            self.functions.keys().cloned().collect()
10172        } else {
10173            names.iter().map(|&idx| argv[idx].clone()).collect()
10174        };
10175        for name in function_names {
10176            if !self.functions.contains_key(name.as_str()) {
10177                continue;
10178            }
10179            let line = if names_only {
10180                format!("declare -f {name}\n")
10181            } else {
10182                format!("{name} () {{ :; }}\n")
10183            };
10184            self.write_stdout(line.as_bytes());
10185        }
10186        self.vm.state.last_status = 0;
10187    }
10188
10189    fn declare_print_vars(&mut self, argv: &[String], names: &[usize]) {
10190        if names.is_empty() {
10191            let vars: Vec<(String, String)> = self
10192                .vm
10193                .state
10194                .env
10195                .scopes
10196                .iter()
10197                .flat_map(|scope| {
10198                    scope
10199                        .iter()
10200                        .map(|(n, v)| (n.to_string(), v.value.as_scalar().to_string()))
10201                })
10202                .collect();
10203            for (name, val) in &vars {
10204                let line = format!("declare -- {name}=\"{val}\"\n");
10205                self.write_stdout(line.as_bytes());
10206            }
10207        } else {
10208            for &idx in names {
10209                let name_arg = &argv[idx];
10210                let name = name_arg
10211                    .find('=')
10212                    .map_or(name_arg.as_str(), |eq| &name_arg[..eq]);
10213                if let Some(var) = self.vm.state.env.get(name) {
10214                    let val = var.value.as_scalar();
10215                    let line = format!("declare -- {name}=\"{val}\"\n");
10216                    self.write_stdout(line.as_bytes());
10217                }
10218            }
10219        }
10220        self.vm.state.last_status = 0;
10221    }
10222
10223    /// Process a single name in a `declare`/`typeset` command.
10224    fn declare_one_name(&mut self, argv: &[String], idx: usize, flags: &DeclareFlags) {
10225        let name_arg = &argv[idx];
10226        let (name, value) = if let Some(eq) = name_arg.find('=') {
10227            (&name_arg[..eq], Some(&name_arg[eq + 1..]))
10228        } else {
10229            (name_arg.as_str(), None)
10230        };
10231
10232        if flags.is_assoc {
10233            self.vm
10234                .state
10235                .init_assoc_array(smol_str::SmolStr::from(name));
10236        } else if flags.is_indexed {
10237            self.vm
10238                .state
10239                .init_indexed_array(smol_str::SmolStr::from(name));
10240        }
10241
10242        if let Some(val) = value {
10243            self.declare_assign_value(name, val, flags);
10244        } else if !flags.is_assoc && !flags.is_indexed && self.vm.state.get_var(name).is_none() {
10245            self.vm
10246                .state
10247                .set_var(smol_str::SmolStr::from(name), smol_str::SmolStr::default());
10248        }
10249
10250        self.declare_apply_attributes(name, flags);
10251
10252        if flags.is_nameref {
10253            self.declare_apply_nameref(name);
10254        }
10255    }
10256
10257    /// Assign a value in `declare`, handling compound arrays and scalar transforms.
10258    fn declare_assign_value(&mut self, name: &str, val: &str, flags: &DeclareFlags) {
10259        let trimmed = val.trim();
10260        if trimmed.starts_with('(') && trimmed.ends_with(')') {
10261            self.declare_assign_compound(name, &trimmed[1..trimmed.len() - 1], flags);
10262            return;
10263        }
10264        let final_val = Self::transform_declare_scalar(trimmed, flags, &mut self.vm.state);
10265        self.vm.state.set_var(
10266            smol_str::SmolStr::from(name),
10267            smol_str::SmolStr::from(final_val.as_str()),
10268        );
10269    }
10270
10271    fn declare_assign_compound(&mut self, name: &str, inner: &str, flags: &DeclareFlags) {
10272        let name_key = smol_str::SmolStr::from(name);
10273        if flags.is_assoc || inner.contains("]=") {
10274            self.declare_assign_assoc_compound(&name_key, inner);
10275        } else {
10276            self.declare_assign_indexed_compound(&name_key, inner);
10277        }
10278    }
10279
10280    fn declare_assign_assoc_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
10281        self.vm.state.init_assoc_array(name_key.clone());
10282        for pair in Self::parse_assoc_pairs(inner) {
10283            self.vm.state.set_array_element(
10284                name_key.clone(),
10285                &pair.0,
10286                smol_str::SmolStr::from(pair.1.as_str()),
10287            );
10288        }
10289    }
10290
10291    fn declare_assign_indexed_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
10292        let elements = Self::parse_array_elements(inner);
10293        self.vm.state.init_indexed_array(name_key.clone());
10294        for (i, elem) in elements.iter().enumerate() {
10295            self.vm
10296                .state
10297                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
10298        }
10299    }
10300
10301    fn transform_declare_scalar(val: &str, flags: &DeclareFlags, state: &mut ShellState) -> String {
10302        if flags.is_integer {
10303            wasmsh_expand::eval_arithmetic(val, state).to_string()
10304        } else if flags.is_lower {
10305            val.to_lowercase()
10306        } else if flags.is_upper {
10307            val.to_uppercase()
10308        } else {
10309            val.to_string()
10310        }
10311    }
10312
10313    /// Apply export, readonly, integer attributes after declare assignment.
10314    fn declare_apply_attributes(&mut self, name: &str, flags: &DeclareFlags) {
10315        if let Some(var) = self.vm.state.env.get_mut(name) {
10316            if flags.is_export {
10317                var.exported = true;
10318            }
10319            if flags.is_readonly {
10320                var.readonly = true;
10321            }
10322            if flags.is_integer {
10323                var.integer = true;
10324            }
10325        }
10326    }
10327
10328    /// Apply nameref attribute for `declare -n`.
10329    fn declare_apply_nameref(&mut self, name: &str) {
10330        let target_value = if let Some(eq_pos) = name.find('=') {
10331            smol_str::SmolStr::from(&name[eq_pos + 1..])
10332        } else if let Some(var) = self.vm.state.env.get(name) {
10333            var.value.as_scalar()
10334        } else {
10335            smol_str::SmolStr::default()
10336        };
10337        let actual_name = name.find('=').map_or(name, |eq| &name[..eq]);
10338        self.vm.state.env.set(
10339            smol_str::SmolStr::from(actual_name),
10340            wasmsh_state::ShellVar {
10341                value: wasmsh_state::VarValue::Scalar(target_value),
10342                exported: false,
10343                readonly: false,
10344                integer: false,
10345                nameref: true,
10346            },
10347        );
10348    }
10349
10350    fn should_stop_execution(&self) -> bool {
10351        self.exec.break_depth > 0
10352            || self.exec.loop_continue
10353            || self.exec.exit_requested.is_some()
10354            || self.exec.resource_exhausted
10355    }
10356
10357    /// Check resource limits (step budget, output limit, cancellation).
10358    /// Returns true if execution should stop. Emits a diagnostic on first violation.
10359    fn check_resource_limits(&mut self) -> bool {
10360        if self.exec.resource_exhausted {
10361            return true;
10362        }
10363        if self.vm.begin_step().is_err() {
10364            self.exec.resource_exhausted = true;
10365            self.exec.stop_reason = self.vm.stop_reason().cloned();
10366            return true;
10367        }
10368        false
10369    }
10370
10371    fn execute_body(&mut self, body: &[HirCompleteCommand]) {
10372        for cc in body {
10373            if self.should_stop_execution() || self.check_resource_limits() {
10374                break;
10375            }
10376            if self.is_set_option_enabled('n') {
10377                continue;
10378            }
10379            self.execute_complete_command(cc);
10380        }
10381    }
10382
10383    fn execute_complete_command(&mut self, cc: &HirCompleteCommand) {
10384        for and_or in &cc.list {
10385            if self.should_stop_execution() || self.is_set_option_enabled('n') {
10386                break;
10387            }
10388            self.execute_and_or(and_or);
10389            if self.exec.exit_requested.is_some() {
10390                break;
10391            }
10392            self.handle_post_and_or(and_or);
10393        }
10394    }
10395
10396    /// Expand a word value via command substitution and word expansion.
10397    fn expand_assignment_value(&mut self, value: Option<&Word>) -> String {
10398        if let Some(w) = value {
10399            let resolved = self.resolve_command_subst(std::slice::from_ref(w));
10400            wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
10401        } else {
10402            String::new()
10403        }
10404    }
10405
10406    /// Execute a variable assignment, handling array syntax:
10407    /// - `name=(val1 val2 ...)` -- indexed array compound assignment
10408    /// - `name[idx]=val` -- single element assignment
10409    /// - `name+=(val1 val2 ...)` -- array append
10410    /// - Plain `name=val` -- scalar assignment
10411    fn execute_assignment(&mut self, raw_name: &smol_str::SmolStr, value: Option<&Word>) {
10412        let (name_str, is_append) = Self::split_assignment_name(raw_name.as_str());
10413        if self.try_assign_array_element(name_str, value) {
10414            return;
10415        }
10416
10417        let val_str = self.expand_assignment_value(value);
10418        let trimmed = val_str.trim();
10419        if trimmed.starts_with('(') && trimmed.ends_with(')') {
10420            self.assign_compound_array(name_str, trimmed, is_append);
10421            return;
10422        }
10423
10424        let final_val = self.resolve_scalar_assignment_value(name_str, &val_str, is_append);
10425        self.vm
10426            .state
10427            .set_var(smol_str::SmolStr::from(name_str), final_val.into());
10428    }
10429
10430    fn split_assignment_name(name: &str) -> (&str, bool) {
10431        if let Some(stripped) = name.strip_suffix('+') {
10432            (stripped, true)
10433        } else {
10434            (name, false)
10435        }
10436    }
10437
10438    fn parse_array_element_assignment(name: &str) -> Option<(&str, &str)> {
10439        let bracket_pos = name.find('[')?;
10440        name.ends_with(']')
10441            .then_some((&name[..bracket_pos], &name[bracket_pos + 1..name.len() - 1]))
10442    }
10443
10444    fn try_assign_array_element(&mut self, name: &str, value: Option<&Word>) -> bool {
10445        let Some((base, index)) = Self::parse_array_element_assignment(name) else {
10446            return false;
10447        };
10448        let val = self.expand_assignment_value(value);
10449        self.vm
10450            .state
10451            .set_array_element(smol_str::SmolStr::from(base), index, val.into());
10452        true
10453    }
10454
10455    fn resolve_scalar_assignment_value(
10456        &mut self,
10457        name: &str,
10458        value: &str,
10459        is_append: bool,
10460    ) -> String {
10461        if self.vm.state.env.get(name).is_some_and(|v| v.integer) {
10462            return self.eval_integer_assignment(name, value, is_append);
10463        }
10464        if is_append {
10465            return format!(
10466                "{}{}",
10467                self.vm.state.get_var(name).unwrap_or_default(),
10468                value
10469            );
10470        }
10471        value.to_string()
10472    }
10473
10474    fn eval_integer_assignment(&mut self, name: &str, value: &str, is_append: bool) -> String {
10475        let arith_input = if is_append {
10476            format!(
10477                "{}+{}",
10478                self.vm.state.get_var(name).unwrap_or_default(),
10479                value
10480            )
10481        } else {
10482            value.to_string()
10483        };
10484        wasmsh_expand::eval_arithmetic(&arith_input, &mut self.vm.state).to_string()
10485    }
10486
10487    /// Assign a compound array value `(...)` to a variable.
10488    fn assign_compound_array(&mut self, name_str: &str, val_str: &str, is_append: bool) {
10489        let inner = &val_str[1..val_str.len() - 1];
10490        let elements = Self::parse_array_elements(inner);
10491        let name_key = smol_str::SmolStr::from(name_str);
10492
10493        if is_append {
10494            self.vm.state.append_array(name_str, elements);
10495            return;
10496        }
10497
10498        if Self::is_assoc_array_assignment(inner, &elements) {
10499            self.assign_assoc_array(&name_key, inner);
10500            return;
10501        }
10502        self.assign_indexed_array(&name_key, &elements);
10503    }
10504
10505    fn is_assoc_array_assignment(inner: &str, elements: &[smol_str::SmolStr]) -> bool {
10506        !elements.is_empty() && inner.contains('[') && inner.contains("]=")
10507    }
10508
10509    fn assign_assoc_array(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
10510        self.vm.state.init_assoc_array(name_key.clone());
10511        for (key, value) in Self::parse_assoc_pairs(inner) {
10512            self.vm.state.set_array_element(
10513                name_key.clone(),
10514                &key,
10515                smol_str::SmolStr::from(value.as_str()),
10516            );
10517        }
10518    }
10519
10520    fn assign_indexed_array(
10521        &mut self,
10522        name_key: &smol_str::SmolStr,
10523        elements: &[smol_str::SmolStr],
10524    ) {
10525        self.vm.state.init_indexed_array(name_key.clone());
10526        for (i, elem) in elements.iter().enumerate() {
10527            self.vm
10528                .state
10529                .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
10530        }
10531    }
10532
10533    fn push_array_element(elements: &mut Vec<smol_str::SmolStr>, current: &mut String) {
10534        if current.is_empty() {
10535            return;
10536        }
10537        elements.push(smol_str::SmolStr::from(current.as_str()));
10538        current.clear();
10539    }
10540
10541    /// Parse space-separated array elements from the inner content of `(...)`.
10542    /// Respects quoting (single and double quotes).
10543    fn parse_array_elements(inner: &str) -> Vec<smol_str::SmolStr> {
10544        let mut elements = Vec::new();
10545        let mut current = String::new();
10546        let mut state = ArrayParseState::default();
10547
10548        for ch in inner.chars() {
10549            match state.process_char(ch) {
10550                ArrayCharAction::Append(c) => current.push(c),
10551                ArrayCharAction::Skip => {}
10552                ArrayCharAction::SplitField => {
10553                    Self::push_array_element(&mut elements, &mut current);
10554                }
10555            }
10556        }
10557        Self::push_array_element(&mut elements, &mut current);
10558        elements
10559    }
10560
10561    /// Parse `[key]=value` pairs from associative array compound assignment.
10562    fn parse_assoc_pairs(inner: &str) -> Vec<(String, String)> {
10563        let mut pairs = Vec::new();
10564        let mut pos = 0;
10565        let bytes = inner.as_bytes();
10566
10567        while pos < bytes.len() {
10568            Self::skip_ascii_whitespace(bytes, &mut pos);
10569            if pos >= bytes.len() {
10570                break;
10571            }
10572            if let Some(key) = Self::parse_assoc_key(inner, &mut pos) {
10573                pairs.push((key, Self::parse_assoc_value(inner, &mut pos)));
10574                continue;
10575            }
10576            Self::skip_non_whitespace(bytes, &mut pos);
10577        }
10578        pairs
10579    }
10580
10581    fn skip_ascii_whitespace(bytes: &[u8], pos: &mut usize) {
10582        while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() {
10583            *pos += 1;
10584        }
10585    }
10586
10587    fn skip_non_whitespace(bytes: &[u8], pos: &mut usize) {
10588        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
10589            *pos += 1;
10590        }
10591    }
10592
10593    fn parse_assoc_key(inner: &str, pos: &mut usize) -> Option<String> {
10594        let bytes = inner.as_bytes();
10595        if *pos >= bytes.len() || bytes[*pos] != b'[' {
10596            return None;
10597        }
10598
10599        *pos += 1;
10600        let key_start = *pos;
10601        while *pos < bytes.len() && bytes[*pos] != b']' {
10602            *pos += 1;
10603        }
10604        let key = inner[key_start..*pos].to_string();
10605        if *pos < bytes.len() {
10606            *pos += 1;
10607        }
10608        if *pos < bytes.len() && bytes[*pos] == b'=' {
10609            *pos += 1;
10610        }
10611        Some(key)
10612    }
10613
10614    /// Parse a single value in an associative array assignment (may be quoted).
10615    fn parse_assoc_value(inner: &str, pos: &mut usize) -> String {
10616        let bytes = inner.as_bytes();
10617        match bytes.get(*pos).copied() {
10618            Some(b'"') => Self::parse_double_quoted_assoc_value(bytes, pos),
10619            Some(b'\'') => Self::parse_single_quoted_assoc_value(bytes, pos),
10620            _ => Self::parse_unquoted_assoc_value(bytes, pos),
10621        }
10622    }
10623
10624    fn parse_double_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
10625        let mut value = String::new();
10626        *pos += 1;
10627        while *pos < bytes.len() && bytes[*pos] != b'"' {
10628            if bytes[*pos] == b'\\' && *pos + 1 < bytes.len() {
10629                *pos += 1;
10630            }
10631            value.push(bytes[*pos] as char);
10632            *pos += 1;
10633        }
10634        if *pos < bytes.len() {
10635            *pos += 1;
10636        }
10637        value
10638    }
10639
10640    fn parse_single_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
10641        let mut value = String::new();
10642        *pos += 1;
10643        while *pos < bytes.len() && bytes[*pos] != b'\'' {
10644            value.push(bytes[*pos] as char);
10645            *pos += 1;
10646        }
10647        if *pos < bytes.len() {
10648            *pos += 1;
10649        }
10650        value
10651    }
10652
10653    fn parse_unquoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
10654        let mut value = String::new();
10655        while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
10656            value.push(bytes[*pos] as char);
10657            *pos += 1;
10658        }
10659        value
10660    }
10661
10662    /// Maximum number of arguments after glob expansion.
10663    const MAX_GLOB_RESULTS: usize = 10_000;
10664
10665    /// Expand glob patterns in argv against the VFS.
10666    /// Supports: basic glob (`*`, `?`, `[...]`), globstar (`**`), nullglob,
10667    /// dotglob, and extglob patterns.
10668    /// When `set -f` (noglob) is active, glob expansion is skipped entirely.
10669    /// Expand globs in argv, skipping entries tagged as quoted.
10670    fn expand_globs_tagged(&mut self, argv: Vec<(String, bool)>) -> Vec<String> {
10671        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
10672            return argv.into_iter().map(|(s, _)| s).collect();
10673        }
10674        let nullglob = self.get_shopt_value("nullglob");
10675        let dotglob = self.get_shopt_value("dotglob");
10676        let globstar = self.get_shopt_value("globstar");
10677        let extglob = self.get_shopt_value("extglob");
10678
10679        let mut result = Vec::new();
10680        for (arg, quoted) in argv {
10681            if quoted {
10682                result.push(arg);
10683            } else {
10684                result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
10685            }
10686        }
10687        result.truncate(Self::MAX_GLOB_RESULTS);
10688        result
10689    }
10690
10691    fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
10692        if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
10693            return argv;
10694        }
10695        let nullglob = self.get_shopt_value("nullglob");
10696        let dotglob = self.get_shopt_value("dotglob");
10697        let globstar = self.get_shopt_value("globstar");
10698        let extglob = self.get_shopt_value("extglob");
10699
10700        let mut result = Vec::new();
10701        for arg in argv {
10702            result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
10703        }
10704        result.truncate(Self::MAX_GLOB_RESULTS);
10705        result
10706    }
10707
10708    #[allow(clippy::fn_params_excessive_bools)]
10709    fn expand_glob_arg(
10710        &self,
10711        arg: String,
10712        nullglob: bool,
10713        dotglob: bool,
10714        globstar: bool,
10715        extglob: bool,
10716    ) -> Vec<String> {
10717        if !Self::is_glob_pattern(&arg, extglob) {
10718            return vec![arg];
10719        }
10720        if globstar && arg.contains("**") {
10721            return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
10722        }
10723        self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
10724    }
10725
10726    fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
10727        let has_bracket_class = arg.contains('[') && arg.contains(']');
10728        arg.contains('*')
10729            || arg.contains('?')
10730            || has_bracket_class
10731            || (extglob && has_extglob_pattern(arg))
10732    }
10733
10734    fn expand_globstar_arg(
10735        &self,
10736        arg: String,
10737        nullglob: bool,
10738        dotglob: bool,
10739        extglob: bool,
10740    ) -> Vec<String> {
10741        let mut matches = self.expand_globstar(&arg, dotglob, extglob);
10742        matches.sort();
10743        self.finalize_glob_matches(arg, matches, nullglob)
10744    }
10745
10746    fn expand_standard_glob_arg(
10747        &self,
10748        arg: String,
10749        nullglob: bool,
10750        dotglob: bool,
10751        extglob: bool,
10752    ) -> Vec<String> {
10753        let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
10754            return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
10755        };
10756        let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
10757        self.finalize_glob_matches(arg, matches, nullglob)
10758    }
10759
10760    fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
10761        let Some(slash_pos) = arg.rfind('/') else {
10762            return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
10763        };
10764
10765        let dir_part = &arg[..=slash_pos];
10766        if Self::path_segment_has_glob(dir_part) {
10767            return None;
10768        }
10769
10770        Some((
10771            self.resolve_cwd_path(dir_part),
10772            arg[slash_pos + 1..].to_string(),
10773            Some(dir_part.to_string()),
10774        ))
10775    }
10776
10777    fn path_segment_has_glob(path: &str) -> bool {
10778        path.contains('*') || path.contains('?') || path.contains('[')
10779    }
10780
10781    fn read_glob_matches(
10782        &self,
10783        dir: &str,
10784        pattern: &str,
10785        prefix: Option<&str>,
10786        dotglob: bool,
10787        extglob: bool,
10788    ) -> Vec<String> {
10789        let Ok(entries) = self.fs.read_dir(dir) else {
10790            return Vec::new();
10791        };
10792
10793        let mut matches: Vec<String> = entries
10794            .iter()
10795            .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
10796            .map(|e| match prefix {
10797                Some(prefix) => format!("{prefix}{}", e.name),
10798                None => e.name.clone(),
10799            })
10800            .collect();
10801        matches.sort();
10802        matches
10803    }
10804
10805    #[allow(clippy::unused_self)]
10806    fn finalize_glob_matches(
10807        &self,
10808        arg: String,
10809        matches: Vec<String>,
10810        nullglob: bool,
10811    ) -> Vec<String> {
10812        if !matches.is_empty() {
10813            return matches;
10814        }
10815        if nullglob {
10816            Vec::new()
10817        } else {
10818            vec![arg]
10819        }
10820    }
10821
10822    /// Expand a globstar (**) pattern against the VFS with recursive directory traversal.
10823    fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
10824        // Split pattern into segments by /
10825        let segments: Vec<&str> = pattern.split('/').collect();
10826        let base_dir = self.vm.state.cwd.clone();
10827        let mut matches = Vec::new();
10828        self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
10829        matches
10830    }
10831
10832    /// Recursive walk for globstar expansion.
10833    fn globstar_walk(
10834        &self,
10835        dir: &str,
10836        segments: &[&str],
10837        seg_idx: usize,
10838        prefix: &str,
10839        dotglob: bool,
10840        extglob: bool,
10841        matches: &mut Vec<String>,
10842    ) {
10843        if seg_idx >= segments.len() {
10844            return;
10845        }
10846
10847        let seg = segments[seg_idx];
10848        if seg == "**" {
10849            self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
10850            return;
10851        }
10852        self.globstar_walk_segment(
10853            dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
10854        );
10855    }
10856
10857    fn globstar_walk_wildcard(
10858        &self,
10859        dir: &str,
10860        segments: &[&str],
10861        seg_idx: usize,
10862        prefix: &str,
10863        dotglob: bool,
10864        extglob: bool,
10865        matches: &mut Vec<String>,
10866    ) {
10867        if seg_idx + 1 < segments.len() {
10868            self.globstar_walk(
10869                dir,
10870                segments,
10871                seg_idx + 1,
10872                prefix,
10873                dotglob,
10874                extglob,
10875                matches,
10876            );
10877        }
10878
10879        let Ok(entries) = self.fs.read_dir(dir) else {
10880            return;
10881        };
10882        for entry in &entries {
10883            if !dotglob && entry.name.starts_with('.') {
10884                continue;
10885            }
10886            let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
10887            if self.fs.stat(&child_path).is_ok_and(|m| m.is_dir) {
10888                self.globstar_walk(
10889                    &child_path,
10890                    segments,
10891                    seg_idx,
10892                    &child_prefix,
10893                    dotglob,
10894                    extglob,
10895                    matches,
10896                );
10897            }
10898        }
10899    }
10900
10901    #[allow(clippy::too_many_arguments)]
10902    fn globstar_walk_segment(
10903        &self,
10904        dir: &str,
10905        seg: &str,
10906        segments: &[&str],
10907        seg_idx: usize,
10908        prefix: &str,
10909        dotglob: bool,
10910        extglob: bool,
10911        matches: &mut Vec<String>,
10912    ) {
10913        let Ok(entries) = self.fs.read_dir(dir) else {
10914            return;
10915        };
10916        let is_last = seg_idx == segments.len() - 1;
10917
10918        for entry in &entries {
10919            if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
10920                continue;
10921            }
10922            self.globstar_handle_matched_entry(
10923                dir,
10924                segments,
10925                seg_idx,
10926                prefix,
10927                dotglob,
10928                extglob,
10929                matches,
10930                &entry.name,
10931                is_last,
10932            );
10933        }
10934    }
10935
10936    #[allow(clippy::too_many_arguments)]
10937    fn globstar_handle_matched_entry(
10938        &self,
10939        dir: &str,
10940        segments: &[&str],
10941        seg_idx: usize,
10942        prefix: &str,
10943        dotglob: bool,
10944        extglob: bool,
10945        matches: &mut Vec<String>,
10946        name: &str,
10947        is_last: bool,
10948    ) {
10949        let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
10950        if is_last {
10951            matches.push(child_prefix);
10952            return;
10953        }
10954        let is_dir = self.fs.stat(&child_path).is_ok_and(|m| m.is_dir);
10955        if is_dir {
10956            self.globstar_walk(
10957                &child_path,
10958                segments,
10959                seg_idx + 1,
10960                &child_prefix,
10961                dotglob,
10962                extglob,
10963                matches,
10964            );
10965        }
10966    }
10967
10968    fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
10969        let child_path = if dir == "/" {
10970            format!("/{name}")
10971        } else {
10972            format!("{dir}/{name}")
10973        };
10974        let child_prefix = if prefix.is_empty() {
10975            name.to_string()
10976        } else {
10977            format!("{prefix}/{name}")
10978        };
10979        (child_path, child_prefix)
10980    }
10981
10982    /// Write data to a file path, reporting errors to stderr.
10983    fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
10984        match self.fs.open(path, opts) {
10985            Ok(h) => {
10986                if let Err(e) = self.fs.write_file(h, data) {
10987                    self.write_stderr(format!("wasmsh: write error: {e}\n").as_bytes());
10988                }
10989                self.fs.close(h);
10990            }
10991            Err(e) => {
10992                self.write_stderr(format!("wasmsh: {target}: {e}\n").as_bytes());
10993            }
10994        }
10995    }
10996
10997    fn current_stdout_len(&self) -> usize {
10998        for capture in self.exec.output_captures.iter().rev() {
10999            if capture.capture_stdout {
11000                return capture.stdout.len();
11001            }
11002        }
11003        self.vm.stdout.len()
11004    }
11005
11006    /// Capture stdout data from the given position, truncating the active stdout buffer.
11007    fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
11008        for capture in self.exec.output_captures.iter_mut().rev() {
11009            if capture.capture_stdout {
11010                let data = capture.stdout[from..].to_vec();
11011                capture.stdout.truncate(from);
11012                return data;
11013            }
11014        }
11015
11016        let data = self.vm.stdout[from..].to_vec();
11017        self.vm.stdout.truncate(from);
11018        data
11019    }
11020
11021    /// Drain the active stderr buffer.
11022    fn take_stderr(&mut self) -> Vec<u8> {
11023        for capture in self.exec.output_captures.iter_mut().rev() {
11024            if capture.capture_stderr {
11025                return std::mem::take(&mut capture.stderr);
11026            }
11027        }
11028        std::mem::take(&mut self.vm.stderr)
11029    }
11030
11031    fn process_subst_out_sink_mut(&mut self, path: &str) -> Option<&mut PendingProcessSubstOut> {
11032        for scope in self.proc_subst_out_scopes.iter_mut().rev() {
11033            if let Some(index) = scope.iter().position(|sink| sink.path == path) {
11034                return scope.get_mut(index);
11035            }
11036        }
11037        None
11038    }
11039
11040    fn write_process_subst_out_with_parent(
11041        &mut self,
11042        path: &str,
11043        data: &[u8],
11044        clear: bool,
11045    ) -> bool {
11046        for scope_index in (0..self.proc_subst_out_scopes.len()).rev() {
11047            let maybe_index = self.proc_subst_out_scopes[scope_index]
11048                .iter()
11049                .position(|sink| sink.path == path);
11050            if let Some(index) = maybe_index {
11051                let mut sink = self.proc_subst_out_scopes[scope_index].remove(index);
11052                if clear {
11053                    sink.clear();
11054                }
11055                sink.write_with_parent(self, data);
11056                self.proc_subst_out_scopes[scope_index].insert(index, sink);
11057                return true;
11058            }
11059        }
11060        false
11061    }
11062
11063    fn prepare_exec_io(&mut self, redirections: &[HirRedirection]) -> Result<Option<ExecIo>, ()> {
11064        let mut exec_io = self.current_exec_io.clone().unwrap_or_default();
11065        let mut handled_any = false;
11066        for redir in redirections {
11067            if self.apply_hir_redir(redir, &mut exec_io)? {
11068                handled_any = true;
11069            }
11070        }
11071        Ok(handled_any.then_some(exec_io))
11072    }
11073
11074    fn apply_hir_redir(
11075        &mut self,
11076        redir: &HirRedirection,
11077        exec_io: &mut ExecIo,
11078    ) -> Result<bool, ()> {
11079        match redir.op {
11080            RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
11081                self.apply_heredoc_redir(redir, exec_io);
11082                Ok(true)
11083            }
11084            RedirectionOp::HereString => {
11085                self.apply_herestring_redir(redir, exec_io);
11086                Ok(true)
11087            }
11088            RedirectionOp::Input => self.apply_input_redir(redir, exec_io).map(|()| true),
11089            RedirectionOp::Output
11090            | RedirectionOp::Append
11091            | RedirectionOp::Clobber
11092            | RedirectionOp::AppendBoth => self.apply_write_redir(redir, exec_io).map(|()| true),
11093            RedirectionOp::DupOutput => {
11094                self.apply_dup_output_redir(redir, exec_io);
11095                Ok(true)
11096            }
11097            RedirectionOp::DupInput => {
11098                self.apply_dup_input_redir(redir, exec_io);
11099                Ok(true)
11100            }
11101            _ => Ok(false),
11102        }
11103    }
11104
11105    fn resolve_redir_target(&mut self, redir: &HirRedirection) -> String {
11106        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
11107        let resolved_target = resolved.first().unwrap_or(&redir.target);
11108        wasmsh_expand::expand_word(resolved_target, &mut self.vm.state)
11109    }
11110
11111    fn apply_heredoc_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
11112        if let Some(body) = &redir.here_doc_body {
11113            let expanded = wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
11114            exec_io
11115                .fds_mut()
11116                .set_input(InputTarget::Bytes(expanded.into_bytes()));
11117        }
11118    }
11119
11120    fn apply_herestring_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
11121        let content = self.resolve_redir_target(redir);
11122        let mut data = content.into_bytes();
11123        data.push(b'\n');
11124        exec_io.fds_mut().set_input(InputTarget::Bytes(data));
11125    }
11126
11127    fn apply_input_redir(
11128        &mut self,
11129        redir: &HirRedirection,
11130        exec_io: &mut ExecIo,
11131    ) -> Result<(), ()> {
11132        let target = self.resolve_redir_target(redir);
11133        let path = self.resolve_cwd_path(&target);
11134        match self.fs.stat(&path) {
11135            Ok(metadata) if !metadata.is_dir => {
11136                exec_io.fds_mut().set_input(InputTarget::File {
11137                    path,
11138                    remove_after_read: false,
11139                });
11140                Ok(())
11141            }
11142            Ok(_) => self.fail_input_redir(&target, "Is a directory"),
11143            Err(_) => self.fail_input_redir(&target, "No such file or directory"),
11144        }
11145    }
11146
11147    fn fail_input_redir(&mut self, target: &str, reason: &str) -> Result<(), ()> {
11148        let msg = format!("wasmsh: {target}: {reason}\n");
11149        self.write_stderr(msg.as_bytes());
11150        self.vm.state.last_status = 1;
11151        Err(())
11152    }
11153
11154    fn apply_write_redir(
11155        &mut self,
11156        redir: &HirRedirection,
11157        exec_io: &mut ExecIo,
11158    ) -> Result<(), ()> {
11159        let target = self.resolve_redir_target(redir);
11160        let path = self.resolve_cwd_path(&target);
11161        let append = matches!(redir.op, RedirectionOp::Append | RedirectionOp::AppendBoth);
11162        let clear_before = matches!(redir.op, RedirectionOp::Output | RedirectionOp::Clobber);
11163
11164        if matches!(redir.op, RedirectionOp::Output) && self.noclobber_rejects(&path, &target) {
11165            return Err(());
11166        }
11167
11168        let destination = self.open_write_destination(path, &target, append, clear_before)?;
11169        Self::attach_write_destination(redir, exec_io, destination);
11170        Ok(())
11171    }
11172
11173    fn open_write_destination(
11174        &mut self,
11175        path: String,
11176        target: &str,
11177        append: bool,
11178        clear_before: bool,
11179    ) -> Result<OutputTarget, ()> {
11180        if self.process_subst_out_sink_mut(&path).is_some() {
11181            if clear_before {
11182                if let Some(sink) = self.process_subst_out_sink_mut(&path) {
11183                    sink.clear();
11184                }
11185            }
11186            return Ok(OutputTarget::ProcessSubst { path });
11187        }
11188        match self.fs.open_write_sink(&path, append) {
11189            Ok(sink) => Ok(OutputTarget::File {
11190                path,
11191                append,
11192                sink: Rc::new(RefCell::new(sink)),
11193            }),
11194            Err(err) => {
11195                let msg = format!("wasmsh: {target}: {err}\n");
11196                self.write_stderr(msg.as_bytes());
11197                self.vm.state.last_status = 1;
11198                Err(())
11199            }
11200        }
11201    }
11202
11203    fn attach_write_destination(
11204        redir: &HirRedirection,
11205        exec_io: &mut ExecIo,
11206        destination: OutputTarget,
11207    ) {
11208        let default_fd = if matches!(redir.op, RedirectionOp::AppendBoth) {
11209            FD_BOTH
11210        } else {
11211            1
11212        };
11213        match redir.fd.unwrap_or(default_fd) {
11214            FD_BOTH => {
11215                exec_io.fds_mut().open_output(1, destination.clone());
11216                exec_io.fds_mut().open_output(2, destination);
11217            }
11218            2 => exec_io.fds_mut().open_output(2, destination),
11219            _ => exec_io.fds_mut().open_output(1, destination),
11220        }
11221    }
11222
11223    fn apply_dup_output_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
11224        let target = self.resolve_redir_target(redir);
11225        let source_fd = redir.fd.unwrap_or(1);
11226        if target == "-" {
11227            exec_io.fds_mut().close(source_fd);
11228        } else if let Ok(target_fd) = target.parse() {
11229            exec_io.fds_mut().dup_output(source_fd, target_fd);
11230        }
11231    }
11232
11233    fn apply_dup_input_redir(&mut self, redir: &HirRedirection, exec_io: &mut ExecIo) {
11234        let target = self.resolve_redir_target(redir);
11235        let source_fd = redir.fd.unwrap_or(0);
11236        if target == "-" {
11237            exec_io.fds_mut().close(source_fd);
11238        } else if let Ok(target_fd) = target.parse() {
11239            exec_io.fds_mut().dup_input(source_fd, target_fd);
11240        }
11241    }
11242
11243    /// Apply redirections: for `>` and `>>`, write captured stdout/stderr to file.
11244    /// For `<`, read file content (handled pre-execution).
11245    /// Supports fd-specific redirections (2>, 2>>) and &> (both stdout and stderr).
11246    fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
11247        for redir in redirections {
11248            if !self.apply_single_redirection(redir, stdout_before) {
11249                return;
11250            }
11251        }
11252    }
11253
11254    fn apply_single_redirection(&mut self, redir: &HirRedirection, stdout_before: usize) -> bool {
11255        let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
11256        let resolved_target = resolved.first().unwrap_or(&redir.target);
11257        let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
11258        let path = self.resolve_cwd_path(&target);
11259        let fd = redir.fd.unwrap_or(1);
11260        match redir.op {
11261            RedirectionOp::Output => {
11262                if self.noclobber_rejects(&path, &target) {
11263                    return false;
11264                }
11265                self.apply_output_redir(&path, &target, fd, stdout_before);
11266            }
11267            RedirectionOp::Clobber => {
11268                self.apply_output_redir(&path, &target, fd, stdout_before);
11269            }
11270            RedirectionOp::Append => {
11271                self.apply_append_redir(&path, &target, fd, stdout_before);
11272            }
11273            RedirectionOp::AppendBoth => {
11274                self.apply_append_redir(&path, &target, FD_BOTH, stdout_before);
11275            }
11276            RedirectionOp::DupOutput => {
11277                self.apply_dup_output_redir_inline(redir, &target, stdout_before);
11278            }
11279            #[allow(unreachable_patterns)]
11280            _ => {}
11281        }
11282        true
11283    }
11284
11285    fn apply_dup_output_redir_inline(
11286        &mut self,
11287        redir: &HirRedirection,
11288        target: &str,
11289        stdout_before: usize,
11290    ) {
11291        let source_fd = redir.fd.unwrap_or(1);
11292        if target == "-" {
11293            if source_fd == 2 {
11294                self.take_stderr();
11295            } else {
11296                self.capture_stdout(stdout_before);
11297            }
11298            return;
11299        }
11300        let target_fd = target.parse::<u32>().ok();
11301        if target_fd == Some(1) && source_fd == 2 {
11302            let stderr_data = self.take_stderr();
11303            self.write_stdout(&stderr_data);
11304        } else if target_fd == Some(2) && source_fd == 1 {
11305            let stdout_data = self.capture_stdout(stdout_before);
11306            self.write_stderr(&stdout_data);
11307        }
11308    }
11309
11310    /// Apply `>` output redirection for a specific fd.
11311    fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
11312        let data = if fd == FD_BOTH {
11313            let mut combined = self.capture_stdout(stdout_before);
11314            combined.extend_from_slice(&self.take_stderr());
11315            combined
11316        } else if fd == 2 {
11317            self.take_stderr()
11318        } else {
11319            self.capture_stdout(stdout_before)
11320        };
11321        if self.write_process_subst_out_with_parent(path, &data, true) {
11322            return;
11323        }
11324        self.write_to_file(path, target, &data, OpenOptions::write());
11325    }
11326
11327    /// Apply `>>` append redirection for a specific fd.
11328    fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
11329        let data = if fd == FD_BOTH {
11330            let mut combined = self.capture_stdout(stdout_before);
11331            combined.extend_from_slice(&self.take_stderr());
11332            combined
11333        } else if fd == 2 {
11334            self.take_stderr()
11335        } else {
11336            self.capture_stdout(stdout_before)
11337        };
11338        if self.write_process_subst_out_with_parent(path, &data, false) {
11339            return;
11340        }
11341        self.write_to_file(path, target, &data, OpenOptions::append());
11342    }
11343
11344    fn noclobber_rejects(&mut self, path: &str, target: &str) -> bool {
11345        if self.vm.state.get_var("SHOPT_C").as_deref() != Some("1") {
11346            return false;
11347        }
11348        if self.fs.stat(path).is_err() {
11349            return false;
11350        }
11351        self.write_stderr(format!("wasmsh: {target}: cannot overwrite existing file\n").as_bytes());
11352        self.vm.state.last_status = 1;
11353        true
11354    }
11355}
11356
11357#[cfg(not(target_arch = "wasm32"))]
11358type PipelineStartedAt = std::time::Instant;
11359#[cfg(target_arch = "wasm32")]
11360type PipelineStartedAt = ();
11361
11362#[cfg(not(target_arch = "wasm32"))]
11363fn pipeline_started_at() -> PipelineStartedAt {
11364    std::time::Instant::now()
11365}
11366
11367#[cfg(target_arch = "wasm32")]
11368fn pipeline_started_at() -> PipelineStartedAt {}
11369
11370#[cfg(not(target_arch = "wasm32"))]
11371fn started_elapsed_seconds(started: PipelineStartedAt) -> f64 {
11372    started.elapsed().as_secs_f64()
11373}
11374
11375#[cfg(target_arch = "wasm32")]
11376fn started_elapsed_seconds(_: PipelineStartedAt) -> f64 {
11377    0.0
11378}
11379
11380/// Convert a protocol diagnostic level to a VM diagnostic level.
11381fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
11382    match level {
11383        DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
11384        DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
11385        DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
11386        _ => wasmsh_vm::DiagLevel::Info,
11387    }
11388}
11389
11390impl Default for WorkerRuntime {
11391    fn default() -> Self {
11392        Self::new()
11393    }
11394}
11395
11396#[cfg(test)]
11397mod tests {
11398    use super::*;
11399
11400    fn first_and_or(source: &str) -> HirAndOr {
11401        let ast = wasmsh_parse::parse(source).unwrap();
11402        let hir = wasmsh_hir::lower(&ast);
11403        hir.items[0].list[0].clone()
11404    }
11405
11406    fn get_stdout(events: &[WorkerEvent]) -> String {
11407        let mut out = Vec::new();
11408        for event in events {
11409            if let WorkerEvent::Stdout(data) = event {
11410                out.extend_from_slice(data);
11411            }
11412        }
11413        String::from_utf8(out).unwrap_or_default()
11414    }
11415
11416    fn get_stderr(events: &[WorkerEvent]) -> String {
11417        let mut out = Vec::new();
11418        for event in events {
11419            if let WorkerEvent::Stderr(data) = event {
11420                out.extend_from_slice(data);
11421            }
11422        }
11423        String::from_utf8(out).unwrap_or_default()
11424    }
11425
11426    fn get_exit(events: &[WorkerEvent]) -> i32 {
11427        events
11428            .iter()
11429            .find_map(|event| match event {
11430                WorkerEvent::Exit(status) => Some(*status),
11431                _ => None,
11432            })
11433            .unwrap_or(-1)
11434    }
11435
11436    fn has_output_limit_diagnostic(events: &[WorkerEvent]) -> bool {
11437        events.iter().any(|event| {
11438            matches!(
11439                event,
11440                WorkerEvent::Diagnostic(_, message) if message.contains("output limit exceeded")
11441            )
11442        })
11443    }
11444
11445    #[test]
11446    fn output_limit_exposes_structured_exhaustion_reason() {
11447        let mut runtime = WorkerRuntime::new();
11448        runtime.handle_command(HostCommand::Init {
11449            step_budget: 0,
11450            allowed_hosts: vec![],
11451        });
11452        runtime.set_output_byte_limit(3);
11453
11454        let events = runtime.handle_command(HostCommand::Run {
11455            input: "echo hello".into(),
11456        });
11457
11458        assert_eq!(get_exit(&events), 128);
11459        assert!(has_output_limit_diagnostic(&events));
11460        assert_eq!(
11461            runtime.exec.stop_reason,
11462            Some(StopReason::Exhausted(ExhaustionReason {
11463                category: BudgetCategory::VisibleOutputBytes,
11464                used: 6,
11465                limit: 3,
11466            }))
11467        );
11468    }
11469
11470    #[test]
11471    fn recursion_limit_exposes_structured_exhaustion_reason() {
11472        let mut runtime = WorkerRuntime::new();
11473        runtime.handle_command(HostCommand::Init {
11474            step_budget: 0,
11475            allowed_hosts: vec![],
11476        });
11477        runtime.set_recursion_limit(2);
11478
11479        let events = runtime.handle_command(HostCommand::Run {
11480            input: "f(){ f; }\nf".into(),
11481        });
11482
11483        assert_eq!(get_exit(&events), 128);
11484        assert!(get_stderr(&events).contains("maximum recursion depth exceeded"));
11485        assert_eq!(
11486            runtime.exec.stop_reason,
11487            Some(StopReason::Exhausted(ExhaustionReason {
11488                category: BudgetCategory::RecursionDepth,
11489                used: 3,
11490                limit: 2,
11491            }))
11492        );
11493    }
11494
11495    #[test]
11496    fn pipe_limit_exposes_structured_exhaustion_reason() {
11497        let mut runtime = WorkerRuntime::new();
11498        runtime.handle_command(HostCommand::Init {
11499            step_budget: 0,
11500            allowed_hosts: vec![],
11501        });
11502        runtime.set_pipe_byte_limit(1);
11503
11504        let events = runtime.handle_command(HostCommand::Run {
11505            input: "printf 'ab' | cat".into(),
11506        });
11507
11508        assert_eq!(get_exit(&events), 128);
11509        assert!(events.iter().any(|event| {
11510            matches!(
11511                event,
11512                WorkerEvent::Diagnostic(_, message) if message.contains("pipe buffer limit exceeded")
11513            )
11514        }));
11515        assert!(matches!(
11516            runtime.exec.stop_reason,
11517            Some(StopReason::Exhausted(ExhaustionReason {
11518                category: BudgetCategory::PipeBytes,
11519                ..
11520            }))
11521        ));
11522    }
11523
11524    #[test]
11525    fn vm_subset_boundary_accepts_simple_builtin_and_or() {
11526        let runtime = WorkerRuntime::new();
11527        let program = runtime
11528            .lower_vm_subset_and_or(&first_and_or("true && echo ok"))
11529            .expect("simple builtin and/or should lower");
11530        assert!(!program.instructions.is_empty());
11531    }
11532
11533    #[test]
11534    fn vm_subset_boundary_rejects_multi_stage_pipeline() {
11535        let runtime = WorkerRuntime::new();
11536        let reason = runtime
11537            .lower_vm_subset_and_or(&first_and_or("echo hello | cat"))
11538            .unwrap_err();
11539        assert_eq!(
11540            reason,
11541            VmSubsetFallbackReason::Lowering(LoweringError::Unsupported(
11542                "pipeline shape is outside the VM subset"
11543            ))
11544        );
11545    }
11546
11547    #[test]
11548    fn vm_subset_boundary_rejects_alias_expansion() {
11549        let mut runtime = WorkerRuntime::new();
11550        runtime
11551            .vm
11552            .state
11553            .set_var("SHOPT_expand_aliases".into(), "1".into());
11554        runtime.aliases.insert("echo".into(), "printf".into());
11555        let reason = runtime
11556            .lower_vm_subset_and_or(&first_and_or("echo hello"))
11557            .unwrap_err();
11558        assert_eq!(reason, VmSubsetFallbackReason::AliasExpansion);
11559    }
11560
11561    #[test]
11562    fn streaming_yes_head_respects_visible_output_limit() {
11563        let mut runtime = WorkerRuntime::new();
11564        runtime.handle_command(HostCommand::Init {
11565            step_budget: 0,
11566            allowed_hosts: vec![],
11567        });
11568        runtime.vm.limits.output_byte_limit = 10;
11569
11570        let events = runtime.handle_command(HostCommand::Run {
11571            input: "yes | head -n 5".into(),
11572        });
11573
11574        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
11575        assert!(!has_output_limit_diagnostic(&events));
11576    }
11577
11578    #[test]
11579    fn streaming_yes_cat_head_respects_visible_output_limit() {
11580        let mut runtime = WorkerRuntime::new();
11581        runtime.handle_command(HostCommand::Init {
11582            step_budget: 0,
11583            allowed_hosts: vec![],
11584        });
11585        runtime.vm.limits.output_byte_limit = 10;
11586
11587        let events = runtime.handle_command(HostCommand::Run {
11588            input: "yes | cat | head -n 5".into(),
11589        });
11590
11591        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
11592        assert!(!has_output_limit_diagnostic(&events));
11593    }
11594
11595    #[test]
11596    fn streaming_yes_head_wc_respects_visible_output_limit() {
11597        let mut runtime = WorkerRuntime::new();
11598        runtime.handle_command(HostCommand::Init {
11599            step_budget: 0,
11600            allowed_hosts: vec![],
11601        });
11602        runtime.vm.limits.output_byte_limit = 8;
11603
11604        let events = runtime.handle_command(HostCommand::Run {
11605            input: "yes | head -n 5 | wc -l".into(),
11606        });
11607
11608        assert_eq!(get_stdout(&events), "5\n");
11609        assert!(!has_output_limit_diagnostic(&events));
11610    }
11611
11612    #[test]
11613    fn streaming_cat_file_head_respects_visible_output_limit() {
11614        let mut runtime = WorkerRuntime::new();
11615        runtime.handle_command(HostCommand::Init {
11616            step_budget: 0,
11617            allowed_hosts: vec![],
11618        });
11619        runtime.handle_command(HostCommand::WriteFile {
11620            path: "/big.txt".into(),
11621            data: b"abcdefghijklmnopqrstuvwxyz".to_vec(),
11622        });
11623        runtime.vm.limits.output_byte_limit = 10;
11624
11625        let events = runtime.handle_command(HostCommand::Run {
11626            input: "cat /big.txt | head -c 10".into(),
11627        });
11628
11629        assert_eq!(get_stdout(&events), "abcdefghij");
11630        assert!(!has_output_limit_diagnostic(&events));
11631    }
11632
11633    #[test]
11634    fn streaming_yes_tr_head_respects_visible_output_limit() {
11635        let mut runtime = WorkerRuntime::new();
11636        runtime.handle_command(HostCommand::Init {
11637            step_budget: 0,
11638            allowed_hosts: vec![],
11639        });
11640        runtime.vm.limits.output_byte_limit = 10;
11641
11642        let events = runtime.handle_command(HostCommand::Run {
11643            input: "yes | tr y z | head -n 5".into(),
11644        });
11645
11646        assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
11647        assert!(!has_output_limit_diagnostic(&events));
11648    }
11649
11650    #[test]
11651    fn streaming_yes_grep_head_respects_visible_output_limit() {
11652        let mut runtime = WorkerRuntime::new();
11653        runtime.handle_command(HostCommand::Init {
11654            step_budget: 0,
11655            allowed_hosts: vec![],
11656        });
11657        runtime.vm.limits.output_byte_limit = 10;
11658
11659        let events = runtime.handle_command(HostCommand::Run {
11660            input: "yes | grep y | head -n 5".into(),
11661        });
11662
11663        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
11664        assert!(!has_output_limit_diagnostic(&events));
11665    }
11666
11667    #[test]
11668    fn streaming_yes_tee_head_respects_visible_output_limit() {
11669        let mut runtime = WorkerRuntime::new();
11670        runtime.handle_command(HostCommand::Init {
11671            step_budget: 0,
11672            allowed_hosts: vec![],
11673        });
11674        runtime.vm.limits.output_byte_limit = 10;
11675
11676        let events = runtime.handle_command(HostCommand::Run {
11677            input: "yes | tee /tee.txt | head -n 5".into(),
11678        });
11679
11680        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
11681        assert!(!has_output_limit_diagnostic(&events));
11682
11683        let file_events = runtime.handle_command(HostCommand::ReadFile {
11684            path: "/tee.txt".into(),
11685        });
11686        assert_eq!(get_stdout(&file_events), "y\ny\ny\ny\ny\n");
11687    }
11688
11689    #[test]
11690    fn streaming_buffered_sort_tee_cat_preserves_sorted_output() {
11691        let mut runtime = WorkerRuntime::new();
11692        runtime.handle_command(HostCommand::Init {
11693            step_budget: 0,
11694            allowed_hosts: vec![],
11695        });
11696
11697        let events = runtime.handle_command(HostCommand::Run {
11698            input: "printf 'b\\na\\n' | sort | tee /sorted.txt | cat".into(),
11699        });
11700
11701        assert_eq!(get_stdout(&events), "a\nb\n");
11702        let file_events = runtime.handle_command(HostCommand::ReadFile {
11703            path: "/sorted.txt".into(),
11704        });
11705        assert_eq!(get_stdout(&file_events), "a\nb\n");
11706    }
11707
11708    #[test]
11709    fn streaming_yes_rev_head_respects_visible_output_limit() {
11710        let mut runtime = WorkerRuntime::new();
11711        runtime.handle_command(HostCommand::Init {
11712            step_budget: 0,
11713            allowed_hosts: vec![],
11714        });
11715        runtime.vm.limits.output_byte_limit = 10;
11716
11717        let events = runtime.handle_command(HostCommand::Run {
11718            input: "yes | rev | head -n 5".into(),
11719        });
11720
11721        assert_eq!(get_stdout(&events), "y\ny\ny\ny\ny\n");
11722        assert!(!has_output_limit_diagnostic(&events));
11723    }
11724
11725    #[test]
11726    fn streaming_echo_cut_head_respects_visible_output_limit() {
11727        let mut runtime = WorkerRuntime::new();
11728        runtime.handle_command(HostCommand::Init {
11729            step_budget: 0,
11730            allowed_hosts: vec![],
11731        });
11732        runtime.vm.limits.output_byte_limit = 6;
11733
11734        let events = runtime.handle_command(HostCommand::Run {
11735            input: "echo abc:def | cut -d: -f2 | head -c 4".into(),
11736        });
11737
11738        assert_eq!(get_stdout(&events), "def\n");
11739        assert!(!has_output_limit_diagnostic(&events));
11740    }
11741
11742    #[test]
11743    fn streaming_echo_tail_head_respects_visible_output_limit() {
11744        let mut runtime = WorkerRuntime::new();
11745        runtime.handle_command(HostCommand::Init {
11746            step_budget: 0,
11747            allowed_hosts: vec![],
11748        });
11749        runtime.vm.limits.output_byte_limit = 3;
11750
11751        let events = runtime.handle_command(HostCommand::Run {
11752            input: "echo -e 'a\\nb\\nc' | tail -n 2 | head -n 1".into(),
11753        });
11754
11755        assert_eq!(get_stdout(&events), "b\n");
11756        assert!(!has_output_limit_diagnostic(&events));
11757    }
11758
11759    #[test]
11760    fn streaming_yes_bat_head_respects_visible_output_limit() {
11761        let mut runtime = WorkerRuntime::new();
11762        runtime.handle_command(HostCommand::Init {
11763            step_budget: 0,
11764            allowed_hosts: vec![],
11765        });
11766        let expected = "    1   │ y\n    2   │ y\n";
11767        runtime.vm.limits.output_byte_limit = expected.len() as u64;
11768
11769        let events = runtime.handle_command(HostCommand::Run {
11770            input: "yes | bat --style=numbers | head -n 2".into(),
11771        });
11772
11773        assert_eq!(get_stdout(&events), expected);
11774        assert!(!has_output_limit_diagnostic(&events));
11775    }
11776
11777    #[test]
11778    fn streaming_yes_sed_head_respects_visible_output_limit() {
11779        let mut runtime = WorkerRuntime::new();
11780        runtime.handle_command(HostCommand::Init {
11781            step_budget: 0,
11782            allowed_hosts: vec![],
11783        });
11784        runtime.vm.limits.output_byte_limit = 10;
11785
11786        let events = runtime.handle_command(HostCommand::Run {
11787            input: "yes | sed 's/y/z/' | head -n 5".into(),
11788        });
11789
11790        assert_eq!(get_stdout(&events), "z\nz\nz\nz\nz\n");
11791        assert!(!has_output_limit_diagnostic(&events));
11792    }
11793
11794    #[test]
11795    fn streaming_echo_paste_serial_head_respects_visible_output_limit() {
11796        let mut runtime = WorkerRuntime::new();
11797        runtime.handle_command(HostCommand::Init {
11798            step_budget: 0,
11799            allowed_hosts: vec![],
11800        });
11801        runtime.vm.limits.output_byte_limit = 6;
11802
11803        let events = runtime.handle_command(HostCommand::Run {
11804            input: "echo -e 'a\\nb\\nc' | paste -s -d , | head -c 6".into(),
11805        });
11806
11807        assert_eq!(get_stdout(&events), "a,b,c\n");
11808        assert!(!has_output_limit_diagnostic(&events));
11809    }
11810
11811    #[test]
11812    fn streaming_echo_column_head_respects_visible_output_limit() {
11813        let mut runtime = WorkerRuntime::new();
11814        runtime.handle_command(HostCommand::Init {
11815            step_budget: 0,
11816            allowed_hosts: vec![],
11817        });
11818        runtime.vm.limits.output_byte_limit = 4;
11819
11820        let events = runtime.handle_command(HostCommand::Run {
11821            input: "echo abc | column | head -c 4".into(),
11822        });
11823
11824        assert_eq!(get_stdout(&events), "abc\n");
11825        assert!(!has_output_limit_diagnostic(&events));
11826    }
11827
11828    #[test]
11829    fn streaming_echo_uniq_head_respects_visible_output_limit() {
11830        let mut runtime = WorkerRuntime::new();
11831        runtime.handle_command(HostCommand::Init {
11832            step_budget: 0,
11833            allowed_hosts: vec![],
11834        });
11835        runtime.vm.limits.output_byte_limit = 6;
11836
11837        let events = runtime.handle_command(HostCommand::Run {
11838            input: "echo -e 'a\\na\\nb' | uniq | head -n 2".into(),
11839        });
11840
11841        assert_eq!(get_stdout(&events), "a\nb\n");
11842        assert!(!has_output_limit_diagnostic(&events));
11843    }
11844
11845    #[test]
11846    fn streaming_buffered_printf_sort_head_respects_visible_output_limit() {
11847        let mut runtime = WorkerRuntime::new();
11848        runtime.handle_command(HostCommand::Init {
11849            step_budget: 0,
11850            allowed_hosts: vec![],
11851        });
11852        runtime.vm.limits.output_byte_limit = 2;
11853
11854        let events = runtime.handle_command(HostCommand::Run {
11855            input: "printf 'b\\na\\n' | sort | head -n 1".into(),
11856        });
11857
11858        assert_eq!(get_stdout(&events), "a\n");
11859        assert!(!has_output_limit_diagnostic(&events));
11860    }
11861
11862    #[test]
11863    fn streaming_buffered_function_stage_preserves_output() {
11864        let mut runtime = WorkerRuntime::new();
11865        runtime.handle_command(HostCommand::Init {
11866            step_budget: 0,
11867            allowed_hosts: vec![],
11868        });
11869
11870        let events = runtime.handle_command(HostCommand::Run {
11871            input: "f(){ cat; }\nprintf hi | f | head -c 2".into(),
11872        });
11873
11874        assert_eq!(get_stdout(&events), "hi");
11875        assert!(!has_output_limit_diagnostic(&events));
11876    }
11877
11878    #[test]
11879    fn streaming_buffered_function_pipe_stderr_respects_visible_output_limit() {
11880        let mut runtime = WorkerRuntime::new();
11881        runtime.handle_command(HostCommand::Init {
11882            step_budget: 0,
11883            allowed_hosts: vec![],
11884        });
11885        runtime.vm.limits.output_byte_limit = 8;
11886
11887        let events = runtime.handle_command(HostCommand::Run {
11888            input: "f(){ echo out; echo err >&2; }\nf |& head -n 2".into(),
11889        });
11890
11891        assert_eq!(get_stdout(&events), "out\nerr\n");
11892        assert!(!has_output_limit_diagnostic(&events));
11893    }
11894
11895    #[test]
11896    fn scheduled_group_stage_pipe_stderr_preserves_output() {
11897        let mut runtime = WorkerRuntime::new();
11898        runtime.handle_command(HostCommand::Init {
11899            step_budget: 0,
11900            allowed_hosts: vec![],
11901        });
11902
11903        let events = runtime.handle_command(HostCommand::Run {
11904            input: "printf x | { cat; echo err >&2; } |& cat".into(),
11905        });
11906
11907        let stdout = get_stdout(&events);
11908        assert!(stdout.contains('x'));
11909        assert!(stdout.contains("err"));
11910    }
11911
11912    #[test]
11913    fn streaming_tee_pipe_stderr_preserves_output_and_stage_status() {
11914        let mut runtime = WorkerRuntime::new();
11915        runtime.handle_command(HostCommand::Init {
11916            step_budget: 0,
11917            allowed_hosts: vec![],
11918        });
11919
11920        let events = runtime.handle_command(HostCommand::Run {
11921            input: "printf x | tee / |& cat\necho ${PIPESTATUS[*]}".into(),
11922        });
11923
11924        let stdout = get_stdout(&events);
11925        assert!(stdout.contains('x'));
11926        assert!(stdout.contains("tee: /: is a directory: /"));
11927        assert!(stdout.contains("0 1 0"));
11928        assert_eq!(get_stderr(&events), "");
11929    }
11930
11931    #[test]
11932    fn streaming_tee_pipe_stderr_respects_pipefail() {
11933        let mut runtime = WorkerRuntime::new();
11934        runtime.handle_command(HostCommand::Init {
11935            step_budget: 0,
11936            allowed_hosts: vec![],
11937        });
11938
11939        let events = runtime.handle_command(HostCommand::Run {
11940            input: "set -o pipefail\nprintf x | tee / |& cat".into(),
11941        });
11942
11943        assert_eq!(runtime.vm.state.last_status, 1);
11944        let stdout = get_stdout(&events);
11945        assert!(stdout.contains('x'));
11946        assert!(stdout.contains("tee: /: is a directory: /"));
11947    }
11948
11949    #[test]
11950    fn generic_pipeline_capture_does_not_count_hidden_stage_output() {
11951        let mut runtime = WorkerRuntime::new();
11952        runtime.handle_command(HostCommand::Init {
11953            step_budget: 0,
11954            allowed_hosts: vec![],
11955        });
11956        runtime.vm.limits.output_byte_limit = 2;
11957
11958        let events = runtime.handle_command(HostCommand::Run {
11959            input: "echo -e 'a\\nb' | grep b".into(),
11960        });
11961
11962        assert_eq!(get_stdout(&events), "b\n");
11963        assert!(!has_output_limit_diagnostic(&events));
11964    }
11965
11966    #[test]
11967    fn generic_pipeline_file_capture_preserves_redirection_behavior() {
11968        let mut runtime = WorkerRuntime::new();
11969        runtime.handle_command(HostCommand::Init {
11970            step_budget: 0,
11971            allowed_hosts: vec![],
11972        });
11973
11974        let events = runtime.handle_command(HostCommand::Run {
11975            input: "echo -e 'a\\nb' | grep b >/filtered.txt | wc -l".into(),
11976        });
11977
11978        assert_eq!(get_stdout(&events), "0\n");
11979
11980        let file_events = runtime.handle_command(HostCommand::ReadFile {
11981            path: "/filtered.txt".into(),
11982        });
11983        assert_eq!(get_stdout(&file_events), "b\n");
11984    }
11985
11986    #[test]
11987    fn scheduler_single_redirect_only_command_creates_target_file() {
11988        let mut runtime = WorkerRuntime::new();
11989        runtime.handle_command(HostCommand::Init {
11990            step_budget: 0,
11991            allowed_hosts: vec![],
11992        });
11993
11994        let events = runtime.handle_command(HostCommand::Run {
11995            input: "> /created.txt".into(),
11996        });
11997
11998        assert_eq!(runtime.vm.state.last_status, 0);
11999        assert_eq!(get_stdout(&events), "");
12000        assert_eq!(get_stderr(&events), "");
12001
12002        let file_events = runtime.handle_command(HostCommand::ReadFile {
12003            path: "/created.txt".into(),
12004        });
12005        assert_eq!(get_stdout(&file_events), "");
12006    }
12007
12008    #[test]
12009    fn command_substitution_keeps_inner_stderr_visible() {
12010        let mut runtime = WorkerRuntime::new();
12011        runtime.handle_command(HostCommand::Init {
12012            step_budget: 0,
12013            allowed_hosts: vec![],
12014        });
12015
12016        let events = runtime.handle_command(HostCommand::Run {
12017            input: "echo $(printf 'hello'; echo err >&2)".into(),
12018        });
12019
12020        assert_eq!(get_stdout(&events), "hello\n");
12021        assert_eq!(get_stderr(&events), "err\n");
12022    }
12023
12024    #[test]
12025    fn command_substitution_isolates_shell_state() {
12026        let mut runtime = WorkerRuntime::new();
12027        runtime.handle_command(HostCommand::Init {
12028            step_budget: 0,
12029            allowed_hosts: vec![],
12030        });
12031
12032        let events = runtime.handle_command(HostCommand::Run {
12033            input: "foo=before; echo $(foo=after; printf hi); echo $foo".into(),
12034        });
12035
12036        assert_eq!(get_stdout(&events), "hi\nbefore\n");
12037    }
12038
12039    #[test]
12040    fn process_substitution_out_feeds_inner_command() {
12041        let mut runtime = WorkerRuntime::new();
12042        runtime.handle_command(HostCommand::Init {
12043            step_budget: 0,
12044            allowed_hosts: vec![],
12045        });
12046
12047        let events = runtime.handle_command(HostCommand::Run {
12048            input: "printf hi > >(cat)".into(),
12049        });
12050
12051        assert_eq!(get_stdout(&events), "hi");
12052        assert_eq!(get_stderr(&events), "");
12053        assert_eq!(runtime.vm.state.last_status, 0);
12054    }
12055
12056    #[test]
12057    fn process_substitution_out_runs_schedulable_inner_pipeline() {
12058        let mut runtime = WorkerRuntime::new();
12059        runtime.handle_command(HostCommand::Init {
12060            step_budget: 0,
12061            allowed_hosts: vec![],
12062        });
12063
12064        let events = runtime.handle_command(HostCommand::Run {
12065            input: "printf 'a\\nb\\n' > >(head -n 1 | cat)".into(),
12066        });
12067
12068        assert_eq!(get_stdout(&events), "a\n");
12069        assert_eq!(get_stderr(&events), "");
12070        assert_eq!(runtime.vm.state.last_status, 0);
12071    }
12072
12073    #[test]
12074    fn process_substitution_out_runs_live_tail_pipeline() {
12075        let mut runtime = WorkerRuntime::new();
12076        runtime.handle_command(HostCommand::Init {
12077            step_budget: 0,
12078            allowed_hosts: vec![],
12079        });
12080
12081        runtime.proc_subst_out_scopes.push(Vec::new());
12082        let path = runtime.register_process_subst_out("tail -n 1 | cat");
12083
12084        {
12085            let sink = runtime
12086                .process_subst_out_sink_mut(&path)
12087                .expect("registered process substitution sink");
12088            match &sink.mode {
12089                PendingProcessSubstOutMode::Live { .. } => {}
12090                PendingProcessSubstOutMode::Buffered { .. } => {
12091                    panic!("expected live process substitution runner")
12092                }
12093            }
12094            sink.write(b"a\nb\n");
12095        }
12096
12097        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
12098        runtime.flush_process_subst_out_scope(scope);
12099        assert_eq!(runtime.vm.stdout, b"b\n");
12100    }
12101
12102    #[test]
12103    fn process_substitution_out_runs_live_buffered_pipeline() {
12104        let mut runtime = WorkerRuntime::new();
12105        runtime.handle_command(HostCommand::Init {
12106            step_budget: 0,
12107            allowed_hosts: vec![],
12108        });
12109
12110        runtime.proc_subst_out_scopes.push(Vec::new());
12111        let path = runtime.register_process_subst_out("sort | cat");
12112
12113        {
12114            let sink = runtime
12115                .process_subst_out_sink_mut(&path)
12116                .expect("registered process substitution sink");
12117            match &sink.mode {
12118                PendingProcessSubstOutMode::Live { runner } => {
12119                    assert!(runner.isolated_runtime.is_some());
12120                }
12121                PendingProcessSubstOutMode::Buffered { .. } => {
12122                    panic!("expected live buffered process substitution runner")
12123                }
12124            }
12125            sink.write(b"b\na\n");
12126        }
12127
12128        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
12129        runtime.flush_process_subst_out_scope(scope);
12130        assert_eq!(runtime.vm.stdout, b"a\nb\n");
12131    }
12132
12133    #[test]
12134    fn process_substitution_in_registers_live_reader_and_cleans_up() {
12135        let mut runtime = WorkerRuntime::new();
12136        runtime.handle_command(HostCommand::Init {
12137            step_budget: 0,
12138            allowed_hosts: vec![],
12139        });
12140
12141        runtime.proc_subst_in_scopes.push(Vec::new());
12142        let path = runtime
12143            .execute_process_subst_in("yes | head -n 2")
12144            .to_string();
12145        assert!(runtime.fs.stat(&path).is_ok());
12146
12147        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
12148        assert_eq!(get_stdout(&file), "y\ny\n");
12149        assert!(runtime.fs.stat(&path).is_err());
12150
12151        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
12152        runtime.flush_process_subst_in_scope(scope);
12153        assert!(runtime.fs.stat(&path).is_err());
12154    }
12155
12156    #[test]
12157    fn process_substitution_in_registers_live_sed_reader_and_cleans_up() {
12158        let mut runtime = WorkerRuntime::new();
12159        runtime.handle_command(HostCommand::Init {
12160            step_budget: 0,
12161            allowed_hosts: vec![],
12162        });
12163
12164        runtime.proc_subst_in_scopes.push(Vec::new());
12165        let path = runtime
12166            .execute_process_subst_in("yes | sed 's/y/z/' | head -n 2")
12167            .to_string();
12168        assert!(runtime.fs.stat(&path).is_ok());
12169
12170        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
12171        assert_eq!(get_stdout(&file), "z\nz\n");
12172        assert!(runtime.fs.stat(&path).is_err());
12173
12174        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
12175        runtime.flush_process_subst_in_scope(scope);
12176        assert!(runtime.fs.stat(&path).is_err());
12177    }
12178
12179    #[test]
12180    fn process_substitution_in_runs_live_buffered_reader_and_cleans_up() {
12181        let mut runtime = WorkerRuntime::new();
12182        runtime.handle_command(HostCommand::Init {
12183            step_budget: 0,
12184            allowed_hosts: vec![],
12185        });
12186
12187        runtime.proc_subst_in_scopes.push(Vec::new());
12188        let path = runtime
12189            .execute_process_subst_in("printf 'b\\na\\n' | sort")
12190            .to_string();
12191
12192        assert!(runtime.fs.stat(&path).is_ok());
12193        let file = runtime.handle_command(HostCommand::ReadFile { path: path.clone() });
12194        assert_eq!(get_stdout(&file), "a\nb\n");
12195        assert!(runtime.fs.stat(&path).is_err());
12196
12197        let scope = runtime.proc_subst_in_scopes.pop().unwrap_or_default();
12198        runtime.flush_process_subst_in_scope(scope);
12199        assert!(runtime.fs.stat(&path).is_err());
12200    }
12201
12202    #[test]
12203    fn live_process_substitution_runner_consumes_before_flush() {
12204        let mut runtime = WorkerRuntime::new();
12205        runtime.handle_command(HostCommand::Init {
12206            step_budget: 0,
12207            allowed_hosts: vec![],
12208        });
12209
12210        runtime.proc_subst_out_scopes.push(Vec::new());
12211        let path = runtime.register_process_subst_out("head -n 1 | cat");
12212
12213        {
12214            let sink = runtime
12215                .process_subst_out_sink_mut(&path)
12216                .expect("registered process substitution sink");
12217            sink.write(b"a\nb\n");
12218            match &sink.mode {
12219                PendingProcessSubstOutMode::Live { runner } => {
12220                    assert_eq!(runner.captured_stdout, b"a\n");
12221                }
12222                PendingProcessSubstOutMode::Buffered { .. } => {
12223                    panic!("expected live process substitution runner")
12224                }
12225            }
12226        }
12227
12228        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
12229        runtime.flush_process_subst_out_scope(scope);
12230        assert_eq!(runtime.vm.stdout, b"a\n");
12231    }
12232
12233    #[test]
12234    fn live_process_substitution_runner_tee_writes_before_flush() {
12235        let mut runtime = WorkerRuntime::new();
12236        runtime.handle_command(HostCommand::Init {
12237            step_budget: 0,
12238            allowed_hosts: vec![],
12239        });
12240
12241        runtime.proc_subst_out_scopes.push(Vec::new());
12242        let path = runtime.register_process_subst_out("tee /tee.txt | cat");
12243
12244        {
12245            let sink = runtime
12246                .process_subst_out_sink_mut(&path)
12247                .expect("registered process substitution sink");
12248            sink.write(b"a\nb\n");
12249            match &sink.mode {
12250                PendingProcessSubstOutMode::Live { runner } => {
12251                    assert!(runner.captured_stdout.starts_with(b"a\nb"));
12252                }
12253                PendingProcessSubstOutMode::Buffered { .. } => {
12254                    panic!("expected live process substitution runner")
12255                }
12256            }
12257        }
12258
12259        let file = runtime.handle_command(HostCommand::ReadFile {
12260            path: "/tee.txt".into(),
12261        });
12262        assert!(get_stdout(&file).starts_with("a\nb"));
12263
12264        let scope = runtime.proc_subst_out_scopes.pop().unwrap_or_default();
12265        runtime.flush_process_subst_out_scope(scope);
12266        assert_eq!(runtime.vm.stdout, b"a\nb\n");
12267
12268        let file = runtime.handle_command(HostCommand::ReadFile {
12269            path: "/tee.txt".into(),
12270        });
12271        assert_eq!(get_stdout(&file), "a\nb\n");
12272    }
12273
12274    #[test]
12275    fn exec_live_redirections_preserve_left_to_right_dup_order() {
12276        let mut runtime = WorkerRuntime::new();
12277        runtime.handle_command(HostCommand::Init {
12278            step_budget: 0,
12279            allowed_hosts: vec![],
12280        });
12281
12282        let events = runtime.handle_command(HostCommand::Run {
12283            input: "printf hi > /first.txt 1>&2\nprintf hi 1>&2 > /second.txt".into(),
12284        });
12285
12286        assert_eq!(get_stdout(&events), "");
12287        assert_eq!(get_stderr(&events), "hi");
12288
12289        let first = runtime.handle_command(HostCommand::ReadFile {
12290            path: "/first.txt".into(),
12291        });
12292        assert_eq!(get_stdout(&first), "");
12293
12294        let second = runtime.handle_command(HostCommand::ReadFile {
12295            path: "/second.txt".into(),
12296        });
12297        assert_eq!(get_stdout(&second), "hi");
12298    }
12299
12300    #[test]
12301    fn exec_process_subst_redirections_preserve_left_to_right_dup_order() {
12302        let mut runtime = WorkerRuntime::new();
12303        runtime.handle_command(HostCommand::Init {
12304            step_budget: 0,
12305            allowed_hosts: vec![],
12306        });
12307
12308        let events = runtime.handle_command(HostCommand::Run {
12309            input: "printf hi > >(cat) 1>&2\nprintf hi 1>&2 > >(cat)".into(),
12310        });
12311
12312        assert_eq!(get_stdout(&events), "hi");
12313        assert_eq!(get_stderr(&events), "hi");
12314    }
12315
12316    #[test]
12317    fn builtin_and_utility_redirections_write_files_during_execution() {
12318        let mut runtime = WorkerRuntime::new();
12319        runtime.handle_command(HostCommand::Init {
12320            step_budget: 0,
12321            allowed_hosts: vec![],
12322        });
12323
12324        let events = runtime.handle_command(HostCommand::Run {
12325            input: "type printf > /builtin.txt\nprintf hi > /utility.txt".into(),
12326        });
12327
12328        let status = events
12329            .iter()
12330            .find_map(|event| {
12331                if let WorkerEvent::Exit(code) = event {
12332                    Some(*code)
12333                } else {
12334                    None
12335                }
12336            })
12337            .unwrap_or(-1);
12338        assert_eq!(status, 0);
12339        assert_eq!(get_stdout(&events), "");
12340        assert_eq!(get_stderr(&events), "");
12341
12342        let builtin = runtime.handle_command(HostCommand::ReadFile {
12343            path: "/builtin.txt".into(),
12344        });
12345        assert!(get_stdout(&builtin).contains("printf"));
12346
12347        let utility = runtime.handle_command(HostCommand::ReadFile {
12348            path: "/utility.txt".into(),
12349        });
12350        assert_eq!(get_stdout(&utility), "hi");
12351    }
12352
12353    #[test]
12354    fn special_param_underscore_uses_previous_command_last_argument() {
12355        let mut runtime = WorkerRuntime::new();
12356        runtime.handle_command(HostCommand::Init {
12357            step_budget: 0,
12358            allowed_hosts: vec![],
12359        });
12360
12361        let first = runtime.handle_command(HostCommand::Run {
12362            input: "echo alpha beta".into(),
12363        });
12364        assert_eq!(get_stdout(&first), "alpha beta\n");
12365        assert_eq!(runtime.vm.state.get_var("_").as_deref(), Some("beta"));
12366
12367        let events = runtime.handle_command(HostCommand::Run {
12368            input: "echo \"last=$_\"".into(),
12369        });
12370
12371        assert_eq!(get_stdout(&events), "last=beta\n");
12372        assert_eq!(runtime.vm.state.get_var("_").as_deref(), Some("last=beta"));
12373    }
12374
12375    #[test]
12376    fn amp_append_redirection_appends_stdout_and_stderr_for_simple_command() {
12377        let mut runtime = WorkerRuntime::new();
12378        runtime.handle_command(HostCommand::Init {
12379            step_budget: 0,
12380            allowed_hosts: vec![],
12381        });
12382
12383        let setup = runtime.handle_command(HostCommand::WriteFile {
12384            path: "/log.txt".into(),
12385            data: b"old\n".to_vec(),
12386        });
12387        assert_eq!(get_stderr(&setup), "");
12388
12389        let events = runtime.handle_command(HostCommand::Run {
12390            input: "f(){ printf 'out\\n'; printf 'err\\n' >&2; }\nf &>> /log.txt\ncat /log.txt"
12391                .into(),
12392        });
12393
12394        assert_eq!(get_stdout(&events), "old\nout\nerr\n");
12395        assert_eq!(get_stderr(&events), "");
12396    }
12397
12398    #[test]
12399    fn clobber_redirection_overrides_noclobber() {
12400        let mut runtime = WorkerRuntime::new();
12401        runtime.handle_command(HostCommand::Init {
12402            step_budget: 0,
12403            allowed_hosts: vec![],
12404        });
12405
12406        let setup = runtime.handle_command(HostCommand::WriteFile {
12407            path: "/existing.txt".into(),
12408            data: b"old\n".to_vec(),
12409        });
12410        assert_eq!(get_stderr(&setup), "");
12411
12412        let events = runtime.handle_command(HostCommand::Run {
12413            input: "set -o noclobber\necho blocked > /existing.txt\ncat /existing.txt\necho force >| /existing.txt\ncat /existing.txt".into(),
12414        });
12415
12416        assert_eq!(get_stdout(&events), "old\nforce\n");
12417        assert!(get_stderr(&events).contains("cannot overwrite existing file"));
12418    }
12419}