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