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