1use indexmap::IndexMap;
7
8use wasmsh_ast::CaseTerminator;
9use wasmsh_ast::RedirectionOp;
10use wasmsh_expand::expand_words_argv;
11use wasmsh_fs::{BackendFs, OpenOptions, Vfs};
12use wasmsh_hir::{
13 HirAndOr, HirAndOrOp, HirCommand, HirCompleteCommand, HirPipeline, HirRedirection,
14};
15use wasmsh_protocol::{DiagnosticLevel, HostCommand, WorkerEvent, PROTOCOL_VERSION};
16use wasmsh_state::ShellState;
17use wasmsh_utils::{UtilContext, UtilRegistry, VecOutput as UtilOutput};
18use wasmsh_vm::Vm;
19
20const FD_BOTH: u32 = u32::MAX;
22
23const CMD_LOCAL: &str = "local";
25const CMD_BREAK: &str = "break";
26const CMD_CONTINUE: &str = "continue";
27const CMD_EXIT: &str = "exit";
28const CMD_EVAL: &str = "eval";
29const CMD_SOURCE: &str = "source";
30const CMD_DOT: &str = ".";
31const CMD_DECLARE: &str = "declare";
32const CMD_TYPESET: &str = "typeset";
33const CMD_LET: &str = "let";
34const CMD_SHOPT: &str = "shopt";
35const CMD_ALIAS: &str = "alias";
36const CMD_UNALIAS: &str = "unalias";
37const CMD_BUILTIN: &str = "builtin";
38const CMD_MAPFILE: &str = "mapfile";
39const CMD_READARRAY: &str = "readarray";
40const CMD_TYPE: &str = "type";
41
42#[derive(Debug, Clone)]
44pub struct BrowserConfig {
45 pub step_budget: u64,
46 pub allowed_hosts: Vec<String>,
48}
49
50impl Default for BrowserConfig {
51 fn default() -> Self {
52 Self {
53 step_budget: 100_000,
54 allowed_hosts: Vec::new(),
55 }
56 }
57}
58
59const MAX_RECURSION_DEPTH: u32 = 100;
61
62struct ExecState {
64 break_depth: u32,
65 loop_continue: bool,
66 exit_requested: Option<i32>,
67 errexit_suppressed: bool,
68 local_save_stack: Vec<(smol_str::SmolStr, Option<smol_str::SmolStr>)>,
69 recursion_depth: u32,
70 resource_exhausted: bool,
72}
73
74impl ExecState {
75 fn new() -> Self {
76 Self {
77 break_depth: 0,
78 loop_continue: false,
79 exit_requested: None,
80 errexit_suppressed: false,
81 local_save_stack: Vec::new(),
82 recursion_depth: 0,
83 resource_exhausted: false,
84 }
85 }
86
87 fn reset(&mut self) {
88 self.break_depth = 0;
89 self.loop_continue = false;
90 self.exit_requested = None;
91 self.errexit_suppressed = false;
92 self.resource_exhausted = false;
93 }
94}
95
96#[derive(Debug)]
98pub struct ExternalCommandResult {
99 pub stdout: Vec<u8>,
101 pub stderr: Vec<u8>,
103 pub status: i32,
105}
106
107pub type ExternalCommandHandler =
112 Box<dyn FnMut(&str, &[String], Option<&[u8]>) -> Option<ExternalCommandResult>>;
113
114#[allow(missing_debug_implementations)]
116pub struct WorkerRuntime {
117 config: BrowserConfig,
118 vm: Vm,
119 fs: BackendFs,
120 utils: UtilRegistry,
121 builtins: wasmsh_builtins::BuiltinRegistry,
122 initialized: bool,
123 pending_stdin: Option<Vec<u8>>,
125 functions: IndexMap<String, HirCommand>,
127 exec: ExecState,
129 aliases: IndexMap<String, String>,
131 external_handler: Option<ExternalCommandHandler>,
133 network: Option<Box<dyn wasmsh_utils::net_types::NetworkBackend>>,
135}
136
137enum ArrayCharAction {
139 Append(char),
140 Skip,
141 SplitField,
142}
143
144#[derive(Default)]
146struct ArrayParseState {
147 in_single_quote: bool,
148 in_double_quote: bool,
149 escape_next: bool,
150}
151
152impl ArrayParseState {
153 fn process_char(&mut self, ch: char) -> ArrayCharAction {
154 if self.escape_next {
155 self.escape_next = false;
156 return ArrayCharAction::Append(ch);
157 }
158 if ch == '\\' && !self.in_single_quote {
159 self.escape_next = true;
160 return ArrayCharAction::Skip;
161 }
162 if ch == '\'' && !self.in_double_quote {
163 self.in_single_quote = !self.in_single_quote;
164 return ArrayCharAction::Skip;
165 }
166 if ch == '"' && !self.in_single_quote {
167 self.in_double_quote = !self.in_double_quote;
168 return ArrayCharAction::Skip;
169 }
170 if ch.is_ascii_whitespace() && !self.in_single_quote && !self.in_double_quote {
171 return ArrayCharAction::SplitField;
172 }
173 ArrayCharAction::Append(ch)
174 }
175}
176
177#[allow(clippy::struct_excessive_bools)]
179struct DeclareFlags {
180 is_assoc: bool,
181 is_indexed: bool,
182 is_integer: bool,
183 is_export: bool,
184 is_readonly: bool,
185 is_lower: bool,
186 is_upper: bool,
187 is_print: bool,
188 is_nameref: bool,
189}
190
191fn parse_declare_flags(argv: &[String]) -> (DeclareFlags, Vec<usize>) {
193 let mut flags = DeclareFlags {
194 is_assoc: false,
195 is_indexed: false,
196 is_integer: false,
197 is_export: false,
198 is_readonly: false,
199 is_lower: false,
200 is_upper: false,
201 is_print: false,
202 is_nameref: false,
203 };
204 let mut names = Vec::new();
205
206 for (i, arg) in argv[1..].iter().enumerate() {
207 if arg.starts_with('-') && arg.len() > 1 {
208 for ch in arg[1..].chars() {
209 match ch {
210 'A' => flags.is_assoc = true,
211 'a' => flags.is_indexed = true,
212 'i' => flags.is_integer = true,
213 'x' => flags.is_export = true,
214 'r' => flags.is_readonly = true,
215 'l' => flags.is_lower = true,
216 'u' => flags.is_upper = true,
217 'p' => flags.is_print = true,
218 'n' => flags.is_nameref = true,
219 _ => {}
220 }
221 }
222 } else {
223 names.push(i + 1);
224 }
225 }
226 (flags, names)
227}
228
229impl WorkerRuntime {
230 #[must_use]
231 pub fn new() -> Self {
232 Self {
233 config: BrowserConfig::default(),
234 vm: Vm::new(ShellState::new(), 0),
235 fs: BackendFs::new(),
236 utils: UtilRegistry::new(),
237 builtins: wasmsh_builtins::BuiltinRegistry::new(),
238 initialized: false,
239 pending_stdin: None,
240 functions: IndexMap::new(),
241 exec: ExecState::new(),
242 aliases: IndexMap::new(),
243 external_handler: None,
244 network: None,
245 }
246 }
247
248 pub fn set_external_handler(&mut self, handler: ExternalCommandHandler) {
250 self.external_handler = Some(handler);
251 }
252
253 pub fn set_network_backend(
255 &mut self,
256 backend: Box<dyn wasmsh_utils::net_types::NetworkBackend>,
257 ) {
258 self.network = Some(backend);
259 }
260
261 pub fn handle_command(&mut self, cmd: HostCommand) -> Vec<WorkerEvent> {
263 match cmd {
264 HostCommand::Init {
265 step_budget,
266 allowed_hosts,
267 } => {
268 self.config.step_budget = step_budget;
269 self.config.allowed_hosts = allowed_hosts;
270 self.vm = Vm::new(ShellState::new(), step_budget);
271 self.fs = BackendFs::new();
272 self.pending_stdin = None;
273 self.functions = IndexMap::new();
274 self.exec.reset();
275 self.aliases = IndexMap::new();
276 self.initialized = true;
277 self.vm.state.set_var("SHOPT_extglob".into(), "1".into());
279 vec![WorkerEvent::Version(PROTOCOL_VERSION.to_string())]
280 }
281 HostCommand::Run { input } => {
282 if !self.initialized {
283 return vec![WorkerEvent::Diagnostic(
284 DiagnosticLevel::Error,
285 "runtime not initialized".into(),
286 )];
287 }
288 self.exec.reset();
289 self.execute_input(&input)
290 }
291 HostCommand::Cancel => {
292 self.vm.cancellation_token().cancel();
293 vec![WorkerEvent::Diagnostic(
294 DiagnosticLevel::Info,
295 "cancel received".into(),
296 )]
297 }
298 HostCommand::ReadFile { path } => {
299 use wasmsh_fs::OpenOptions;
300 match self.fs.open(&path, OpenOptions::read()) {
301 Ok(h) => match self.fs.read_file(h) {
302 Ok(data) => {
303 self.fs.close(h);
304 vec![WorkerEvent::Stdout(data)]
305 }
306 Err(e) => {
307 self.fs.close(h);
308 vec![WorkerEvent::Diagnostic(
309 DiagnosticLevel::Error,
310 format!("read error: {path}: {e}"),
311 )]
312 }
313 },
314 Err(e) => vec![WorkerEvent::Diagnostic(
315 DiagnosticLevel::Error,
316 format!("read error: {e}"),
317 )],
318 }
319 }
320 HostCommand::WriteFile { path, data } => {
321 use wasmsh_fs::OpenOptions;
322 match self.fs.open(&path, OpenOptions::write()) {
323 Ok(h) => {
324 if let Err(e) = self.fs.write_file(h, &data) {
325 self.vm.stderr.extend_from_slice(
326 format!("wasmsh: write error: {e}\n").as_bytes(),
327 );
328 }
329 self.fs.close(h);
330 vec![WorkerEvent::FsChanged(path)]
331 }
332 Err(e) => vec![WorkerEvent::Diagnostic(
333 DiagnosticLevel::Error,
334 format!("write error: {e}"),
335 )],
336 }
337 }
338 HostCommand::ListDir { path } => match self.fs.read_dir(&path) {
339 Ok(entries) => {
340 let names: Vec<u8> = entries
341 .iter()
342 .map(|e| e.name.as_str())
343 .collect::<Vec<_>>()
344 .join("\n")
345 .into_bytes();
346 vec![WorkerEvent::Stdout(names)]
347 }
348 Err(e) => vec![WorkerEvent::Diagnostic(
349 DiagnosticLevel::Error,
350 format!("readdir error: {e}"),
351 )],
352 },
353 HostCommand::Mount { .. } => {
354 vec![WorkerEvent::Diagnostic(
355 DiagnosticLevel::Warning,
356 "mount not yet implemented".into(),
357 )]
358 }
359 _ => {
360 vec![WorkerEvent::Diagnostic(
361 DiagnosticLevel::Warning,
362 "unknown command".into(),
363 )]
364 }
365 }
366 }
367
368 fn execute_input_inner(&mut self, input: &str) -> Vec<WorkerEvent> {
370 self.exec.recursion_depth += 1;
371 if self.exec.recursion_depth > MAX_RECURSION_DEPTH {
372 self.exec.recursion_depth -= 1;
373 return vec![WorkerEvent::Stderr(
374 b"wasmsh: maximum recursion depth exceeded\n".to_vec(),
375 )];
376 }
377 let result = self.execute_input_inner_impl(input);
378 self.exec.recursion_depth -= 1;
379 result
380 }
381
382 fn execute_input_inner_impl(&mut self, input: &str) -> Vec<WorkerEvent> {
384 let ast = match wasmsh_parse::parse(input) {
385 Ok(ast) => ast,
386 Err(e) => {
387 self.vm.state.last_status = 2;
388 return vec![WorkerEvent::Stderr(
389 format!("wasmsh: parse error: {e}\n").into_bytes(),
390 )];
391 }
392 };
393 let hir = wasmsh_hir::lower(&ast);
394 for cc in &hir.items {
395 if self.exec.exit_requested.is_some() {
396 break;
397 }
398 let line = input
400 .as_bytes()
401 .iter()
402 .take(cc.span.start as usize)
403 .filter(|&&b| b == b'\n')
404 .count() as u32
405 + 1;
406 self.vm.state.lineno = line;
407 for and_or in &cc.list {
408 self.execute_pipeline_chain(and_or);
409 if self.exec.exit_requested.is_some() {
410 break;
411 }
412 if self.should_errexit(and_or) {
413 self.exec.exit_requested = Some(self.vm.state.last_status);
414 break;
415 }
416 }
417 }
418 let mut events = Vec::new();
420 if !self.vm.stdout.is_empty() {
421 events.push(WorkerEvent::Stdout(std::mem::take(&mut self.vm.stdout)));
422 }
423 if !self.vm.stderr.is_empty() {
424 events.push(WorkerEvent::Stderr(std::mem::take(&mut self.vm.stderr)));
425 }
426 events
427 }
428
429 fn execute_input(&mut self, input: &str) -> Vec<WorkerEvent> {
430 let mut events = self.execute_input_inner(input);
431 self.run_exit_trap_if_needed(&mut events);
432 self.drain_io_events(&mut events);
433 self.drain_diagnostic_events(&mut events);
434 self.push_output_limit_warning(&mut events);
435
436 let exit_status = if self.exec.resource_exhausted {
437 128
440 } else {
441 self.exec
442 .exit_requested
443 .unwrap_or(self.vm.state.last_status)
444 };
445 events.push(WorkerEvent::Exit(exit_status));
446 events
447 }
448
449 fn run_exit_trap_if_needed(&mut self, events: &mut Vec<WorkerEvent>) {
450 let Some(exit_code) = self.exec.exit_requested else {
451 return;
452 };
453 let Some(handler_str) = self.take_exit_trap_handler() else {
454 return;
455 };
456 self.exec.exit_requested = None;
457 events.extend(self.execute_input_inner(&handler_str));
458 self.exec.exit_requested = Some(exit_code);
459 }
460
461 fn take_exit_trap_handler(&mut self) -> Option<String> {
462 let handler = self.vm.state.get_var("_TRAP_EXIT")?;
463 if handler.is_empty() {
464 return None;
465 }
466 let handler_str = handler.to_string();
467 self.vm.state.set_var(
468 smol_str::SmolStr::from("_TRAP_EXIT"),
469 smol_str::SmolStr::default(),
470 );
471 Some(handler_str)
472 }
473
474 fn drain_io_events(&mut self, events: &mut Vec<WorkerEvent>) {
475 self.push_buffer_event(events, true);
476 self.push_buffer_event(events, false);
477 }
478
479 fn push_buffer_event(&mut self, events: &mut Vec<WorkerEvent>, stdout: bool) {
480 let buffer = if stdout {
481 &mut self.vm.stdout
482 } else {
483 &mut self.vm.stderr
484 };
485 if buffer.is_empty() {
486 return;
487 }
488
489 let data = std::mem::take(buffer);
490 events.push(if stdout {
491 WorkerEvent::Stdout(data)
492 } else {
493 WorkerEvent::Stderr(data)
494 });
495 }
496
497 fn drain_diagnostic_events(&mut self, events: &mut Vec<WorkerEvent>) {
498 for diag in self.vm.diagnostics.drain(..) {
499 events.push(WorkerEvent::Diagnostic(
500 Self::to_protocol_diag_level(diag.level),
501 diag.message,
502 ));
503 }
504 }
505
506 fn to_protocol_diag_level(level: wasmsh_vm::DiagLevel) -> DiagnosticLevel {
507 match level {
508 wasmsh_vm::DiagLevel::Trace => DiagnosticLevel::Trace,
509 wasmsh_vm::DiagLevel::Info => DiagnosticLevel::Info,
510 wasmsh_vm::DiagLevel::Warning => DiagnosticLevel::Warning,
511 wasmsh_vm::DiagLevel::Error => DiagnosticLevel::Error,
512 }
513 }
514
515 fn push_output_limit_warning(&self, events: &mut Vec<WorkerEvent>) {
516 if self.vm.limits.output_byte_limit == 0
517 || self.vm.output_bytes <= self.vm.limits.output_byte_limit
518 {
519 return;
520 }
521
522 events.push(WorkerEvent::Diagnostic(
523 DiagnosticLevel::Error,
524 format!(
525 "output limit exceeded: {} bytes (limit: {}); execution aborted",
526 self.vm.output_bytes, self.vm.limits.output_byte_limit
527 ),
528 ));
529 }
530
531 fn execute_pipeline_chain(&mut self, and_or: &HirAndOr) {
532 self.execute_pipeline(&and_or.first);
533 for (op, pipeline) in &and_or.rest {
534 match op {
535 HirAndOrOp::And => {
536 if self.vm.state.last_status == 0 {
537 self.execute_pipeline(pipeline);
538 }
539 }
540 HirAndOrOp::Or => {
541 if self.vm.state.last_status != 0 {
542 self.execute_pipeline(pipeline);
543 }
544 }
545 }
546 }
547 }
548
549 fn execute_pipeline(&mut self, pipeline: &HirPipeline) {
550 let cmds = &pipeline.commands;
551 if cmds.len() == 1 {
552 self.execute_single_pipeline(&cmds[0]);
553 } else {
554 self.execute_multi_pipeline(cmds, pipeline);
555 }
556 if pipeline.negated {
557 self.vm.state.last_status = i32::from(self.vm.state.last_status == 0);
558 }
559 }
560
561 fn execute_single_pipeline(&mut self, cmd: &HirCommand) {
562 self.execute_command(cmd);
563 self.set_pipestatus(&[self.vm.state.last_status]);
564 }
565
566 fn execute_multi_pipeline(&mut self, cmds: &[HirCommand], pipeline: &HirPipeline) {
567 let pipefail = self.vm.state.get_var("SHOPT_o_pipefail").as_deref() == Some("1");
568 let mut rightmost_failure: i32 = 0;
569 let mut statuses: Vec<i32> = Vec::new();
570
571 for (i, cmd) in cmds.iter().enumerate() {
572 let is_last = i == cmds.len() - 1;
573 let stdout_before = self.vm.stdout.len();
574 let stderr_before = self.vm.stderr.len();
575
576 self.execute_command(cmd);
577 statuses.push(self.vm.state.last_status);
578
579 if pipefail && self.vm.state.last_status != 0 {
580 rightmost_failure = self.vm.state.last_status;
581 }
582
583 if !is_last {
584 self.pipe_stage_output(
585 stdout_before,
586 stderr_before,
587 pipeline.pipe_stderr.get(i).copied().unwrap_or(false),
588 );
589 }
590 }
591
592 self.set_pipestatus(&statuses);
593 if pipefail && rightmost_failure != 0 {
594 self.vm.state.last_status = rightmost_failure;
595 }
596 }
597
598 fn pipe_stage_output(&mut self, stdout_before: usize, stderr_before: usize, pipe_stderr: bool) {
599 use wasmsh_vm::pipe::PipeBuffer;
600
601 let mut stage_output = self.vm.stdout[stdout_before..].to_vec();
602 self.vm.stdout.truncate(stdout_before);
603
604 if pipe_stderr {
605 let stage_stderr = self.vm.stderr[stderr_before..].to_vec();
606 self.vm.stderr.truncate(stderr_before);
607 stage_output.extend_from_slice(&stage_stderr);
608 }
609
610 let mut pipe = PipeBuffer::default_size();
611 pipe.write_all(&stage_output);
612 pipe.close_write();
613 self.pending_stdin = Some(pipe.drain());
614 }
615
616 fn set_pipestatus(&mut self, statuses: &[i32]) {
617 let status_key = smol_str::SmolStr::from("PIPESTATUS");
618 self.vm.state.init_indexed_array(status_key.clone());
619 for (i, s) in statuses.iter().enumerate() {
620 self.vm.state.set_array_element(
621 status_key.clone(),
622 &i.to_string(),
623 smol_str::SmolStr::from(s.to_string()),
624 );
625 }
626 }
627
628 fn execute_subst(&mut self, inner: &str) -> smol_str::SmolStr {
630 let saved_stdout = std::mem::take(&mut self.vm.stdout);
631 let events = self.execute_input_inner(inner);
632 let mut result = String::new();
633 for e in &events {
634 if let WorkerEvent::Stdout(d) = e {
635 result.push_str(&String::from_utf8_lossy(d));
636 }
637 }
638 if !self.vm.stdout.is_empty() {
639 result.push_str(&String::from_utf8_lossy(&self.vm.stdout));
640 self.vm.stdout.clear();
641 }
642 self.vm.stdout = saved_stdout;
643 smol_str::SmolStr::from(result.trim_end_matches('\n'))
644 }
645
646 fn next_proc_subst_id() -> u64 {
648 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
649 COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
650 }
651
652 fn execute_process_subst_in(&mut self, inner: &str) -> smol_str::SmolStr {
655 let output = self.execute_subst_raw(inner);
656 let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
657 let h = self.fs.open(&path, OpenOptions::write()).unwrap();
658 let _ = self.fs.write_file(h, output.as_bytes());
659 self.fs.close(h);
660 smol_str::SmolStr::from(path)
661 }
662
663 fn execute_process_subst_out(&mut self, _inner: &str) -> smol_str::SmolStr {
667 let path = format!("/tmp/_proc_subst_{}", Self::next_proc_subst_id());
668 let h = self.fs.open(&path, OpenOptions::write()).unwrap();
670 let _ = self.fs.write_file(h, b"");
671 self.fs.close(h);
672 smol_str::SmolStr::from(path)
673 }
674
675 fn execute_subst_raw(&mut self, inner: &str) -> String {
677 let saved_stdout = std::mem::take(&mut self.vm.stdout);
678 let events = self.execute_input_inner(inner);
679 let mut result = String::new();
680 for e in &events {
681 if let WorkerEvent::Stdout(d) = e {
682 result.push_str(&String::from_utf8_lossy(d));
683 }
684 }
685 if !self.vm.stdout.is_empty() {
686 result.push_str(&String::from_utf8_lossy(&self.vm.stdout));
687 self.vm.stdout.clear();
688 }
689 self.vm.stdout = saved_stdout;
690 result
691 }
692
693 fn resolve_command_subst(&mut self, words: &[wasmsh_ast::Word]) -> Vec<wasmsh_ast::Word> {
695 words
696 .iter()
697 .map(|w| {
698 let parts: Vec<wasmsh_ast::WordPart> = w
699 .parts
700 .iter()
701 .map(|p| match p {
702 wasmsh_ast::WordPart::CommandSubstitution(inner) => {
703 wasmsh_ast::WordPart::Literal(self.execute_subst(inner))
704 }
705 wasmsh_ast::WordPart::ProcessSubstIn(inner) => {
706 wasmsh_ast::WordPart::Literal(self.execute_process_subst_in(inner))
707 }
708 wasmsh_ast::WordPart::ProcessSubstOut(inner) => {
709 wasmsh_ast::WordPart::Literal(self.execute_process_subst_out(inner))
710 }
711 wasmsh_ast::WordPart::DoubleQuoted(inner_parts) => {
712 let resolved: Vec<wasmsh_ast::WordPart> = inner_parts
713 .iter()
714 .map(|ip| match ip {
715 wasmsh_ast::WordPart::CommandSubstitution(inner) => {
716 wasmsh_ast::WordPart::Literal(self.execute_subst(inner))
717 }
718 wasmsh_ast::WordPart::ProcessSubstIn(inner) => {
719 wasmsh_ast::WordPart::Literal(
720 self.execute_process_subst_in(inner),
721 )
722 }
723 wasmsh_ast::WordPart::ProcessSubstOut(inner) => {
724 wasmsh_ast::WordPart::Literal(
725 self.execute_process_subst_out(inner),
726 )
727 }
728 other => other.clone(),
729 })
730 .collect();
731 wasmsh_ast::WordPart::DoubleQuoted(resolved)
732 }
733 other => other.clone(),
734 })
735 .collect();
736 wasmsh_ast::Word {
737 parts,
738 span: w.span,
739 }
740 })
741 .collect()
742 }
743
744 fn execute_command(&mut self, cmd: &HirCommand) {
745 match cmd {
746 HirCommand::Exec(exec) => self.execute_exec(exec),
747 HirCommand::Assign(assign) => {
748 for a in &assign.assignments {
749 self.execute_assignment(&a.name, a.value.as_ref());
750 }
751 let stdout_before = self.vm.stdout.len();
752 self.apply_redirections(&assign.redirections, stdout_before);
753 self.vm.state.last_status = 0;
754 }
755 HirCommand::If(if_cmd) => self.execute_if(if_cmd),
756 HirCommand::While(loop_cmd) => self.execute_while_loop(loop_cmd),
757 HirCommand::Until(loop_cmd) => self.execute_until_loop(loop_cmd),
758 HirCommand::For(for_cmd) => self.execute_for_loop(for_cmd),
759 HirCommand::Group(block) => self.execute_body(&block.body),
760 HirCommand::Subshell(block) => {
761 self.vm.state.env.push_scope();
762 self.execute_body(&block.body);
763 self.vm.state.env.pop_scope();
764 }
765 HirCommand::Case(case_cmd) => self.execute_case(case_cmd),
766 HirCommand::FunctionDef(fd) => {
767 self.functions
768 .insert(fd.name.to_string(), (*fd.body).clone());
769 self.vm.state.last_status = 0;
770 }
771 HirCommand::RedirectOnly(ro) => {
772 let stdout_before = self.vm.stdout.len();
773 self.apply_redirections(&ro.redirections, stdout_before);
774 self.vm.state.last_status = 0;
775 }
776 HirCommand::DoubleBracket(db) => {
777 let result = self.eval_double_bracket(&db.words);
778 self.vm.state.last_status = i32::from(!result);
779 }
780 HirCommand::ArithCommand(ac) => {
781 let result = wasmsh_expand::eval_arithmetic(&ac.expr, &mut self.vm.state);
782 self.vm.state.last_status = i32::from(result == 0);
783 }
784 HirCommand::ArithFor(af) => self.execute_arith_for(af),
785 HirCommand::Select(sel) => self.execute_select(sel),
786 _ => {}
787 }
788 }
789
790 fn execute_exec(&mut self, exec: &wasmsh_hir::HirExec) {
792 let resolved = self.resolve_command_subst(&exec.argv);
793 let expanded = expand_words_argv(&resolved, &mut self.vm.state);
794
795 if self.check_nounset_error() {
796 return;
797 }
798 if expanded.is_empty() {
799 return;
800 }
801
802 let tagged: Vec<(String, bool)> = expanded
804 .into_iter()
805 .flat_map(|ew| {
806 if ew.was_quoted {
807 vec![(ew.text, true)]
808 } else {
809 wasmsh_expand::expand_braces(&ew.text)
810 .into_iter()
811 .map(|s| (s, false))
812 .collect()
813 }
814 })
815 .collect();
816 let argv = self.expand_globs_tagged(tagged);
817
818 for assignment in &exec.env {
819 self.execute_assignment(&assignment.name, assignment.value.as_ref());
820 }
821
822 if self.collect_stdin_from_redirections(&exec.redirections) {
823 return;
824 }
825
826 if self.try_alias_expansion(&argv) {
827 return;
828 }
829
830 let stdout_before = self.vm.stdout.len();
831 let cmd_name = &argv[0];
832 self.trace_command(&argv);
833
834 if self.try_runtime_command(cmd_name, &argv) {
835 return;
836 }
837
838 self.dispatch_command(cmd_name, &argv);
839 self.apply_redirections(&exec.redirections, stdout_before);
840 }
841
842 fn check_nounset_error(&mut self) -> bool {
844 if let Some(var_name) = self.vm.state.get_var("_NOUNSET_ERROR") {
845 if !var_name.is_empty() {
846 let msg = format!("wasmsh: {var_name}: unbound variable\n");
847 self.vm.stderr.extend_from_slice(msg.as_bytes());
848 self.vm.state.set_var(
849 smol_str::SmolStr::from("_NOUNSET_ERROR"),
850 smol_str::SmolStr::default(),
851 );
852 self.vm.state.last_status = 1;
853 return true;
854 }
855 }
856 false
857 }
858
859 fn collect_stdin_from_redirections(&mut self, redirections: &[HirRedirection]) -> bool {
862 for redir in redirections {
863 match redir.op {
864 RedirectionOp::HereDoc | RedirectionOp::HereDocStrip => {
865 if let Some(body) = &redir.here_doc_body {
866 let expanded =
867 wasmsh_expand::expand_string(&body.content, &mut self.vm.state);
868 self.pending_stdin = Some(expanded.into_bytes());
869 }
870 }
871 RedirectionOp::HereString => {
872 let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
873 let resolved_target = resolved.first().unwrap_or(&redir.target);
874 let content = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
875 let mut data = content.into_bytes();
876 data.push(b'\n');
877 self.pending_stdin = Some(data);
878 }
879 RedirectionOp::Input => {
880 let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
881 let resolved_target = resolved.first().unwrap_or(&redir.target);
882 let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
883 let path = self.resolve_cwd_path(&target);
884 if let Ok(h) = self.fs.open(&path, OpenOptions::read()) {
885 match self.fs.read_file(h) {
886 Ok(data) => {
887 self.pending_stdin = Some(data);
888 }
889 Err(e) => {
890 let msg = format!("wasmsh: {target}: read error: {e}\n");
891 self.vm.stderr.extend_from_slice(msg.as_bytes());
892 self.vm.state.last_status = 1;
893 self.fs.close(h);
894 return true;
895 }
896 }
897 self.fs.close(h);
898 } else {
899 let msg = format!("wasmsh: {target}: No such file or directory\n");
900 self.vm.stderr.extend_from_slice(msg.as_bytes());
901 self.vm.state.last_status = 1;
902 return true;
903 }
904 }
905 _ => {}
906 }
907 }
908 false
909 }
910
911 fn try_alias_expansion(&mut self, argv: &[String]) -> bool {
913 if let Some(alias_val) = self.aliases.get(&argv[0]).cloned() {
914 let rest = if argv.len() > 1 {
915 format!(" {}", argv[1..].join(" "))
916 } else {
917 String::new()
918 };
919 let expanded = format!("{alias_val}{rest}");
920 let sub_events = self.execute_input_inner(&expanded);
921 self.merge_sub_events(sub_events);
922 return true;
923 }
924 false
925 }
926
927 fn trace_command(&mut self, argv: &[String]) {
929 if self.vm.state.get_var("SHOPT_x").as_deref() == Some("1") {
930 let ps4 = self
931 .vm
932 .state
933 .get_var("PS4")
934 .unwrap_or_else(|| smol_str::SmolStr::from("+ "));
935 let trace_line = format!("{}{}\n", ps4, argv.join(" "));
936 self.vm.stderr.extend_from_slice(trace_line.as_bytes());
937 }
938 }
939
940 fn try_runtime_command(&mut self, cmd_name: &str, argv: &[String]) -> bool {
943 match cmd_name {
944 CMD_LOCAL => {
945 self.execute_local(argv);
946 true
947 }
948 CMD_BREAK => {
949 self.exec.break_depth = argv.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
950 self.vm.state.last_status = 0;
951 true
952 }
953 CMD_CONTINUE => {
954 self.exec.loop_continue = true;
955 self.vm.state.last_status = 0;
956 true
957 }
958 CMD_EXIT => {
959 let code = argv
960 .get(1)
961 .and_then(|s| s.parse().ok())
962 .unwrap_or(self.vm.state.last_status);
963 self.exec.exit_requested = Some(code);
964 self.vm.state.last_status = code;
965 true
966 }
967 CMD_EVAL => {
968 let code = argv[1..].join(" ");
969 let sub_events = self.execute_input_inner(&code);
970 self.merge_sub_events_with_diagnostics(sub_events);
971 true
972 }
973 CMD_SOURCE | CMD_DOT => {
974 self.execute_source(argv);
975 true
976 }
977 CMD_DECLARE | CMD_TYPESET => {
978 self.execute_declare(argv);
979 true
980 }
981 CMD_LET => {
982 self.execute_let(argv);
983 true
984 }
985 CMD_SHOPT => {
986 self.execute_shopt(argv);
987 true
988 }
989 CMD_ALIAS => {
990 self.execute_alias(argv);
991 true
992 }
993 CMD_UNALIAS => {
994 self.execute_unalias(argv);
995 true
996 }
997 CMD_BUILTIN => {
998 self.execute_builtin_keyword(argv);
999 true
1000 }
1001 CMD_MAPFILE | CMD_READARRAY => {
1002 self.execute_mapfile(argv);
1003 true
1004 }
1005 CMD_TYPE => {
1006 self.execute_type(argv);
1007 true
1008 }
1009 _ => false,
1010 }
1011 }
1012
1013 fn execute_local(&mut self, argv: &[String]) {
1015 for arg in &argv[1..] {
1016 let (name, value) = if let Some(eq) = arg.find('=') {
1017 (&arg[..eq], Some(&arg[eq + 1..]))
1018 } else {
1019 (arg.as_str(), None)
1020 };
1021 let old = self.vm.state.get_var(name);
1022 self.exec
1023 .local_save_stack
1024 .push((smol_str::SmolStr::from(name), old));
1025 let val = value.map_or(smol_str::SmolStr::default(), smol_str::SmolStr::from);
1026 self.vm.state.set_var(smol_str::SmolStr::from(name), val);
1027 }
1028 self.vm.state.last_status = 0;
1029 }
1030
1031 fn execute_source(&mut self, argv: &[String]) {
1033 let Some(path) = argv.get(1) else { return };
1034 let resolved = if path.contains('/') {
1035 Some(self.resolve_cwd_path(path))
1036 } else {
1037 let direct = self.resolve_cwd_path(path);
1038 if self.fs.stat(&direct).is_ok() {
1039 Some(direct)
1040 } else {
1041 self.search_path_for_file(path)
1042 }
1043 };
1044 let Some(full) = resolved else {
1045 let msg = format!("source: {path}: not found\n");
1046 self.vm.stderr.extend_from_slice(msg.as_bytes());
1047 self.vm.state.last_status = 1;
1048 return;
1049 };
1050 let Ok(h) = self.fs.open(&full, OpenOptions::read()) else {
1051 let msg = format!("source: {path}: not found\n");
1052 self.vm.stderr.extend_from_slice(msg.as_bytes());
1053 self.vm.state.last_status = 1;
1054 return;
1055 };
1056 match self.fs.read_file(h) {
1057 Ok(data) => {
1058 self.fs.close(h);
1059 self.vm
1060 .state
1061 .source_stack
1062 .push(smol_str::SmolStr::from(full.as_str()));
1063 let code = String::from_utf8_lossy(&data).to_string();
1064 let sub_events = self.execute_input_inner(&code);
1065 self.vm.state.source_stack.pop();
1066 self.merge_sub_events_with_diagnostics(sub_events);
1067 }
1068 Err(e) => {
1069 self.fs.close(h);
1070 let msg = format!("source: {path}: read error: {e}\n");
1071 self.vm.stderr.extend_from_slice(msg.as_bytes());
1072 self.vm.state.last_status = 1;
1073 }
1074 }
1075 }
1076
1077 fn merge_sub_events(&mut self, events: Vec<WorkerEvent>) {
1079 for e in events {
1080 match e {
1081 WorkerEvent::Stdout(d) => self.vm.stdout.extend_from_slice(&d),
1082 WorkerEvent::Stderr(d) => self.vm.stderr.extend_from_slice(&d),
1083 _ => {}
1084 }
1085 }
1086 }
1087
1088 fn merge_sub_events_with_diagnostics(&mut self, events: Vec<WorkerEvent>) {
1090 for e in events {
1091 match e {
1092 WorkerEvent::Stdout(d) => self.vm.stdout.extend_from_slice(&d),
1093 WorkerEvent::Stderr(d) => self.vm.stderr.extend_from_slice(&d),
1094 WorkerEvent::Diagnostic(level, msg) => {
1095 self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
1096 level: convert_diag_level(level),
1097 category: wasmsh_vm::DiagCategory::Runtime,
1098 message: msg,
1099 });
1100 }
1101 _ => {}
1102 }
1103 }
1104 }
1105
1106 fn call_shell_script(&mut self, argv: &[String]) {
1108 if argv.len() < 2 {
1109 return;
1111 }
1112
1113 if argv[1] == "-c" {
1115 if let Some(script) = argv.get(2) {
1116 let sub_events = self.execute_input_inner(script);
1117 self.merge_sub_events_with_diagnostics(sub_events);
1118 }
1119 return;
1120 }
1121
1122 let path = if argv[1].starts_with('/') {
1124 argv[1].clone()
1125 } else {
1126 format!("{}/{}", self.vm.state.cwd, argv[1])
1127 };
1128 let Ok(h) = self.fs.open(&path, OpenOptions::read()) else {
1129 let msg = format!("{}: {}: No such file or directory\n", argv[0], argv[1]);
1130 self.vm.stderr.extend_from_slice(msg.as_bytes());
1131 self.vm.state.last_status = 127;
1132 return;
1133 };
1134 let data = self.fs.read_file(h).unwrap_or_default();
1135 self.fs.close(h);
1136 let content = String::from_utf8_lossy(&data).to_string();
1137
1138 let old_positional = std::mem::take(&mut self.vm.state.positional);
1140 self.vm.state.positional = argv[1..]
1141 .iter()
1142 .map(|s| smol_str::SmolStr::from(s.as_str()))
1143 .collect();
1144
1145 let sub_events = self.execute_input_inner(&content);
1146 self.merge_sub_events_with_diagnostics(sub_events);
1147
1148 self.vm.state.positional = old_positional;
1149 }
1150
1151 fn dispatch_command(&mut self, cmd_name: &str, argv: &[String]) {
1153 if self.check_resource_limits() {
1154 return;
1155 }
1156 if cmd_name == "bash" || cmd_name == "sh" {
1157 self.call_shell_script(argv);
1158 return;
1159 }
1160 if let Some(body) = self.functions.get(cmd_name).cloned() {
1161 self.call_shell_function(cmd_name, argv, &body);
1162 } else if self.builtins.is_builtin(cmd_name) {
1163 self.call_builtin(cmd_name, argv);
1164 } else if self.utils.is_utility(cmd_name) {
1165 if cmd_name == "find" && argv.iter().any(|a| a == "-exec") {
1166 self.call_find_with_exec(argv);
1167 } else if cmd_name == "xargs" {
1168 self.call_xargs_with_exec(argv);
1169 } else {
1170 self.call_utility(cmd_name, argv);
1171 }
1172 } else if let Some(ref mut handler) = self.external_handler {
1173 let stdin_data = self.pending_stdin.take();
1174 if let Some(result) = handler(cmd_name, argv, stdin_data.as_deref()) {
1175 self.vm.stdout.extend_from_slice(&result.stdout);
1176 self.vm.stderr.extend_from_slice(&result.stderr);
1177 self.vm.output_bytes += (result.stdout.len() + result.stderr.len()) as u64;
1178 self.vm.state.last_status = result.status;
1179 } else {
1180 let msg = format!("wasmsh: {cmd_name}: command not found\n");
1181 self.vm.stderr.extend_from_slice(msg.as_bytes());
1182 self.vm.state.last_status = 127;
1183 }
1184 } else {
1185 let msg = format!("wasmsh: {cmd_name}: command not found\n");
1186 self.vm.stderr.extend_from_slice(msg.as_bytes());
1187 self.vm.state.last_status = 127;
1188 }
1189 }
1190
1191 fn call_shell_function(&mut self, cmd_name: &str, argv: &[String], body: &HirCommand) {
1193 let old_positional = std::mem::take(&mut self.vm.state.positional);
1194 self.vm.state.positional = argv[1..]
1195 .iter()
1196 .map(|s| smol_str::SmolStr::from(s.as_str()))
1197 .collect();
1198 self.vm
1199 .state
1200 .func_stack
1201 .push(smol_str::SmolStr::from(cmd_name));
1202 let locals_before = self.exec.local_save_stack.len();
1203 self.execute_command(body);
1204 let new_locals: Vec<_> = self.exec.local_save_stack.drain(locals_before..).collect();
1205 for (name, old_val) in new_locals.into_iter().rev() {
1206 if let Some(val) = old_val {
1207 self.vm.state.set_var(name, val);
1208 } else {
1209 self.vm.state.unset_var(&name).ok();
1210 }
1211 }
1212 self.vm.state.func_stack.pop();
1213 self.vm.state.positional = old_positional;
1214 }
1215
1216 fn call_builtin(&mut self, cmd_name: &str, argv: &[String]) {
1218 let builtin_fn = self.builtins.get(cmd_name).unwrap();
1219 let stdin_data = self.pending_stdin.take();
1220 let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
1221 let mut sink = wasmsh_builtins::VecSink::default();
1222 let status = {
1223 let mut ctx = wasmsh_builtins::BuiltinContext {
1224 state: &mut self.vm.state,
1225 output: &mut sink,
1226 fs: Some(&self.fs),
1227 stdin: stdin_data.as_deref(),
1228 };
1229 builtin_fn(&mut ctx, &argv_refs)
1230 };
1231 self.vm.stdout.extend_from_slice(&sink.stdout);
1232 self.vm.stderr.extend_from_slice(&sink.stderr);
1233 self.vm.output_bytes += (sink.stdout.len() + sink.stderr.len()) as u64;
1234 self.vm.state.last_status = status;
1235 self.pending_stdin = None;
1236 }
1237
1238 fn extract_find_exec(argv: &[String]) -> Option<(Vec<String>, Vec<String>)> {
1241 let exec_pos = argv.iter().position(|a| a == "-exec")?;
1242 let term_pos = argv[exec_pos + 1..]
1244 .iter()
1245 .position(|a| a == "\\;" || a == ";")
1246 .map(|p| p + exec_pos + 1)?;
1247 let template: Vec<String> = argv[exec_pos + 1..term_pos].to_vec();
1248 if template.is_empty() {
1249 return None;
1250 }
1251 let mut cleaned: Vec<String> = argv[..exec_pos].to_vec();
1252 cleaned.extend_from_slice(&argv[term_pos + 1..]);
1253 Some((template, cleaned))
1254 }
1255
1256 fn shell_quote(s: &str) -> String {
1258 if s.chars()
1259 .all(|c| c.is_alphanumeric() || matches!(c, '/' | '.' | '_' | '-'))
1260 {
1261 s.to_string()
1262 } else {
1263 format!("'{}'", s.replace('\'', "'\\''"))
1264 }
1265 }
1266
1267 fn call_find_with_exec(&mut self, argv: &[String]) {
1270 let Some((template, cleaned_argv)) = Self::extract_find_exec(argv) else {
1271 self.call_utility("find", argv);
1273 return;
1274 };
1275
1276 let saved_stdout = std::mem::take(&mut self.vm.stdout);
1278 self.call_utility("find", &cleaned_argv);
1279 let find_output = std::mem::take(&mut self.vm.stdout);
1280 self.vm.stdout = saved_stdout;
1281
1282 let paths_str = String::from_utf8_lossy(&find_output);
1284 let paths: Vec<&str> = paths_str.lines().filter(|l| !l.is_empty()).collect();
1285
1286 let mut last_status = 0i32;
1288 for path in paths {
1289 let cmd_line: String = template
1290 .iter()
1291 .map(|t| {
1292 if t == "{}" {
1293 Self::shell_quote(path)
1294 } else {
1295 t.clone()
1296 }
1297 })
1298 .collect::<Vec<_>>()
1299 .join(" ");
1300 let sub_events = self.execute_input_inner(&cmd_line);
1301 self.merge_sub_events(sub_events);
1302 if self.vm.state.last_status != 0 {
1303 last_status = self.vm.state.last_status;
1304 }
1305 }
1306 self.vm.state.last_status = last_status;
1307 }
1308
1309 fn call_xargs_with_exec(&mut self, argv: &[String]) {
1313 let mut has_non_echo = false;
1315 let mut i = 1;
1316 while i < argv.len() {
1317 let arg = &argv[i];
1318 if matches!(arg.as_str(), "-I" | "-n" | "-d" | "-P" | "-L") && i + 1 < argv.len() {
1319 i += 2;
1320 } else if matches!(arg.as_str(), "-0" | "--null" | "-t" | "-p") || arg.starts_with('-')
1321 {
1322 i += 1;
1323 } else {
1324 if arg != "echo" {
1326 has_non_echo = true;
1327 }
1328 break;
1329 }
1330 }
1331
1332 if !has_non_echo {
1333 self.call_utility("xargs", argv);
1334 return;
1335 }
1336
1337 let saved_stdout = std::mem::take(&mut self.vm.stdout);
1339 self.call_utility("xargs", argv);
1340 let xargs_output = std::mem::take(&mut self.vm.stdout);
1341 self.vm.stdout = saved_stdout;
1342
1343 let output_str = String::from_utf8_lossy(&xargs_output);
1345 let mut last_status = 0i32;
1346 for line in output_str.lines().filter(|l| !l.is_empty()) {
1347 let sub_events = self.execute_input_inner(line);
1348 self.merge_sub_events(sub_events);
1349 if self.vm.state.last_status != 0 {
1350 last_status = self.vm.state.last_status;
1351 }
1352 }
1353 self.vm.state.last_status = last_status;
1354 }
1355
1356 fn call_utility(&mut self, cmd_name: &str, argv: &[String]) {
1358 let stdin_data = self.pending_stdin.take();
1359 let argv_refs: Vec<&str> = argv.iter().map(String::as_str).collect();
1360 let mut output = UtilOutput::default();
1361 let cwd = self.vm.state.cwd.clone();
1362 let status = {
1363 let util_fn = self.utils.get(cmd_name).unwrap();
1364 let mut ctx = UtilContext {
1365 fs: &mut self.fs,
1366 output: &mut output,
1367 cwd: &cwd,
1368 stdin: stdin_data.as_deref(),
1369 state: Some(&self.vm.state),
1370 network: self.network.as_deref(),
1371 };
1372 util_fn(&mut ctx, &argv_refs)
1373 };
1374 self.vm.stdout.extend_from_slice(&output.stdout);
1375 self.vm.stderr.extend_from_slice(&output.stderr);
1376 self.vm.output_bytes += (output.stdout.len() + output.stderr.len()) as u64;
1377 self.vm.state.last_status = status;
1378 }
1379
1380 fn execute_if(&mut self, if_cmd: &wasmsh_hir::HirIf) {
1382 let saved_suppress = self.exec.errexit_suppressed;
1383 self.exec.errexit_suppressed = true;
1384 self.execute_body(&if_cmd.condition);
1385 self.exec.errexit_suppressed = saved_suppress;
1386 if self.vm.state.last_status == 0 {
1387 self.execute_body(&if_cmd.then_body);
1388 return;
1389 }
1390 for elif in &if_cmd.elifs {
1391 let saved = self.exec.errexit_suppressed;
1392 self.exec.errexit_suppressed = true;
1393 self.execute_body(&elif.condition);
1394 self.exec.errexit_suppressed = saved;
1395 if self.vm.state.last_status == 0 {
1396 self.execute_body(&elif.then_body);
1397 return;
1398 }
1399 }
1400 if let Some(else_body) = &if_cmd.else_body {
1401 self.execute_body(else_body);
1402 }
1403 }
1404
1405 fn execute_while_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
1407 loop {
1408 if self.check_resource_limits() {
1409 break;
1410 }
1411 let saved = self.exec.errexit_suppressed;
1412 self.exec.errexit_suppressed = true;
1413 self.execute_body(&loop_cmd.condition);
1414 self.exec.errexit_suppressed = saved;
1415 if self.vm.state.last_status != 0 {
1416 break;
1417 }
1418 self.execute_body(&loop_cmd.body);
1419 if self.handle_loop_control() {
1420 break;
1421 }
1422 }
1423 }
1424
1425 fn execute_until_loop(&mut self, loop_cmd: &wasmsh_hir::HirLoop) {
1427 loop {
1428 if self.check_resource_limits() {
1429 break;
1430 }
1431 let saved = self.exec.errexit_suppressed;
1432 self.exec.errexit_suppressed = true;
1433 self.execute_body(&loop_cmd.condition);
1434 self.exec.errexit_suppressed = saved;
1435 if self.vm.state.last_status == 0 {
1436 break;
1437 }
1438 self.execute_body(&loop_cmd.body);
1439 if self.handle_loop_control() {
1440 break;
1441 }
1442 }
1443 }
1444
1445 fn handle_loop_control(&mut self) -> bool {
1447 if self.exec.break_depth > 0 {
1448 self.exec.break_depth -= 1;
1449 return true;
1450 }
1451 if self.exec.loop_continue {
1452 self.exec.loop_continue = false;
1453 }
1454 self.exec.exit_requested.is_some()
1455 }
1456
1457 fn execute_for_loop(&mut self, for_cmd: &wasmsh_hir::HirFor) {
1459 let words = self.expand_for_words(for_cmd.words.as_deref());
1460 for word in words {
1461 if self.check_resource_limits() {
1462 break;
1463 }
1464 self.vm.state.set_var(for_cmd.var_name.clone(), word.into());
1465 self.execute_body(&for_cmd.body);
1466 if self.exec.break_depth > 0 {
1467 self.exec.break_depth -= 1;
1468 break;
1469 }
1470 if self.exec.loop_continue {
1471 self.exec.loop_continue = false;
1472 continue;
1473 }
1474 if self.exec.exit_requested.is_some() {
1475 break;
1476 }
1477 }
1478 }
1479
1480 fn expand_for_words(&mut self, words: Option<&[wasmsh_ast::Word]>) -> Vec<String> {
1482 if let Some(ws) = words {
1483 let resolved = self.resolve_command_subst(ws);
1484 let mut result = Vec::new();
1485 for w in &resolved {
1486 let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
1487 result.extend(expanded.fields);
1488 }
1489 let result: Vec<String> = result
1490 .into_iter()
1491 .flat_map(|arg| wasmsh_expand::expand_braces(&arg))
1492 .collect();
1493 self.expand_globs(result)
1494 } else {
1495 self.vm
1496 .state
1497 .positional
1498 .iter()
1499 .map(ToString::to_string)
1500 .collect()
1501 }
1502 }
1503
1504 fn execute_case(&mut self, case_cmd: &wasmsh_hir::HirCase) {
1506 let nocasematch = self.vm.state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
1507 let value = wasmsh_expand::expand_word(&case_cmd.word, &mut self.vm.state);
1508 let mut i = 0;
1509 let mut fallthrough = false;
1510 while i < case_cmd.items.len() {
1511 let item = &case_cmd.items[i];
1512 let pattern_matched = if fallthrough {
1513 true
1514 } else {
1515 item.patterns.iter().any(|pattern| {
1516 let pat = wasmsh_expand::expand_word(pattern, &mut self.vm.state);
1517 if nocasematch {
1518 glob_match_inner(
1519 pat.to_lowercase().as_bytes(),
1520 value.to_lowercase().as_bytes(),
1521 )
1522 } else {
1523 glob_match_inner(pat.as_bytes(), value.as_bytes())
1524 }
1525 })
1526 };
1527 if pattern_matched {
1528 self.execute_body(&item.body);
1529 match item.terminator {
1530 CaseTerminator::Break => break,
1531 CaseTerminator::Fallthrough => {
1532 fallthrough = true;
1533 i += 1;
1534 }
1535 CaseTerminator::ContinueTesting => {
1536 fallthrough = false;
1537 i += 1;
1538 }
1539 }
1540 } else {
1541 fallthrough = false;
1542 i += 1;
1543 }
1544 }
1545 }
1546
1547 fn execute_arith_for(&mut self, af: &wasmsh_hir::HirArithFor) {
1549 if !af.init.is_empty() {
1550 wasmsh_expand::eval_arithmetic(&af.init, &mut self.vm.state);
1551 }
1552 loop {
1553 if self.check_resource_limits() {
1554 break;
1555 }
1556 if !af.cond.is_empty() {
1557 let cond_val = wasmsh_expand::eval_arithmetic(&af.cond, &mut self.vm.state);
1558 if cond_val == 0 {
1559 break;
1560 }
1561 }
1562 self.execute_body(&af.body);
1563 if self.handle_loop_control() {
1564 break;
1565 }
1566 if !af.step.is_empty() {
1567 wasmsh_expand::eval_arithmetic(&af.step, &mut self.vm.state);
1568 }
1569 }
1570 }
1571
1572 fn execute_select(&mut self, sel: &wasmsh_hir::HirSelect) {
1574 self.collect_stdin_from_redirections(&sel.redirections);
1575
1576 let words: Vec<String> = if let Some(ws) = &sel.words {
1577 let resolved = self.resolve_command_subst(ws);
1578 let mut result = Vec::new();
1579 for w in &resolved {
1580 let expanded = wasmsh_expand::expand_word_split(w, &mut self.vm.state);
1581 result.extend(expanded.fields);
1582 }
1583 result
1584 } else {
1585 self.vm
1586 .state
1587 .positional
1588 .iter()
1589 .map(ToString::to_string)
1590 .collect()
1591 };
1592
1593 if words.is_empty() {
1594 return;
1595 }
1596 for (idx, w) in words.iter().enumerate() {
1597 let line = format!("{}) {}\n", idx + 1, w);
1598 self.vm.stderr.extend_from_slice(line.as_bytes());
1599 }
1600
1601 let stdin_data = self.pending_stdin.take().unwrap_or_default();
1602 let input = String::from_utf8_lossy(&stdin_data);
1603 let first_line = input.lines().next().unwrap_or("");
1604
1605 self.vm.state.set_var(
1606 smol_str::SmolStr::from("REPLY"),
1607 smol_str::SmolStr::from(first_line.trim()),
1608 );
1609
1610 let selected = first_line.trim().parse::<usize>().ok().and_then(|n| {
1611 if n >= 1 && n <= words.len() {
1612 Some(&words[n - 1])
1613 } else {
1614 None
1615 }
1616 });
1617
1618 if let Some(word) = selected {
1619 self.vm
1620 .state
1621 .set_var(sel.var_name.clone(), smol_str::SmolStr::from(word.as_str()));
1622 } else {
1623 self.vm
1624 .state
1625 .set_var(sel.var_name.clone(), smol_str::SmolStr::default());
1626 }
1627
1628 self.execute_body(&sel.body);
1629 }
1630
1631 fn dbl_bracket_expand(&mut self, word: &wasmsh_ast::Word) -> String {
1635 let resolved = self.resolve_command_subst(std::slice::from_ref(word));
1636 wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
1637 }
1638
1639 fn eval_double_bracket(&mut self, words: &[wasmsh_ast::Word]) -> bool {
1641 let tokens: Vec<String> = words.iter().map(|w| self.dbl_bracket_expand(w)).collect();
1643 let mut pos = 0;
1644 dbl_bracket_eval_or(&tokens, &mut pos, &self.fs, &mut self.vm.state)
1645 }
1646
1647 fn resolve_cwd_path(&self, path: &str) -> String {
1648 if path.starts_with('/') {
1649 wasmsh_fs::normalize_path(path)
1650 } else {
1651 wasmsh_fs::normalize_path(&format!("{}/{}", self.vm.state.cwd, path))
1652 }
1653 }
1654
1655 fn execute_alias(&mut self, argv: &[String]) {
1657 let args = &argv[1..];
1658 if args.is_empty() {
1659 for (name, value) in &self.aliases {
1661 let line = format!("alias {name}='{value}'\n");
1662 self.vm.stdout.extend_from_slice(line.as_bytes());
1663 }
1664 self.vm.state.last_status = 0;
1665 return;
1666 }
1667 for arg in args {
1668 if let Some(eq_pos) = arg.find('=') {
1669 let name = &arg[..eq_pos];
1670 let value = &arg[eq_pos + 1..];
1671 self.aliases.insert(name.to_string(), value.to_string());
1672 } else {
1673 if let Some(value) = self.aliases.get(arg.as_str()) {
1675 let line = format!("alias {arg}='{value}'\n");
1676 self.vm.stdout.extend_from_slice(line.as_bytes());
1677 } else {
1678 let msg = format!("alias: {arg}: not found\n");
1679 self.vm.stderr.extend_from_slice(msg.as_bytes());
1680 self.vm.state.last_status = 1;
1681 return;
1682 }
1683 }
1684 }
1685 self.vm.state.last_status = 0;
1686 }
1687
1688 fn execute_unalias(&mut self, argv: &[String]) {
1690 let args = &argv[1..];
1691 if args.is_empty() {
1692 self.vm
1693 .stderr
1694 .extend_from_slice(b"unalias: usage: unalias [-a] name ...\n");
1695 self.vm.state.last_status = 1;
1696 return;
1697 }
1698 for arg in args {
1699 if arg == "-a" {
1700 self.aliases.clear();
1701 } else if self.aliases.shift_remove(arg.as_str()).is_none() {
1702 let msg = format!("unalias: {arg}: not found\n");
1703 self.vm.stderr.extend_from_slice(msg.as_bytes());
1704 self.vm.state.last_status = 1;
1705 return;
1706 }
1707 }
1708 self.vm.state.last_status = 0;
1709 }
1710
1711 fn execute_type(&mut self, argv: &[String]) {
1714 let mut status = 0;
1715 for name in &argv[1..] {
1716 if self.aliases.contains_key(name.as_str()) {
1717 let val = self.aliases.get(name.as_str()).unwrap();
1718 let msg = format!("{name} is aliased to `{val}'\n");
1719 self.vm.stdout.extend_from_slice(msg.as_bytes());
1720 } else if self.functions.contains_key(name.as_str()) {
1721 let msg = format!("{name} is a function\n");
1722 self.vm.stdout.extend_from_slice(msg.as_bytes());
1723 } else if self.builtins.is_builtin(name) {
1724 let msg = format!("{name} is a shell builtin\n");
1725 self.vm.stdout.extend_from_slice(msg.as_bytes());
1726 } else if self.utils.is_utility(name) {
1727 let msg = format!("{name} is a shell utility\n");
1728 self.vm.stdout.extend_from_slice(msg.as_bytes());
1729 } else {
1730 let msg = format!("wasmsh: type: {name}: not found\n");
1731 self.vm.stderr.extend_from_slice(msg.as_bytes());
1732 status = 1;
1733 }
1734 }
1735 self.vm.state.last_status = status;
1736 }
1737
1738 fn execute_builtin_keyword(&mut self, argv: &[String]) {
1741 if argv.len() < 2 {
1742 self.vm.state.last_status = 0;
1743 return;
1744 }
1745 let cmd_name = &argv[1];
1746 let builtin_argv: Vec<String> = argv[1..].to_vec();
1747 if let Some(builtin_fn) = self.builtins.get(cmd_name) {
1748 let stdin_data = self.pending_stdin.take();
1749 let argv_refs: Vec<&str> = builtin_argv.iter().map(String::as_str).collect();
1750 let mut sink = wasmsh_builtins::VecSink::default();
1751 let status = {
1752 let mut ctx = wasmsh_builtins::BuiltinContext {
1753 state: &mut self.vm.state,
1754 output: &mut sink,
1755 fs: Some(&self.fs),
1756 stdin: stdin_data.as_deref(),
1757 };
1758 builtin_fn(&mut ctx, &argv_refs)
1759 };
1760 self.vm.stdout.extend_from_slice(&sink.stdout);
1761 self.vm.stderr.extend_from_slice(&sink.stderr);
1762 self.vm.output_bytes += (sink.stdout.len() + sink.stderr.len()) as u64;
1763 self.vm.state.last_status = status;
1764 } else {
1765 let msg = format!("builtin: {cmd_name}: not a shell builtin\n");
1766 self.vm.stderr.extend_from_slice(msg.as_bytes());
1767 self.vm.state.last_status = 1;
1768 }
1769 }
1770
1771 fn execute_mapfile(&mut self, argv: &[String]) {
1774 let (strip_newline, array_name) = Self::parse_mapfile_args(&argv[1..]);
1775 let data = self.pending_stdin.take().unwrap_or_default();
1776 let text = String::from_utf8_lossy(&data);
1777
1778 let name_key = smol_str::SmolStr::from(array_name.as_str());
1779 self.vm.state.init_indexed_array(name_key.clone());
1780 self.populate_mapfile_array(&name_key, &text, strip_newline);
1781 self.vm.state.last_status = 0;
1782 }
1783
1784 fn parse_mapfile_args(args: &[String]) -> (bool, String) {
1785 let mut strip_newline = false;
1786 let mut positional: Vec<&str> = Vec::new();
1787 for arg in args {
1788 match arg.as_str() {
1789 "-t" => strip_newline = true,
1790 _ => positional.push(arg),
1791 }
1792 }
1793 let array_name = positional
1794 .last()
1795 .map_or("MAPFILE".to_string(), ToString::to_string);
1796 (strip_newline, array_name)
1797 }
1798
1799 fn populate_mapfile_array(
1800 &mut self,
1801 name_key: &smol_str::SmolStr,
1802 text: &str,
1803 strip_newline: bool,
1804 ) {
1805 let mut idx = 0;
1806 for line in text.split('\n') {
1807 if line.is_empty() && idx > 0 {
1808 continue;
1809 }
1810 let value = if strip_newline {
1811 line.to_string()
1812 } else {
1813 format!("{line}\n")
1814 };
1815 self.vm.state.set_array_element(
1816 name_key.clone(),
1817 &idx.to_string(),
1818 smol_str::SmolStr::from(value.as_str()),
1819 );
1820 idx += 1;
1821 }
1822 }
1823
1824 fn search_path_for_file(&self, filename: &str) -> Option<String> {
1826 let path_var = self.vm.state.get_var("PATH")?;
1827 for dir in path_var.split(':') {
1828 if dir.is_empty() {
1829 continue;
1830 }
1831 let candidate = format!("{dir}/{filename}");
1832 let full = self.resolve_cwd_path(&candidate);
1833 if self.fs.stat(&full).is_ok() {
1834 return Some(full);
1835 }
1836 }
1837 None
1838 }
1839
1840 fn should_errexit(&self, and_or: &HirAndOr) -> bool {
1841 !self.exec.errexit_suppressed
1842 && and_or.rest.is_empty()
1843 && !and_or.first.negated
1844 && self.vm.state.get_var("SHOPT_e").as_deref() == Some("1")
1845 && self.vm.state.last_status != 0
1846 && self.exec.exit_requested.is_none()
1847 }
1848
1849 fn execute_let(&mut self, argv: &[String]) {
1852 if argv.len() < 2 {
1853 self.vm
1854 .stderr
1855 .extend_from_slice(b"let: expression expected\n");
1856 self.vm.state.last_status = 1;
1857 return;
1858 }
1859 let mut last_val: i64 = 0;
1860 for expr in &argv[1..] {
1861 last_val = wasmsh_expand::eval_arithmetic(expr, &mut self.vm.state);
1862 }
1863 self.vm.state.last_status = i32::from(last_val == 0);
1864 }
1865
1866 const SHOPT_OPTIONS: &'static [&'static str] = &[
1868 "extglob",
1869 "nullglob",
1870 "dotglob",
1871 "globstar",
1872 "nocasematch",
1873 "nocaseglob",
1874 "failglob",
1875 "lastpipe",
1876 "expand_aliases",
1877 ];
1878
1879 fn execute_shopt(&mut self, argv: &[String]) {
1881 let (set_mode, names) = Self::parse_shopt_args(&argv[1..]);
1882 if let Some(enable) = set_mode {
1883 self.shopt_set_options(&names, enable);
1884 } else {
1885 self.shopt_print_options(&names);
1886 }
1887 }
1888
1889 fn parse_shopt_args(args: &[String]) -> (Option<bool>, Vec<&str>) {
1890 let mut set_mode = None;
1891 let mut names = Vec::new();
1892
1893 for arg in args {
1894 match arg.as_str() {
1895 "-s" => set_mode = Some(true),
1896 "-u" => set_mode = Some(false),
1897 _ => names.push(arg.as_str()),
1898 }
1899 }
1900
1901 (set_mode, names)
1902 }
1903
1904 fn shopt_set_options(&mut self, names: &[&str], enable: bool) {
1906 if names.is_empty() {
1907 self.vm
1908 .stderr
1909 .extend_from_slice(b"shopt: option name required\n");
1910 self.vm.state.last_status = 1;
1911 return;
1912 }
1913 let val = if enable { "1" } else { "0" };
1914 for name in names {
1915 if self.reject_invalid_shopt_name(name) {
1916 return;
1917 }
1918 self.set_shopt_value(name, val);
1919 }
1920 self.vm.state.last_status = 0;
1921 }
1922
1923 fn shopt_print_options(&mut self, names: &[&str]) {
1925 let options_to_print: Vec<&str> = if names.is_empty() {
1926 Self::SHOPT_OPTIONS.to_vec()
1927 } else {
1928 names.to_vec()
1929 };
1930 for name in &options_to_print {
1931 if self.reject_invalid_shopt_name(name) {
1932 return;
1933 }
1934 let enabled = self.get_shopt_value(name);
1935 let status_str = if enabled { "on" } else { "off" };
1936 let line = format!("{name}\t{status_str}\n");
1937 self.vm.stdout.extend_from_slice(line.as_bytes());
1938 }
1939 self.vm.state.last_status = 0;
1940 }
1941
1942 fn reject_invalid_shopt_name(&mut self, name: &str) -> bool {
1943 if Self::SHOPT_OPTIONS.contains(&name) {
1944 return false;
1945 }
1946
1947 let msg = format!("shopt: {name}: invalid shell option name\n");
1948 self.vm.stderr.extend_from_slice(msg.as_bytes());
1949 self.vm.state.last_status = 1;
1950 true
1951 }
1952
1953 fn shopt_var_name(name: &str) -> String {
1954 format!("SHOPT_{name}")
1955 }
1956
1957 fn set_shopt_value(&mut self, name: &str, value: &str) {
1958 let var = Self::shopt_var_name(name);
1959 self.vm.state.set_var(
1960 smol_str::SmolStr::from(var.as_str()),
1961 smol_str::SmolStr::from(value),
1962 );
1963 }
1964
1965 fn get_shopt_value(&self, name: &str) -> bool {
1966 let var = Self::shopt_var_name(name);
1967 self.vm.state.get_var(&var).as_deref() == Some("1")
1968 }
1969
1970 fn execute_declare(&mut self, argv: &[String]) {
1973 let (flags, names) = parse_declare_flags(argv);
1974
1975 if flags.is_print {
1976 self.declare_print(argv, &names);
1977 return;
1978 }
1979
1980 for &idx in &names {
1981 self.declare_one_name(argv, idx, &flags);
1982 }
1983 self.vm.state.last_status = 0;
1984 }
1985
1986 fn declare_print(&mut self, argv: &[String], names: &[usize]) {
1988 if names.is_empty() {
1989 let vars: Vec<(String, String)> = self
1990 .vm
1991 .state
1992 .env
1993 .scopes
1994 .iter()
1995 .flat_map(|scope| {
1996 scope
1997 .iter()
1998 .map(|(n, v)| (n.to_string(), v.value.as_scalar().to_string()))
1999 })
2000 .collect();
2001 for (name, val) in &vars {
2002 let line = format!("declare -- {name}=\"{val}\"\n");
2003 self.vm.stdout.extend_from_slice(line.as_bytes());
2004 }
2005 } else {
2006 for &idx in names {
2007 let name_arg = &argv[idx];
2008 let name = name_arg
2009 .find('=')
2010 .map_or(name_arg.as_str(), |eq| &name_arg[..eq]);
2011 if let Some(var) = self.vm.state.env.get(name) {
2012 let val = var.value.as_scalar();
2013 let line = format!("declare -- {name}=\"{val}\"\n");
2014 self.vm.stdout.extend_from_slice(line.as_bytes());
2015 }
2016 }
2017 }
2018 self.vm.state.last_status = 0;
2019 }
2020
2021 fn declare_one_name(&mut self, argv: &[String], idx: usize, flags: &DeclareFlags) {
2023 let name_arg = &argv[idx];
2024 let (name, value) = if let Some(eq) = name_arg.find('=') {
2025 (&name_arg[..eq], Some(&name_arg[eq + 1..]))
2026 } else {
2027 (name_arg.as_str(), None)
2028 };
2029
2030 if flags.is_assoc {
2031 self.vm
2032 .state
2033 .init_assoc_array(smol_str::SmolStr::from(name));
2034 } else if flags.is_indexed {
2035 self.vm
2036 .state
2037 .init_indexed_array(smol_str::SmolStr::from(name));
2038 }
2039
2040 if let Some(val) = value {
2041 self.declare_assign_value(name, val, flags);
2042 } else if !flags.is_assoc && !flags.is_indexed && self.vm.state.get_var(name).is_none() {
2043 self.vm
2044 .state
2045 .set_var(smol_str::SmolStr::from(name), smol_str::SmolStr::default());
2046 }
2047
2048 self.declare_apply_attributes(name, flags);
2049
2050 if flags.is_nameref {
2051 self.declare_apply_nameref(name);
2052 }
2053 }
2054
2055 fn declare_assign_value(&mut self, name: &str, val: &str, flags: &DeclareFlags) {
2057 if val.starts_with('(') && val.ends_with(')') {
2058 self.declare_assign_compound(name, &val[1..val.len() - 1], flags);
2059 return;
2060 }
2061 let final_val = Self::transform_declare_scalar(val, flags, &mut self.vm.state);
2062 self.vm.state.set_var(
2063 smol_str::SmolStr::from(name),
2064 smol_str::SmolStr::from(final_val.as_str()),
2065 );
2066 }
2067
2068 fn declare_assign_compound(&mut self, name: &str, inner: &str, flags: &DeclareFlags) {
2069 let name_key = smol_str::SmolStr::from(name);
2070 if flags.is_assoc || inner.contains("]=") {
2071 self.declare_assign_assoc_compound(&name_key, inner);
2072 } else {
2073 self.declare_assign_indexed_compound(&name_key, inner);
2074 }
2075 }
2076
2077 fn declare_assign_assoc_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2078 self.vm.state.init_assoc_array(name_key.clone());
2079 for pair in Self::parse_assoc_pairs(inner) {
2080 self.vm.state.set_array_element(
2081 name_key.clone(),
2082 &pair.0,
2083 smol_str::SmolStr::from(pair.1.as_str()),
2084 );
2085 }
2086 }
2087
2088 fn declare_assign_indexed_compound(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2089 let elements = Self::parse_array_elements(inner);
2090 self.vm.state.init_indexed_array(name_key.clone());
2091 for (i, elem) in elements.iter().enumerate() {
2092 self.vm
2093 .state
2094 .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
2095 }
2096 }
2097
2098 fn transform_declare_scalar(val: &str, flags: &DeclareFlags, state: &mut ShellState) -> String {
2099 if flags.is_integer {
2100 wasmsh_expand::eval_arithmetic(val, state).to_string()
2101 } else if flags.is_lower {
2102 val.to_lowercase()
2103 } else if flags.is_upper {
2104 val.to_uppercase()
2105 } else {
2106 val.to_string()
2107 }
2108 }
2109
2110 fn declare_apply_attributes(&mut self, name: &str, flags: &DeclareFlags) {
2112 if let Some(var) = self.vm.state.env.get_mut(name) {
2113 if flags.is_export {
2114 var.exported = true;
2115 }
2116 if flags.is_readonly {
2117 var.readonly = true;
2118 }
2119 if flags.is_integer {
2120 var.integer = true;
2121 }
2122 }
2123 }
2124
2125 fn declare_apply_nameref(&mut self, name: &str) {
2127 let target_value = if let Some(eq_pos) = name.find('=') {
2128 smol_str::SmolStr::from(&name[eq_pos + 1..])
2129 } else if let Some(var) = self.vm.state.env.get(name) {
2130 var.value.as_scalar()
2131 } else {
2132 smol_str::SmolStr::default()
2133 };
2134 let actual_name = name.find('=').map_or(name, |eq| &name[..eq]);
2135 self.vm.state.env.set(
2136 smol_str::SmolStr::from(actual_name),
2137 wasmsh_state::ShellVar {
2138 value: wasmsh_state::VarValue::Scalar(target_value),
2139 exported: false,
2140 readonly: false,
2141 integer: false,
2142 nameref: true,
2143 },
2144 );
2145 }
2146
2147 fn should_stop_execution(&self) -> bool {
2148 self.exec.break_depth > 0
2149 || self.exec.loop_continue
2150 || self.exec.exit_requested.is_some()
2151 || self.exec.resource_exhausted
2152 }
2153
2154 fn check_resource_limits(&mut self) -> bool {
2157 if self.exec.resource_exhausted {
2158 return true;
2159 }
2160 self.vm.steps += 1;
2162 if self.vm.limits.step_limit > 0 && self.vm.steps >= self.vm.limits.step_limit {
2163 self.exec.resource_exhausted = true;
2164 self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2165 level: wasmsh_vm::DiagLevel::Error,
2166 category: wasmsh_vm::DiagCategory::Budget,
2167 message: format!(
2168 "step budget exhausted: {} steps (limit: {})",
2169 self.vm.steps, self.vm.limits.step_limit
2170 ),
2171 });
2172 return true;
2173 }
2174 if self.vm.cancellation_token().is_cancelled() {
2176 self.exec.resource_exhausted = true;
2177 self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2178 level: wasmsh_vm::DiagLevel::Error,
2179 category: wasmsh_vm::DiagCategory::Budget,
2180 message: "execution cancelled".to_string(),
2181 });
2182 return true;
2183 }
2184 if self.vm.limits.output_byte_limit > 0
2186 && self.vm.output_bytes > self.vm.limits.output_byte_limit
2187 {
2188 self.exec.resource_exhausted = true;
2189 self.vm.diagnostics.push(wasmsh_vm::DiagnosticEvent {
2190 level: wasmsh_vm::DiagLevel::Error,
2191 category: wasmsh_vm::DiagCategory::Budget,
2192 message: format!(
2193 "output limit exceeded: {} bytes (limit: {})",
2194 self.vm.output_bytes, self.vm.limits.output_byte_limit
2195 ),
2196 });
2197 return true;
2198 }
2199 false
2200 }
2201
2202 fn execute_body(&mut self, body: &[HirCompleteCommand]) {
2203 for cc in body {
2204 if self.should_stop_execution() || self.check_resource_limits() {
2205 break;
2206 }
2207 self.execute_complete_command(cc);
2208 }
2209 }
2210
2211 fn execute_complete_command(&mut self, cc: &HirCompleteCommand) {
2212 for and_or in &cc.list {
2213 if self.should_stop_execution() {
2214 break;
2215 }
2216 self.execute_pipeline_chain(and_or);
2217 if self.should_errexit(and_or) {
2218 self.exec.exit_requested = Some(self.vm.state.last_status);
2219 }
2220 }
2221 }
2222
2223 fn expand_assignment_value(&mut self, value: Option<&wasmsh_ast::Word>) -> String {
2225 if let Some(w) = value {
2226 let resolved = self.resolve_command_subst(std::slice::from_ref(w));
2227 wasmsh_expand::expand_word(&resolved[0], &mut self.vm.state)
2228 } else {
2229 String::new()
2230 }
2231 }
2232
2233 fn execute_assignment(
2239 &mut self,
2240 raw_name: &smol_str::SmolStr,
2241 value: Option<&wasmsh_ast::Word>,
2242 ) {
2243 let (name_str, is_append) = Self::split_assignment_name(raw_name.as_str());
2244 if self.try_assign_array_element(name_str, value) {
2245 return;
2246 }
2247
2248 let val_str = self.expand_assignment_value(value);
2249 if val_str.starts_with('(') && val_str.ends_with(')') {
2250 self.assign_compound_array(name_str, &val_str, is_append);
2251 return;
2252 }
2253
2254 let final_val = self.resolve_scalar_assignment_value(name_str, &val_str, is_append);
2255 self.vm
2256 .state
2257 .set_var(smol_str::SmolStr::from(name_str), final_val.into());
2258 }
2259
2260 fn split_assignment_name(name: &str) -> (&str, bool) {
2261 if let Some(stripped) = name.strip_suffix('+') {
2262 (stripped, true)
2263 } else {
2264 (name, false)
2265 }
2266 }
2267
2268 fn parse_array_element_assignment(name: &str) -> Option<(&str, &str)> {
2269 let bracket_pos = name.find('[')?;
2270 name.ends_with(']')
2271 .then_some((&name[..bracket_pos], &name[bracket_pos + 1..name.len() - 1]))
2272 }
2273
2274 fn try_assign_array_element(&mut self, name: &str, value: Option<&wasmsh_ast::Word>) -> bool {
2275 let Some((base, index)) = Self::parse_array_element_assignment(name) else {
2276 return false;
2277 };
2278 let val = self.expand_assignment_value(value);
2279 self.vm
2280 .state
2281 .set_array_element(smol_str::SmolStr::from(base), index, val.into());
2282 true
2283 }
2284
2285 fn resolve_scalar_assignment_value(
2286 &mut self,
2287 name: &str,
2288 value: &str,
2289 is_append: bool,
2290 ) -> String {
2291 if self.vm.state.env.get(name).is_some_and(|v| v.integer) {
2292 return self.eval_integer_assignment(name, value, is_append);
2293 }
2294 if is_append {
2295 return format!(
2296 "{}{}",
2297 self.vm.state.get_var(name).unwrap_or_default(),
2298 value
2299 );
2300 }
2301 value.to_string()
2302 }
2303
2304 fn eval_integer_assignment(&mut self, name: &str, value: &str, is_append: bool) -> String {
2305 let arith_input = if is_append {
2306 format!(
2307 "{}+{}",
2308 self.vm.state.get_var(name).unwrap_or_default(),
2309 value
2310 )
2311 } else {
2312 value.to_string()
2313 };
2314 wasmsh_expand::eval_arithmetic(&arith_input, &mut self.vm.state).to_string()
2315 }
2316
2317 fn assign_compound_array(&mut self, name_str: &str, val_str: &str, is_append: bool) {
2319 let inner = &val_str[1..val_str.len() - 1];
2320 let elements = Self::parse_array_elements(inner);
2321 let name_key = smol_str::SmolStr::from(name_str);
2322
2323 if is_append {
2324 self.vm.state.append_array(name_str, elements);
2325 return;
2326 }
2327
2328 if Self::is_assoc_array_assignment(inner, &elements) {
2329 self.assign_assoc_array(&name_key, inner);
2330 return;
2331 }
2332 self.assign_indexed_array(&name_key, &elements);
2333 }
2334
2335 fn is_assoc_array_assignment(inner: &str, elements: &[smol_str::SmolStr]) -> bool {
2336 !elements.is_empty() && inner.contains('[') && inner.contains("]=")
2337 }
2338
2339 fn assign_assoc_array(&mut self, name_key: &smol_str::SmolStr, inner: &str) {
2340 self.vm.state.init_assoc_array(name_key.clone());
2341 for (key, value) in Self::parse_assoc_pairs(inner) {
2342 self.vm.state.set_array_element(
2343 name_key.clone(),
2344 &key,
2345 smol_str::SmolStr::from(value.as_str()),
2346 );
2347 }
2348 }
2349
2350 fn assign_indexed_array(
2351 &mut self,
2352 name_key: &smol_str::SmolStr,
2353 elements: &[smol_str::SmolStr],
2354 ) {
2355 self.vm.state.init_indexed_array(name_key.clone());
2356 for (i, elem) in elements.iter().enumerate() {
2357 self.vm
2358 .state
2359 .set_array_element(name_key.clone(), &i.to_string(), elem.clone());
2360 }
2361 }
2362
2363 fn push_array_element(elements: &mut Vec<smol_str::SmolStr>, current: &mut String) {
2364 if current.is_empty() {
2365 return;
2366 }
2367 elements.push(smol_str::SmolStr::from(current.as_str()));
2368 current.clear();
2369 }
2370
2371 fn parse_array_elements(inner: &str) -> Vec<smol_str::SmolStr> {
2374 let mut elements = Vec::new();
2375 let mut current = String::new();
2376 let mut state = ArrayParseState::default();
2377
2378 for ch in inner.chars() {
2379 match state.process_char(ch) {
2380 ArrayCharAction::Append(c) => current.push(c),
2381 ArrayCharAction::Skip => {}
2382 ArrayCharAction::SplitField => {
2383 Self::push_array_element(&mut elements, &mut current);
2384 }
2385 }
2386 }
2387 Self::push_array_element(&mut elements, &mut current);
2388 elements
2389 }
2390
2391 fn parse_assoc_pairs(inner: &str) -> Vec<(String, String)> {
2393 let mut pairs = Vec::new();
2394 let mut pos = 0;
2395 let bytes = inner.as_bytes();
2396
2397 while pos < bytes.len() {
2398 Self::skip_ascii_whitespace(bytes, &mut pos);
2399 if pos >= bytes.len() {
2400 break;
2401 }
2402 if let Some(key) = Self::parse_assoc_key(inner, &mut pos) {
2403 pairs.push((key, Self::parse_assoc_value(inner, &mut pos)));
2404 continue;
2405 }
2406 Self::skip_non_whitespace(bytes, &mut pos);
2407 }
2408 pairs
2409 }
2410
2411 fn skip_ascii_whitespace(bytes: &[u8], pos: &mut usize) {
2412 while *pos < bytes.len() && bytes[*pos].is_ascii_whitespace() {
2413 *pos += 1;
2414 }
2415 }
2416
2417 fn skip_non_whitespace(bytes: &[u8], pos: &mut usize) {
2418 while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
2419 *pos += 1;
2420 }
2421 }
2422
2423 fn parse_assoc_key(inner: &str, pos: &mut usize) -> Option<String> {
2424 let bytes = inner.as_bytes();
2425 if *pos >= bytes.len() || bytes[*pos] != b'[' {
2426 return None;
2427 }
2428
2429 *pos += 1;
2430 let key_start = *pos;
2431 while *pos < bytes.len() && bytes[*pos] != b']' {
2432 *pos += 1;
2433 }
2434 let key = inner[key_start..*pos].to_string();
2435 if *pos < bytes.len() {
2436 *pos += 1;
2437 }
2438 if *pos < bytes.len() && bytes[*pos] == b'=' {
2439 *pos += 1;
2440 }
2441 Some(key)
2442 }
2443
2444 fn parse_assoc_value(inner: &str, pos: &mut usize) -> String {
2446 let bytes = inner.as_bytes();
2447 match bytes.get(*pos).copied() {
2448 Some(b'"') => Self::parse_double_quoted_assoc_value(bytes, pos),
2449 Some(b'\'') => Self::parse_single_quoted_assoc_value(bytes, pos),
2450 _ => Self::parse_unquoted_assoc_value(bytes, pos),
2451 }
2452 }
2453
2454 fn parse_double_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2455 let mut value = String::new();
2456 *pos += 1;
2457 while *pos < bytes.len() && bytes[*pos] != b'"' {
2458 if bytes[*pos] == b'\\' && *pos + 1 < bytes.len() {
2459 *pos += 1;
2460 }
2461 value.push(bytes[*pos] as char);
2462 *pos += 1;
2463 }
2464 if *pos < bytes.len() {
2465 *pos += 1;
2466 }
2467 value
2468 }
2469
2470 fn parse_single_quoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2471 let mut value = String::new();
2472 *pos += 1;
2473 while *pos < bytes.len() && bytes[*pos] != b'\'' {
2474 value.push(bytes[*pos] as char);
2475 *pos += 1;
2476 }
2477 if *pos < bytes.len() {
2478 *pos += 1;
2479 }
2480 value
2481 }
2482
2483 fn parse_unquoted_assoc_value(bytes: &[u8], pos: &mut usize) -> String {
2484 let mut value = String::new();
2485 while *pos < bytes.len() && !bytes[*pos].is_ascii_whitespace() {
2486 value.push(bytes[*pos] as char);
2487 *pos += 1;
2488 }
2489 value
2490 }
2491
2492 const MAX_GLOB_RESULTS: usize = 10_000;
2494
2495 fn expand_globs_tagged(&mut self, argv: Vec<(String, bool)>) -> Vec<String> {
2501 if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
2502 return argv.into_iter().map(|(s, _)| s).collect();
2503 }
2504 let nullglob = self.get_shopt_value("nullglob");
2505 let dotglob = self.get_shopt_value("dotglob");
2506 let globstar = self.get_shopt_value("globstar");
2507 let extglob = self.get_shopt_value("extglob");
2508
2509 let mut result = Vec::new();
2510 for (arg, quoted) in argv {
2511 if quoted {
2512 result.push(arg);
2513 } else {
2514 result.extend(
2515 self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob),
2516 );
2517 }
2518 }
2519 result.truncate(Self::MAX_GLOB_RESULTS);
2520 result
2521 }
2522
2523 fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
2524 if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
2525 return argv;
2526 }
2527 let nullglob = self.get_shopt_value("nullglob");
2528 let dotglob = self.get_shopt_value("dotglob");
2529 let globstar = self.get_shopt_value("globstar");
2530 let extglob = self.get_shopt_value("extglob");
2531
2532 let mut result = Vec::new();
2533 for arg in argv {
2534 result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
2535 }
2536 result.truncate(Self::MAX_GLOB_RESULTS);
2537 result
2538 }
2539
2540 #[allow(clippy::fn_params_excessive_bools)]
2541 fn expand_glob_arg(
2542 &self,
2543 arg: String,
2544 nullglob: bool,
2545 dotglob: bool,
2546 globstar: bool,
2547 extglob: bool,
2548 ) -> Vec<String> {
2549 if !Self::is_glob_pattern(&arg, extglob) {
2550 return vec![arg];
2551 }
2552 if globstar && arg.contains("**") {
2553 return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
2554 }
2555 self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
2556 }
2557
2558 fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
2559 let has_bracket_class = arg.contains('[') && arg.contains(']');
2560 arg.contains('*')
2561 || arg.contains('?')
2562 || has_bracket_class
2563 || (extglob && has_extglob_pattern(arg))
2564 }
2565
2566 fn expand_globstar_arg(
2567 &self,
2568 arg: String,
2569 nullglob: bool,
2570 dotglob: bool,
2571 extglob: bool,
2572 ) -> Vec<String> {
2573 let mut matches = self.expand_globstar(&arg, dotglob, extglob);
2574 matches.sort();
2575 self.finalize_glob_matches(arg, matches, nullglob)
2576 }
2577
2578 fn expand_standard_glob_arg(
2579 &self,
2580 arg: String,
2581 nullglob: bool,
2582 dotglob: bool,
2583 extglob: bool,
2584 ) -> Vec<String> {
2585 let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
2586 return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
2587 };
2588 let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
2589 self.finalize_glob_matches(arg, matches, nullglob)
2590 }
2591
2592 fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
2593 let Some(slash_pos) = arg.rfind('/') else {
2594 return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
2595 };
2596
2597 let dir_part = &arg[..=slash_pos];
2598 if Self::path_segment_has_glob(dir_part) {
2599 return None;
2600 }
2601
2602 Some((
2603 self.resolve_cwd_path(dir_part),
2604 arg[slash_pos + 1..].to_string(),
2605 Some(dir_part.to_string()),
2606 ))
2607 }
2608
2609 fn path_segment_has_glob(path: &str) -> bool {
2610 path.contains('*') || path.contains('?') || path.contains('[')
2611 }
2612
2613 fn read_glob_matches(
2614 &self,
2615 dir: &str,
2616 pattern: &str,
2617 prefix: Option<&str>,
2618 dotglob: bool,
2619 extglob: bool,
2620 ) -> Vec<String> {
2621 let Ok(entries) = self.fs.read_dir(dir) else {
2622 return Vec::new();
2623 };
2624
2625 let mut matches: Vec<String> = entries
2626 .iter()
2627 .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
2628 .map(|e| match prefix {
2629 Some(prefix) => format!("{prefix}{}", e.name),
2630 None => e.name.clone(),
2631 })
2632 .collect();
2633 matches.sort();
2634 matches
2635 }
2636
2637 #[allow(clippy::unused_self)]
2638 fn finalize_glob_matches(
2639 &self,
2640 arg: String,
2641 matches: Vec<String>,
2642 nullglob: bool,
2643 ) -> Vec<String> {
2644 if !matches.is_empty() {
2645 return matches;
2646 }
2647 if nullglob {
2648 Vec::new()
2649 } else {
2650 vec![arg]
2651 }
2652 }
2653
2654 fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
2656 let segments: Vec<&str> = pattern.split('/').collect();
2658 let base_dir = self.vm.state.cwd.clone();
2659 let mut matches = Vec::new();
2660 self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
2661 matches
2662 }
2663
2664 fn globstar_walk(
2666 &self,
2667 dir: &str,
2668 segments: &[&str],
2669 seg_idx: usize,
2670 prefix: &str,
2671 dotglob: bool,
2672 extglob: bool,
2673 matches: &mut Vec<String>,
2674 ) {
2675 if seg_idx >= segments.len() {
2676 return;
2677 }
2678
2679 let seg = segments[seg_idx];
2680 if seg == "**" {
2681 self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
2682 return;
2683 }
2684 self.globstar_walk_segment(
2685 dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
2686 );
2687 }
2688
2689 fn globstar_walk_wildcard(
2690 &self,
2691 dir: &str,
2692 segments: &[&str],
2693 seg_idx: usize,
2694 prefix: &str,
2695 dotglob: bool,
2696 extglob: bool,
2697 matches: &mut Vec<String>,
2698 ) {
2699 if seg_idx + 1 < segments.len() {
2700 self.globstar_walk(
2701 dir,
2702 segments,
2703 seg_idx + 1,
2704 prefix,
2705 dotglob,
2706 extglob,
2707 matches,
2708 );
2709 }
2710
2711 let Ok(entries) = self.fs.read_dir(dir) else {
2712 return;
2713 };
2714 for entry in &entries {
2715 if !dotglob && entry.name.starts_with('.') {
2716 continue;
2717 }
2718 let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
2719 if self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false) {
2720 self.globstar_walk(
2721 &child_path,
2722 segments,
2723 seg_idx,
2724 &child_prefix,
2725 dotglob,
2726 extglob,
2727 matches,
2728 );
2729 }
2730 }
2731 }
2732
2733 #[allow(clippy::too_many_arguments)]
2734 fn globstar_walk_segment(
2735 &self,
2736 dir: &str,
2737 seg: &str,
2738 segments: &[&str],
2739 seg_idx: usize,
2740 prefix: &str,
2741 dotglob: bool,
2742 extglob: bool,
2743 matches: &mut Vec<String>,
2744 ) {
2745 let Ok(entries) = self.fs.read_dir(dir) else {
2746 return;
2747 };
2748 let is_last = seg_idx == segments.len() - 1;
2749
2750 for entry in &entries {
2751 if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
2752 continue;
2753 }
2754 self.globstar_handle_matched_entry(
2755 dir,
2756 segments,
2757 seg_idx,
2758 prefix,
2759 dotglob,
2760 extglob,
2761 matches,
2762 &entry.name,
2763 is_last,
2764 );
2765 }
2766 }
2767
2768 #[allow(clippy::too_many_arguments)]
2769 fn globstar_handle_matched_entry(
2770 &self,
2771 dir: &str,
2772 segments: &[&str],
2773 seg_idx: usize,
2774 prefix: &str,
2775 dotglob: bool,
2776 extglob: bool,
2777 matches: &mut Vec<String>,
2778 name: &str,
2779 is_last: bool,
2780 ) {
2781 let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
2782 if is_last {
2783 matches.push(child_prefix);
2784 return;
2785 }
2786 let is_dir = self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false);
2787 if is_dir {
2788 self.globstar_walk(
2789 &child_path,
2790 segments,
2791 seg_idx + 1,
2792 &child_prefix,
2793 dotglob,
2794 extglob,
2795 matches,
2796 );
2797 }
2798 }
2799
2800 fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
2801 let child_path = if dir == "/" {
2802 format!("/{name}")
2803 } else {
2804 format!("{dir}/{name}")
2805 };
2806 let child_prefix = if prefix.is_empty() {
2807 name.to_string()
2808 } else {
2809 format!("{prefix}/{name}")
2810 };
2811 (child_path, child_prefix)
2812 }
2813
2814 fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
2816 match self.fs.open(path, opts) {
2817 Ok(h) => {
2818 if let Err(e) = self.fs.write_file(h, data) {
2819 self.vm
2820 .stderr
2821 .extend_from_slice(format!("wasmsh: write error: {e}\n").as_bytes());
2822 }
2823 self.fs.close(h);
2824 }
2825 Err(e) => {
2826 self.vm
2827 .stderr
2828 .extend_from_slice(format!("wasmsh: {target}: {e}\n").as_bytes());
2829 }
2830 }
2831 }
2832
2833 fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
2835 let data = self.vm.stdout[from..].to_vec();
2836 self.vm.stdout.truncate(from);
2837 data
2838 }
2839
2840 fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
2844 for redir in redirections {
2845 let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
2847 let resolved_target = resolved.first().unwrap_or(&redir.target);
2848 let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
2849 let path = self.resolve_cwd_path(&target);
2850 let fd = redir.fd.unwrap_or(1);
2851
2852 match redir.op {
2853 RedirectionOp::Output => {
2854 self.apply_output_redir(&path, &target, fd, stdout_before);
2855 }
2856 RedirectionOp::Append => {
2857 self.apply_append_redir(&path, &target, fd, stdout_before);
2858 }
2859 RedirectionOp::DupOutput => {
2860 let target_fd: u32 = target.parse().unwrap_or(1);
2861 let source_fd = redir.fd.unwrap_or(1);
2862 if source_fd == 2 && target_fd == 1 {
2863 let stderr_data = std::mem::take(&mut self.vm.stderr);
2864 self.vm.stdout.extend_from_slice(&stderr_data);
2865 } else if source_fd == 1 && target_fd == 2 {
2866 let stdout_data = self.capture_stdout(stdout_before);
2867 self.vm.stderr.extend_from_slice(&stdout_data);
2868 }
2869 }
2870 #[allow(unreachable_patterns)]
2871 _ => {}
2872 }
2873 }
2874 }
2875
2876 fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2878 let data = if fd == FD_BOTH {
2879 let mut combined = self.capture_stdout(stdout_before);
2880 combined.extend_from_slice(&std::mem::take(&mut self.vm.stderr));
2881 combined
2882 } else if fd == 2 {
2883 std::mem::take(&mut self.vm.stderr)
2884 } else {
2885 self.capture_stdout(stdout_before)
2886 };
2887 self.write_to_file(path, target, &data, OpenOptions::write());
2888 }
2889
2890 fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2892 let data = if fd == 2 {
2893 std::mem::take(&mut self.vm.stderr)
2894 } else {
2895 self.capture_stdout(stdout_before)
2896 };
2897 self.write_to_file(path, target, &data, OpenOptions::append());
2898 }
2899}
2900
2901fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
2903 match level {
2904 DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
2905 DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
2906 DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
2907 _ => wasmsh_vm::DiagLevel::Info,
2908 }
2909}
2910
2911fn dbl_bracket_eval_or(
2915 tokens: &[String],
2916 pos: &mut usize,
2917 fs: &BackendFs,
2918 state: &mut ShellState,
2919) -> bool {
2920 let mut result = dbl_bracket_eval_and(tokens, pos, fs, state);
2921 while *pos < tokens.len() && tokens[*pos] == "||" {
2922 *pos += 1;
2923 let rhs = dbl_bracket_eval_and(tokens, pos, fs, state);
2924 result = result || rhs;
2925 }
2926 result
2927}
2928
2929fn dbl_bracket_eval_and(
2931 tokens: &[String],
2932 pos: &mut usize,
2933 fs: &BackendFs,
2934 state: &mut ShellState,
2935) -> bool {
2936 let mut result = dbl_bracket_eval_not(tokens, pos, fs, state);
2937 while *pos < tokens.len() && tokens[*pos] == "&&" {
2938 *pos += 1;
2939 let rhs = dbl_bracket_eval_not(tokens, pos, fs, state);
2940 result = result && rhs;
2941 }
2942 result
2943}
2944
2945fn dbl_bracket_eval_not(
2947 tokens: &[String],
2948 pos: &mut usize,
2949 fs: &BackendFs,
2950 state: &mut ShellState,
2951) -> bool {
2952 if *pos < tokens.len() && tokens[*pos] == "!" {
2953 *pos += 1;
2954 return !dbl_bracket_eval_not(tokens, pos, fs, state);
2955 }
2956 dbl_bracket_eval_primary(tokens, pos, fs, state)
2957}
2958
2959fn dbl_bracket_eval_primary(
2961 tokens: &[String],
2962 pos: &mut usize,
2963 fs: &BackendFs,
2964 state: &mut ShellState,
2965) -> bool {
2966 if *pos >= tokens.len() {
2967 return false;
2968 }
2969 if let Some(result) = dbl_bracket_try_group(tokens, pos, fs, state) {
2970 return result;
2971 }
2972 if let Some(result) = dbl_bracket_try_unary(tokens, pos, fs) {
2973 return result;
2974 }
2975 if *pos + 1 == tokens.len() {
2976 return dbl_bracket_take_truthy_token(tokens, pos);
2977 }
2978 if let Some(result) = dbl_bracket_try_binary(tokens, pos, state) {
2979 return result;
2980 }
2981 dbl_bracket_take_truthy_token(tokens, pos)
2982}
2983
2984fn dbl_bracket_try_group(
2985 tokens: &[String],
2986 pos: &mut usize,
2987 fs: &BackendFs,
2988 state: &mut ShellState,
2989) -> Option<bool> {
2990 if tokens.get(*pos).map(String::as_str) != Some("(") {
2991 return None;
2992 }
2993
2994 *pos += 1;
2995 let result = dbl_bracket_eval_or(tokens, pos, fs, state);
2996 if tokens.get(*pos).map(String::as_str) == Some(")") {
2997 *pos += 1;
2998 }
2999 Some(result)
3000}
3001
3002fn dbl_bracket_take_truthy_token(tokens: &[String], pos: &mut usize) -> bool {
3003 let Some(token) = tokens.get(*pos) else {
3004 return false;
3005 };
3006 *pos += 1;
3007 !token.is_empty()
3008}
3009
3010fn dbl_bracket_try_unary(tokens: &[String], pos: &mut usize, fs: &BackendFs) -> Option<bool> {
3012 if *pos + 1 >= tokens.len() {
3013 return None;
3014 }
3015 let flag = dbl_bracket_parse_unary_flag(&tokens[*pos])?;
3016 match flag {
3017 b'z' | b'n' => Some(dbl_bracket_eval_string_test(tokens, pos, flag)),
3018 b'f' | b'd' | b'e' | b's' | b'r' | b'w' | b'x' => {
3019 dbl_bracket_eval_file_test(tokens, pos, flag, fs)
3020 }
3021 _ => None,
3022 }
3023}
3024
3025fn dbl_bracket_parse_unary_flag(op: &str) -> Option<u8> {
3026 if !op.starts_with('-') || op.len() != 2 {
3027 return None;
3028 }
3029 Some(op.as_bytes()[1])
3030}
3031
3032fn dbl_bracket_eval_string_test(tokens: &[String], pos: &mut usize, flag: u8) -> bool {
3033 *pos += 1;
3034 let arg = &tokens[*pos];
3035 *pos += 1;
3036 if flag == b'z' {
3037 arg.is_empty()
3038 } else {
3039 !arg.is_empty()
3040 }
3041}
3042
3043fn dbl_bracket_eval_file_test(
3044 tokens: &[String],
3045 pos: &mut usize,
3046 flag: u8,
3047 fs: &BackendFs,
3048) -> Option<bool> {
3049 if *pos + 2 < tokens.len() && is_binary_op(&tokens[*pos + 2]) {
3050 return None;
3051 }
3052 *pos += 1;
3053 let path_str = &tokens[*pos];
3054 *pos += 1;
3055 Some(eval_file_test(flag, path_str, fs))
3056}
3057
3058fn dbl_bracket_try_binary(
3060 tokens: &[String],
3061 pos: &mut usize,
3062 state: &mut ShellState,
3063) -> Option<bool> {
3064 if *pos + 2 > tokens.len() {
3065 return None;
3066 }
3067 let op_idx = *pos + 1;
3068 if op_idx >= tokens.len() || !is_binary_op(&tokens[op_idx]) {
3069 return None;
3070 }
3071 let lhs = tokens[*pos].clone();
3072 *pos += 1;
3073 let op = tokens[*pos].clone();
3074 *pos += 1;
3075
3076 let rhs = dbl_bracket_collect_rhs(tokens, pos, &op);
3077 Some(eval_binary_op(&lhs, &op, &rhs, state))
3078}
3079
3080fn dbl_bracket_collect_rhs(tokens: &[String], pos: &mut usize, op: &str) -> String {
3083 if *pos >= tokens.len() {
3084 return String::new();
3085 }
3086 if op == "=~" {
3087 return dbl_bracket_collect_regex_rhs(tokens, pos);
3088 }
3089 let rhs = tokens[*pos].clone();
3090 *pos += 1;
3091 rhs
3092}
3093
3094fn dbl_bracket_collect_regex_rhs(tokens: &[String], pos: &mut usize) -> String {
3095 let mut rhs = String::new();
3096 while *pos < tokens.len() && tokens[*pos] != "&&" && tokens[*pos] != "||" {
3097 rhs.push_str(&tokens[*pos]);
3098 *pos += 1;
3099 }
3100 rhs
3101}
3102
3103fn is_binary_op(s: &str) -> bool {
3105 matches!(
3106 s,
3107 "==" | "!=" | "=~" | "=" | "<" | ">" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge"
3108 )
3109}
3110
3111fn eval_binary_op(lhs: &str, op: &str, rhs: &str, state: &mut ShellState) -> bool {
3113 match op {
3114 "==" | "=" => glob_cmp(lhs, rhs, state, false),
3115 "!=" => !glob_cmp(lhs, rhs, state, false),
3116 "=~" => eval_regex_match(lhs, rhs, state),
3117 "<" => lhs < rhs,
3118 ">" => lhs > rhs,
3119 _ => eval_int_cmp(lhs, op, rhs),
3120 }
3121}
3122
3123fn glob_cmp(lhs: &str, rhs: &str, state: &ShellState, _negate: bool) -> bool {
3125 let nocasematch = state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
3126 if nocasematch {
3127 glob_match_inner(rhs.to_lowercase().as_bytes(), lhs.to_lowercase().as_bytes())
3128 } else {
3129 glob_match_inner(rhs.as_bytes(), lhs.as_bytes())
3130 }
3131}
3132
3133fn eval_regex_match(lhs: &str, rhs: &str, state: &mut ShellState) -> bool {
3135 let captures = regex_match_with_captures(lhs, rhs);
3136 let br_name = smol_str::SmolStr::from("BASH_REMATCH");
3137 let Some(caps) = captures else {
3138 state.init_indexed_array(br_name);
3139 return false;
3140 };
3141 state.init_indexed_array(br_name.clone());
3142 for (i, cap) in caps.iter().enumerate() {
3143 state.set_array_element(
3144 br_name.clone(),
3145 &i.to_string(),
3146 smol_str::SmolStr::from(cap.as_str()),
3147 );
3148 }
3149 true
3150}
3151
3152fn eval_int_cmp(lhs: &str, op: &str, rhs: &str) -> bool {
3154 let a: i64 = lhs.trim().parse().unwrap_or(0);
3155 let b: i64 = rhs.trim().parse().unwrap_or(0);
3156 match op {
3157 "-eq" => a == b,
3158 "-ne" => a != b,
3159 "-lt" => a < b,
3160 "-le" => a <= b,
3161 "-gt" => a > b,
3162 "-ge" => a >= b,
3163 _ => false,
3164 }
3165}
3166
3167fn eval_file_test(flag: u8, path: &str, fs: &BackendFs) -> bool {
3169 use wasmsh_fs::Vfs;
3170 match fs.stat(path) {
3171 Ok(meta) => match flag {
3172 b'f' => !meta.is_dir,
3173 b'd' => meta.is_dir,
3174 b's' => meta.size > 0,
3175 b'e' | b'r' | b'w' | b'x' => true,
3177 _ => false,
3178 },
3179 Err(_) => false,
3180 }
3181}
3182
3183fn regex_strip_anchors(pattern: &str) -> (&str, bool, bool) {
3185 let anchored_start = pattern.starts_with('^');
3186 let anchored_end = pattern.ends_with('$') && !pattern.ends_with("\\$");
3187 let core = match (anchored_start, anchored_end) {
3188 (true, true) if pattern.len() >= 2 => &pattern[1..pattern.len() - 1],
3189 (true, _) => &pattern[1..],
3190 (_, true) => &pattern[..pattern.len() - 1],
3191 _ => pattern,
3192 };
3193 (core, anchored_start, anchored_end)
3194}
3195
3196fn has_regex_metachar(core: &str) -> bool {
3198 core.contains('.')
3199 || core.contains('+')
3200 || core.contains('*')
3201 || core.contains('?')
3202 || core.contains('[')
3203 || core.contains('(')
3204 || core.contains('|')
3205}
3206
3207fn literal_match_range(text: &str, core: &str, start: bool, end: bool) -> Option<(usize, usize)> {
3209 match (start, end) {
3210 (true, true) if text == core => Some((0, text.len())),
3211 (true, false) if text.starts_with(core) => Some((0, core.len())),
3212 (false, true) if text.ends_with(core) => Some((text.len() - core.len(), text.len())),
3213 (false, false) => text.find(core).map(|pos| (pos, pos + core.len())),
3214 _ => None,
3215 }
3216}
3217
3218fn regex_match_with_captures(text: &str, pattern: &str) -> Option<Vec<String>> {
3224 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3225
3226 if !has_regex_metachar(core) {
3227 return regex_match_literal_with_captures(text, core, anchored_start, anchored_end);
3228 }
3229
3230 regex_find_first_match(text, core, anchored_start, anchored_end)
3231}
3232
3233fn regex_find_first_match(
3234 text: &str,
3235 core: &str,
3236 anchored_start: bool,
3237 anchored_end: bool,
3238) -> Option<Vec<String>> {
3239 let end = if anchored_start { 0 } else { text.len() };
3240 for start in 0..=end {
3241 if let Some(result) = regex_match_from_start(text, core, anchored_end, start) {
3242 return Some(result);
3243 }
3244 }
3245 None
3246}
3247
3248fn regex_match_literal_with_captures(
3249 text: &str,
3250 core: &str,
3251 anchored_start: bool,
3252 anchored_end: bool,
3253) -> Option<Vec<String>> {
3254 literal_match_range(text, core, anchored_start, anchored_end)
3255 .map(|(s, e)| vec![text[s..e].to_string()])
3256}
3257
3258fn regex_match_from_start(
3259 text: &str,
3260 core: &str,
3261 anchored_end: bool,
3262 start: usize,
3263) -> Option<Vec<String>> {
3264 let mut group_caps: Vec<(usize, usize)> = Vec::new();
3265 let end = regex_match_capturing(
3266 text.as_bytes(),
3267 start,
3268 core.as_bytes(),
3269 0,
3270 anchored_end,
3271 &mut group_caps,
3272 )?;
3273 Some(regex_build_capture_list(text, start, end, &group_caps))
3274}
3275
3276fn regex_build_capture_list(
3277 text: &str,
3278 start: usize,
3279 end: usize,
3280 group_caps: &[(usize, usize)],
3281) -> Vec<String> {
3282 let mut result = vec![text[start..end].to_string()];
3283 for &(gs, ge) in group_caps {
3284 result.push(text[gs..ge].to_string());
3285 }
3286 result
3287}
3288
3289fn regex_match_capturing(
3293 text: &[u8],
3294 ti: usize,
3295 pat: &[u8],
3296 pi: usize,
3297 must_end: bool,
3298 captures: &mut Vec<(usize, usize)>,
3299) -> Option<usize> {
3300 if pi >= pat.len() {
3301 return regex_check_end(ti, text.len(), must_end);
3302 }
3303
3304 if pat[pi] == b'(' {
3305 return regex_match_group(text, ti, pat, pi, must_end, captures);
3306 }
3307
3308 regex_match_elem(text, ti, pat, pi, must_end, captures)
3309}
3310
3311fn regex_check_end(ti: usize, text_len: usize, must_end: bool) -> Option<usize> {
3313 if must_end && ti < text_len {
3314 None
3315 } else {
3316 Some(ti)
3317 }
3318}
3319
3320fn regex_match_group(
3322 text: &[u8],
3323 ti: usize,
3324 pat: &[u8],
3325 pi: usize,
3326 must_end: bool,
3327 captures: &mut Vec<(usize, usize)>,
3328) -> Option<usize> {
3329 let close = find_matching_paren_bytes(pat, pi + 1)?;
3330 let inner = &pat[pi + 1..close];
3331 let rest = &pat[close + 1..];
3332 let (quant, after_quant_offset) = parse_group_quantifier(pat, close);
3333 let after_quant = &pat[after_quant_offset..];
3334 let alternatives = split_alternatives_bytes(inner);
3335
3336 regex_dispatch_group_quant(
3337 text,
3338 ti,
3339 rest,
3340 after_quant,
3341 must_end,
3342 captures,
3343 &alternatives,
3344 quant,
3345 )
3346}
3347
3348fn parse_group_quantifier(pat: &[u8], close: usize) -> (u8, usize) {
3349 if close + 1 < pat.len() {
3350 match pat[close + 1] {
3351 q @ (b'*' | b'+' | b'?') => (q, close + 2),
3352 _ => (0, close + 1),
3353 }
3354 } else {
3355 (0, close + 1)
3356 }
3357}
3358
3359#[allow(clippy::too_many_arguments)]
3360fn regex_dispatch_group_quant(
3361 text: &[u8],
3362 ti: usize,
3363 rest: &[u8],
3364 after_quant: &[u8],
3365 must_end: bool,
3366 captures: &mut Vec<(usize, usize)>,
3367 alternatives: &[Vec<u8>],
3368 quant: u8,
3369) -> Option<usize> {
3370 match quant {
3371 b'+' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 1),
3372 b'*' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 0),
3373 b'?' => regex_match_group_opt(text, ti, after_quant, must_end, captures, alternatives),
3374 _ => regex_match_group_exact(text, ti, rest, must_end, captures, alternatives),
3375 }
3376}
3377
3378fn regex_match_group_rep(
3380 text: &[u8],
3381 ti: usize,
3382 after: &[u8],
3383 must_end: bool,
3384 captures: &mut Vec<(usize, usize)>,
3385 alternatives: &[Vec<u8>],
3386 min_reps: usize,
3387) -> Option<usize> {
3388 let save = captures.len();
3389 for end_pos in (ti..=text.len()).rev() {
3390 captures.truncate(save);
3391 if let Some(result) = regex_try_group_rep_at(
3392 text,
3393 ti,
3394 end_pos,
3395 after,
3396 must_end,
3397 captures,
3398 alternatives,
3399 min_reps,
3400 save,
3401 ) {
3402 return Some(result);
3403 }
3404 }
3405 captures.truncate(save);
3406 None
3407}
3408
3409#[allow(clippy::too_many_arguments)]
3410fn regex_try_group_rep_at(
3411 text: &[u8],
3412 ti: usize,
3413 end_pos: usize,
3414 after: &[u8],
3415 must_end: bool,
3416 captures: &mut Vec<(usize, usize)>,
3417 alternatives: &[Vec<u8>],
3418 min_reps: usize,
3419 save: usize,
3420) -> Option<usize> {
3421 if !regex_match_group_repeated(text, ti, end_pos, alternatives, min_reps) {
3422 return None;
3423 }
3424 let final_end = regex_match_capturing(text, end_pos, after, 0, must_end, captures)?;
3425 captures.insert(save, (ti, end_pos));
3426 Some(final_end)
3427}
3428
3429fn regex_match_group_opt(
3431 text: &[u8],
3432 ti: usize,
3433 after: &[u8],
3434 must_end: bool,
3435 captures: &mut Vec<(usize, usize)>,
3436 alternatives: &[Vec<u8>],
3437) -> Option<usize> {
3438 let save = captures.len();
3439 if let Some(result) =
3441 regex_try_group_one_alt(text, ti, after, must_end, captures, alternatives, save)
3442 {
3443 return Some(result);
3444 }
3445 captures.truncate(save);
3447 if let Some(final_end) = regex_match_capturing(text, ti, after, 0, must_end, captures) {
3448 captures.insert(save, (ti, ti));
3449 return Some(final_end);
3450 }
3451 captures.truncate(save);
3452 None
3453}
3454
3455fn regex_try_group_one_alt(
3456 text: &[u8],
3457 ti: usize,
3458 after: &[u8],
3459 must_end: bool,
3460 captures: &mut Vec<(usize, usize)>,
3461 alternatives: &[Vec<u8>],
3462 save: usize,
3463) -> Option<usize> {
3464 for alt in alternatives {
3465 captures.truncate(save);
3466 if let Some(result) =
3467 regex_try_alt_then_continue(text, ti, alt, after, must_end, captures, save)
3468 {
3469 return Some(result);
3470 }
3471 captures.truncate(save);
3472 }
3473 None
3474}
3475
3476fn regex_try_alt_then_continue(
3477 text: &[u8],
3478 ti: usize,
3479 alt: &[u8],
3480 after: &[u8],
3481 must_end: bool,
3482 captures: &mut Vec<(usize, usize)>,
3483 save: usize,
3484) -> Option<usize> {
3485 let end = regex_try_match_at(text, ti, alt)?;
3486 let final_end = regex_match_capturing(text, end, after, 0, must_end, captures)?;
3487 captures.insert(save, (ti, end));
3488 Some(final_end)
3489}
3490
3491fn regex_match_group_exact(
3493 text: &[u8],
3494 ti: usize,
3495 rest: &[u8],
3496 must_end: bool,
3497 captures: &mut Vec<(usize, usize)>,
3498 alternatives: &[Vec<u8>],
3499) -> Option<usize> {
3500 regex_try_group_one_alt(
3501 text,
3502 ti,
3503 rest,
3504 must_end,
3505 captures,
3506 alternatives,
3507 captures.len(),
3508 )
3509}
3510
3511fn parse_quantifier(pat: &[u8], pos: usize) -> (u8, usize) {
3513 if pos < pat.len() {
3514 match pat[pos] {
3515 b'*' => (b'*', pos + 1),
3516 b'+' => (b'+', pos + 1),
3517 b'?' => (b'?', pos + 1),
3518 _ => (0, pos),
3519 }
3520 } else {
3521 (0, pos)
3522 }
3523}
3524
3525fn regex_match_elem(
3527 text: &[u8],
3528 ti: usize,
3529 pat: &[u8],
3530 pi: usize,
3531 must_end: bool,
3532 captures: &mut Vec<(usize, usize)>,
3533) -> Option<usize> {
3534 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3535 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3536
3537 match quant {
3538 b'*' | b'+' => regex_match_repeated_elem(
3539 text,
3540 ti,
3541 pat,
3542 after_quant,
3543 quant,
3544 must_end,
3545 captures,
3546 &matches_fn,
3547 ),
3548 b'?' => {
3549 regex_match_optional_elem(text, ti, pat, after_quant, must_end, captures, &matches_fn)
3550 }
3551 _ => regex_match_single_elem(text, ti, pat, elem_end, must_end, captures, &matches_fn),
3552 }
3553}
3554
3555fn count_regex_matches(text: &[u8], ti: usize, matches_fn: &dyn Fn(u8) -> bool) -> usize {
3556 let mut count = 0;
3557 while ti + count < text.len() && matches_fn(text[ti + count]) {
3558 count += 1;
3559 }
3560 count
3561}
3562
3563fn regex_match_repeated_elem(
3564 text: &[u8],
3565 ti: usize,
3566 pat: &[u8],
3567 after_quant: usize,
3568 quant: u8,
3569 must_end: bool,
3570 captures: &mut Vec<(usize, usize)>,
3571 matches_fn: &dyn Fn(u8) -> bool,
3572) -> Option<usize> {
3573 let min = usize::from(quant == b'+');
3574 let count = count_regex_matches(text, ti, matches_fn);
3575 for c in (min..=count).rev() {
3576 if let Some(end) = regex_match_capturing(text, ti + c, pat, after_quant, must_end, captures)
3577 {
3578 return Some(end);
3579 }
3580 }
3581 None
3582}
3583
3584fn regex_match_optional_elem(
3585 text: &[u8],
3586 ti: usize,
3587 pat: &[u8],
3588 after_quant: usize,
3589 must_end: bool,
3590 captures: &mut Vec<(usize, usize)>,
3591 matches_fn: &dyn Fn(u8) -> bool,
3592) -> Option<usize> {
3593 if ti < text.len() && matches_fn(text[ti]) {
3594 if let Some(end) = regex_match_capturing(text, ti + 1, pat, after_quant, must_end, captures)
3595 {
3596 return Some(end);
3597 }
3598 }
3599 regex_match_capturing(text, ti, pat, after_quant, must_end, captures)
3600}
3601
3602fn regex_match_single_elem(
3603 text: &[u8],
3604 ti: usize,
3605 pat: &[u8],
3606 elem_end: usize,
3607 must_end: bool,
3608 captures: &mut Vec<(usize, usize)>,
3609 matches_fn: &dyn Fn(u8) -> bool,
3610) -> Option<usize> {
3611 if ti < text.len() && matches_fn(text[ti]) {
3612 regex_match_capturing(text, ti + 1, pat, elem_end, must_end, captures)
3613 } else {
3614 None
3615 }
3616}
3617
3618fn regex_try_match_at(text: &[u8], start: usize, pattern: &[u8]) -> Option<usize> {
3620 regex_try_match_inner(text, start, pattern, 0)
3621}
3622
3623fn regex_try_match_inner(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3625 if pi >= pat.len() {
3626 return Some(ti);
3627 }
3628 if pat[pi] == b'(' {
3629 return regex_try_match_group(text, ti, pat, pi);
3630 }
3631 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3632 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3633 regex_try_apply_quant(text, ti, pat, elem_end, after_quant, quant, &matches_fn)
3634}
3635
3636fn regex_try_match_group(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3638 let close = find_matching_paren_bytes(pat, pi + 1)?;
3639 let inner = &pat[pi + 1..close];
3640 let rest = &pat[close + 1..];
3641 let alternatives = split_alternatives_bytes(inner);
3642 for alt in &alternatives {
3643 if let Some(end) = regex_try_alt_and_rest(text, ti, alt, rest) {
3644 return Some(end);
3645 }
3646 }
3647 None
3648}
3649
3650fn regex_try_alt_and_rest(text: &[u8], ti: usize, alt: &[u8], rest: &[u8]) -> Option<usize> {
3651 let after_alt = regex_try_match_inner(text, ti, alt, 0)?;
3652 regex_try_match_inner(text, after_alt, rest, 0)
3653}
3654
3655fn regex_try_apply_quant(
3657 text: &[u8],
3658 ti: usize,
3659 pat: &[u8],
3660 elem_end: usize,
3661 after_quant: usize,
3662 quant: u8,
3663 matches_fn: &dyn Fn(u8) -> bool,
3664) -> Option<usize> {
3665 match quant {
3666 b'*' | b'+' => regex_try_match_repeated_elem(text, ti, pat, after_quant, quant, matches_fn),
3667 b'?' => regex_try_match_optional_elem(text, ti, pat, after_quant, matches_fn),
3668 _ => regex_try_match_single_elem(text, ti, pat, elem_end, matches_fn),
3669 }
3670}
3671
3672fn regex_try_match_repeated_elem(
3673 text: &[u8],
3674 ti: usize,
3675 pat: &[u8],
3676 after_quant: usize,
3677 quant: u8,
3678 matches_fn: &dyn Fn(u8) -> bool,
3679) -> Option<usize> {
3680 let min = usize::from(quant == b'+');
3681 let count = count_regex_matches(text, ti, matches_fn);
3682 for c in (min..=count).rev() {
3683 if let Some(end) = regex_try_match_inner(text, ti + c, pat, after_quant) {
3684 return Some(end);
3685 }
3686 }
3687 None
3688}
3689
3690fn regex_try_match_optional_elem(
3691 text: &[u8],
3692 ti: usize,
3693 pat: &[u8],
3694 after_quant: usize,
3695 matches_fn: &dyn Fn(u8) -> bool,
3696) -> Option<usize> {
3697 if ti < text.len() && matches_fn(text[ti]) {
3698 if let Some(end) = regex_try_match_inner(text, ti + 1, pat, after_quant) {
3699 return Some(end);
3700 }
3701 }
3702 regex_try_match_inner(text, ti, pat, after_quant)
3703}
3704
3705fn regex_try_match_single_elem(
3706 text: &[u8],
3707 ti: usize,
3708 pat: &[u8],
3709 elem_end: usize,
3710 matches_fn: &dyn Fn(u8) -> bool,
3711) -> Option<usize> {
3712 if ti < text.len() && matches_fn(text[ti]) {
3713 regex_try_match_inner(text, ti + 1, pat, elem_end)
3714 } else {
3715 None
3716 }
3717}
3718
3719fn regex_match_group_repeated(
3721 text: &[u8],
3722 start: usize,
3723 end: usize,
3724 alternatives: &[Vec<u8>],
3725 min_reps: usize,
3726) -> bool {
3727 if start == end {
3728 return min_reps == 0;
3729 }
3730 if start > end {
3731 return false;
3732 }
3733 for alt in alternatives {
3734 if regex_group_repetition_matches(text, start, end, alternatives, min_reps, alt) {
3735 return true;
3736 }
3737 }
3738 false
3739}
3740
3741fn regex_group_repetition_matches(
3742 text: &[u8],
3743 start: usize,
3744 end: usize,
3745 alternatives: &[Vec<u8>],
3746 min_reps: usize,
3747 alt: &[u8],
3748) -> bool {
3749 let Some(after) = regex_try_match_inner(text, start, alt, 0) else {
3750 return false;
3751 };
3752 if after <= start || after > end {
3753 return false;
3754 }
3755 if after == end && min_reps <= 1 {
3756 return true;
3757 }
3758 regex_match_group_repeated(text, after, end, alternatives, min_reps.saturating_sub(1))
3759}
3760
3761fn find_matching_paren_bytes(pat: &[u8], start: usize) -> Option<usize> {
3763 let mut depth = 1;
3764 let mut i = start;
3765 while i < pat.len() {
3766 if pat[i] == b'\\' {
3767 i += 2;
3768 continue;
3769 }
3770 if pat[i] == b'(' {
3771 depth += 1;
3772 } else if pat[i] == b')' {
3773 depth -= 1;
3774 if depth == 0 {
3775 return Some(i);
3776 }
3777 }
3778 i += 1;
3779 }
3780 None
3781}
3782
3783fn split_alternatives_bytes(pat: &[u8]) -> Vec<Vec<u8>> {
3785 let mut alternatives = Vec::new();
3786 let mut current = Vec::new();
3787 let mut depth = 0i32;
3788 let mut i = 0;
3789 while i < pat.len() {
3790 if pat[i] == b'\\' && i + 1 < pat.len() {
3791 current.push(pat[i]);
3792 current.push(pat[i + 1]);
3793 i += 2;
3794 continue;
3795 }
3796 split_alt_classify_byte(pat[i], &mut depth, &mut current, &mut alternatives);
3797 i += 1;
3798 }
3799 alternatives.push(current);
3800 alternatives
3801}
3802
3803fn split_alt_classify_byte(
3804 byte: u8,
3805 depth: &mut i32,
3806 current: &mut Vec<u8>,
3807 alternatives: &mut Vec<Vec<u8>>,
3808) {
3809 match byte {
3810 b'(' => {
3811 *depth += 1;
3812 current.push(byte);
3813 }
3814 b')' => {
3815 *depth -= 1;
3816 current.push(byte);
3817 }
3818 b'|' if *depth == 0 => {
3819 alternatives.push(std::mem::take(current));
3820 }
3821 _ => {
3822 current.push(byte);
3823 }
3824 }
3825}
3826
3827#[allow(dead_code)]
3832fn simple_regex_match(text: &str, pattern: &str) -> bool {
3833 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3834
3835 if has_regex_metachar(core) {
3836 return regex_like_match(text, pattern);
3837 }
3838
3839 literal_match_range(text, core, anchored_start, anchored_end).is_some()
3841}
3842
3843#[allow(dead_code)]
3848fn regex_like_match(text: &str, pattern: &str) -> bool {
3849 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3850
3851 if anchored_start {
3852 regex_match_at(text, 0, core, anchored_end)
3853 } else {
3854 (0..=text.len()).any(|start| regex_match_at(text, start, core, anchored_end))
3855 }
3856}
3857
3858#[allow(dead_code)]
3861fn regex_match_at(text: &str, start: usize, core: &str, must_end: bool) -> bool {
3862 let text_bytes = text.as_bytes();
3863 let core_bytes = core.as_bytes();
3864 regex_backtrack(text_bytes, start, core_bytes, 0, must_end)
3865}
3866
3867#[allow(dead_code)]
3869fn regex_backtrack(text: &[u8], ti: usize, pat: &[u8], pi: usize, must_end: bool) -> bool {
3870 if pi >= pat.len() {
3871 return if must_end { ti >= text.len() } else { true };
3872 }
3873
3874 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3875 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3876
3877 match quant {
3878 b'*' => regex_backtrack_star(text, ti, pat, after_quant, must_end, &matches_fn),
3879 b'+' => regex_backtrack_plus(text, ti, pat, after_quant, must_end, &matches_fn),
3880 b'?' => regex_backtrack_optional(text, ti, pat, after_quant, must_end, &matches_fn),
3881 _ => regex_backtrack_single(text, ti, pat, elem_end, must_end, &matches_fn),
3882 }
3883}
3884
3885fn regex_backtrack_star(
3886 text: &[u8],
3887 ti: usize,
3888 pat: &[u8],
3889 after_quant: usize,
3890 must_end: bool,
3891 matches_fn: &dyn Fn(u8) -> bool,
3892) -> bool {
3893 let mut count = 0;
3894 loop {
3895 if regex_backtrack(text, ti + count, pat, after_quant, must_end) {
3896 return true;
3897 }
3898 if ti + count < text.len() && matches_fn(text[ti + count]) {
3899 count += 1;
3900 } else {
3901 return false;
3902 }
3903 }
3904}
3905
3906fn regex_backtrack_plus(
3907 text: &[u8],
3908 ti: usize,
3909 pat: &[u8],
3910 after_quant: usize,
3911 must_end: bool,
3912 matches_fn: &dyn Fn(u8) -> bool,
3913) -> bool {
3914 let count = count_regex_matches(text, ti, matches_fn);
3915 (1..=count).any(|matched| regex_backtrack(text, ti + matched, pat, after_quant, must_end))
3916}
3917
3918fn regex_backtrack_optional(
3919 text: &[u8],
3920 ti: usize,
3921 pat: &[u8],
3922 after_quant: usize,
3923 must_end: bool,
3924 matches_fn: &dyn Fn(u8) -> bool,
3925) -> bool {
3926 regex_backtrack(text, ti, pat, after_quant, must_end)
3927 || (ti < text.len()
3928 && matches_fn(text[ti])
3929 && regex_backtrack(text, ti + 1, pat, after_quant, must_end))
3930}
3931
3932fn regex_backtrack_single(
3933 text: &[u8],
3934 ti: usize,
3935 pat: &[u8],
3936 elem_end: usize,
3937 must_end: bool,
3938 matches_fn: &dyn Fn(u8) -> bool,
3939) -> bool {
3940 ti < text.len()
3941 && matches_fn(text[ti])
3942 && regex_backtrack(text, ti + 1, pat, elem_end, must_end)
3943}
3944
3945fn parse_regex_elem(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3948 match pat[pi] {
3949 b'.' => (pi + 1, Box::new(|_: u8| true)),
3950 b'[' => parse_regex_char_class(pat, pi),
3951 b'\\' if pi + 1 < pat.len() => {
3952 let escaped = pat[pi + 1];
3953 (pi + 2, Box::new(move |c: u8| c == escaped))
3954 }
3955 ch => (pi + 1, Box::new(move |c: u8| c == ch)),
3956 }
3957}
3958
3959fn parse_regex_char_class(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3960 let mut i = pi + 1;
3961 let negate = i < pat.len() && (pat[i] == b'^' || pat[i] == b'!');
3962 if negate {
3963 i += 1;
3964 }
3965 let mut chars = Vec::new();
3966 while i < pat.len() && pat[i] != b']' {
3967 if i + 2 < pat.len() && pat[i + 1] == b'-' {
3968 chars.extend(pat[i]..=pat[i + 2]);
3969 i += 3;
3970 } else {
3971 chars.push(pat[i]);
3972 i += 1;
3973 }
3974 }
3975 let end = if i < pat.len() { i + 1 } else { i };
3976 (
3977 end,
3978 Box::new(move |c: u8| regex_char_class_matches(&chars, negate, c)),
3979 )
3980}
3981
3982fn regex_char_class_matches(chars: &[u8], negate: bool, c: u8) -> bool {
3983 let found = chars.contains(&c);
3984 if negate {
3985 !found
3986 } else {
3987 found
3988 }
3989}
3990
3991fn glob_match_char_class(pattern: &[u8], mut pi: usize, ch: u8) -> (usize, bool) {
3994 let negate = pi < pattern.len() && (pattern[pi] == b'!' || pattern[pi] == b'^');
3995 if negate {
3996 pi += 1;
3997 }
3998 let mut matched = false;
3999 let mut first = true;
4000 while pi < pattern.len() && (first || pattern[pi] != b']') {
4001 first = false;
4002 let (next_pi, item_matched) = glob_match_char_class_item(pattern, pi, ch);
4003 matched |= item_matched;
4004 pi = next_pi;
4005 }
4006 if pi < pattern.len() && pattern[pi] == b']' {
4007 pi += 1;
4008 }
4009 (pi, matched != negate)
4010}
4011
4012fn glob_match_char_class_item(pattern: &[u8], pi: usize, ch: u8) -> (usize, bool) {
4013 if pi + 2 < pattern.len() && pattern[pi + 1] == b'-' {
4014 let lo = pattern[pi];
4015 let hi = pattern[pi + 2];
4016 return (pi + 3, ch >= lo && ch <= hi);
4017 }
4018 (pi + 1, pattern[pi] == ch)
4019}
4020
4021enum GlobPatternStep {
4022 Consume(usize),
4023 Star,
4024 Class(usize, bool),
4025 Mismatch,
4026}
4027
4028fn glob_step(pattern: &[u8], pi: usize, ch: u8) -> GlobPatternStep {
4029 if pi >= pattern.len() {
4030 return GlobPatternStep::Mismatch;
4031 }
4032
4033 match pattern[pi] {
4034 b'?' => GlobPatternStep::Consume(pi + 1),
4035 b'*' => GlobPatternStep::Star,
4036 b'[' => {
4037 let (new_pi, matched) = glob_match_char_class(pattern, pi + 1, ch);
4038 GlobPatternStep::Class(new_pi, matched)
4039 }
4040 literal if literal == ch => GlobPatternStep::Consume(pi + 1),
4041 _ => GlobPatternStep::Mismatch,
4042 }
4043}
4044
4045fn glob_backtrack(pi: &mut usize, ni: &mut usize, star_pi: usize, star_ni: &mut usize) -> bool {
4046 if star_pi == usize::MAX {
4047 return false;
4048 }
4049
4050 *pi = star_pi + 1;
4051 *star_ni += 1;
4052 *ni = *star_ni;
4053 true
4054}
4055
4056fn glob_match_inner(pattern: &[u8], name: &[u8]) -> bool {
4060 let mut pi = 0;
4061 let mut ni = 0;
4062 let mut star_pi = usize::MAX;
4063 let mut star_ni = usize::MAX;
4064
4065 while ni < name.len() {
4066 match glob_step(pattern, pi, name[ni]) {
4067 GlobPatternStep::Star => {
4068 star_pi = pi;
4069 star_ni = ni;
4070 pi += 1;
4071 }
4072 GlobPatternStep::Consume(new_pi) | GlobPatternStep::Class(new_pi, true) => {
4073 pi = new_pi;
4074 ni += 1;
4075 }
4076 GlobPatternStep::Class(_, false) | GlobPatternStep::Mismatch => {
4077 if !glob_backtrack(&mut pi, &mut ni, star_pi, &mut star_ni) {
4078 return false;
4079 }
4080 }
4081 }
4082 }
4083
4084 while pi < pattern.len() && pattern[pi] == b'*' {
4086 pi += 1;
4087 }
4088
4089 pi == pattern.len()
4090}
4091
4092fn glob_match_ext(pattern: &str, name: &str, dotglob: bool, extglob: bool) -> bool {
4094 if name.starts_with('.') && !pattern.starts_with('.') && !dotglob {
4096 return false;
4097 }
4098 if extglob && has_extglob_pattern(pattern) {
4099 return extglob_match(pattern, name);
4100 }
4101 glob_match_inner(pattern.as_bytes(), name.as_bytes())
4102}
4103
4104fn has_extglob_pattern(pattern: &str) -> bool {
4106 let bytes = pattern.as_bytes();
4107 for i in 0..bytes.len().saturating_sub(1) {
4108 if bytes[i + 1] == b'(' && matches!(bytes[i], b'?' | b'*' | b'+' | b'@' | b'!') {
4109 return true;
4110 }
4111 }
4112 false
4113}
4114
4115pub fn extglob_match(pattern: &str, name: &str) -> bool {
4120 extglob_match_recursive(pattern.as_bytes(), name.as_bytes())
4121}
4122
4123fn extglob_match_recursive(pattern: &[u8], name: &[u8]) -> bool {
4124 let Some((pi, op, close)) = find_extglob_operator(pattern) else {
4126 return glob_match_inner(pattern, name);
4127 };
4128
4129 let open = pi + 2;
4130 let alternatives = split_alternatives(&pattern[open..close]);
4131 let prefix = &pattern[..pi];
4132 let suffix = &pattern[close + 1..];
4133
4134 match op {
4135 b'@' | b'?' => extglob_match_at_or_opt(op, prefix, &alternatives, suffix, name),
4136 b'*' => extglob_star(prefix, &alternatives, suffix, name, 0),
4137 b'+' => extglob_plus(prefix, &alternatives, suffix, name, 0),
4138 b'!' => extglob_match_negate(prefix, &alternatives, suffix, name),
4139 _ => unreachable!(),
4140 }
4141}
4142
4143fn find_extglob_operator(pattern: &[u8]) -> Option<(usize, u8, usize)> {
4145 let mut pi = 0;
4146 while pi < pattern.len() {
4147 if pi + 1 < pattern.len()
4148 && pattern[pi + 1] == b'('
4149 && matches!(pattern[pi], b'?' | b'*' | b'+' | b'@' | b'!')
4150 {
4151 if let Some(close) = find_matching_paren(pattern, pi + 2) {
4152 return Some((pi, pattern[pi], close));
4153 }
4154 }
4155 pi += 1;
4156 }
4157 None
4158}
4159
4160fn build_combined(prefix: &[u8], mid: &[u8], suffix: &[u8]) -> Vec<u8> {
4162 let mut combined = Vec::with_capacity(prefix.len() + mid.len() + suffix.len());
4163 combined.extend_from_slice(prefix);
4164 combined.extend_from_slice(mid);
4165 combined.extend_from_slice(suffix);
4166 combined
4167}
4168
4169fn extglob_match_at_or_opt(
4171 op: u8,
4172 prefix: &[u8],
4173 alternatives: &[Vec<u8>],
4174 suffix: &[u8],
4175 name: &[u8],
4176) -> bool {
4177 if op == b'?' && extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4179 return true;
4180 }
4181 for alt in alternatives {
4183 if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4184 return true;
4185 }
4186 }
4187 false
4188}
4189
4190fn extglob_match_negate(
4192 prefix: &[u8],
4193 alternatives: &[Vec<u8>],
4194 suffix: &[u8],
4195 name: &[u8],
4196) -> bool {
4197 for alt in alternatives {
4198 if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4199 return false;
4200 }
4201 }
4202 let wildcard = build_combined(prefix, b"*", suffix);
4203 glob_match_inner(&wildcard, name)
4204}
4205
4206fn extglob_star(
4208 prefix: &[u8],
4209 alternatives: &[Vec<u8>],
4210 suffix: &[u8],
4211 name: &[u8],
4212 depth: u32,
4213) -> bool {
4214 if depth > 20 {
4215 return false;
4216 }
4217 if extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4219 return true;
4220 }
4221 extglob_try_extend(prefix, alternatives, suffix, name, depth)
4223}
4224
4225fn extglob_try_extend(
4226 prefix: &[u8],
4227 alternatives: &[Vec<u8>],
4228 suffix: &[u8],
4229 name: &[u8],
4230 depth: u32,
4231) -> bool {
4232 let prefix_len = prefix.len();
4233 for alt in alternatives {
4234 let new_prefix = build_combined(prefix, alt, &[]);
4235 if new_prefix.len() > prefix_len
4236 && extglob_star(&new_prefix, alternatives, suffix, name, depth + 1)
4237 {
4238 return true;
4239 }
4240 }
4241 false
4242}
4243
4244fn extglob_plus(
4246 prefix: &[u8],
4247 alternatives: &[Vec<u8>],
4248 suffix: &[u8],
4249 name: &[u8],
4250 depth: u32,
4251) -> bool {
4252 if depth > 20 {
4253 return false;
4254 }
4255 for alt in alternatives {
4256 let new_prefix = build_combined(prefix, alt, &[]);
4257 if extglob_star(&new_prefix, alternatives, suffix, name, depth + 1) {
4258 return true;
4259 }
4260 }
4261 false
4262}
4263
4264fn find_matching_paren(pattern: &[u8], open: usize) -> Option<usize> {
4266 let mut depth: u32 = 1;
4267 let mut i = open;
4268 while i < pattern.len() {
4269 if pattern[i] == b'(' {
4270 depth += 1;
4271 } else if pattern[i] == b')' {
4272 depth -= 1;
4273 if depth == 0 {
4274 return Some(i);
4275 }
4276 }
4277 i += 1;
4278 }
4279 None
4280}
4281
4282fn split_alternatives(pat: &[u8]) -> Vec<Vec<u8>> {
4284 let mut result = Vec::new();
4285 let mut current = Vec::new();
4286 let mut depth: u32 = 0;
4287 for &b in pat {
4288 if b == b'(' {
4289 depth += 1;
4290 current.push(b);
4291 } else if b == b')' {
4292 depth -= 1;
4293 current.push(b);
4294 } else if b == b'|' && depth == 0 {
4295 result.push(std::mem::take(&mut current));
4296 } else {
4297 current.push(b);
4298 }
4299 }
4300 result.push(current);
4301 result
4302}
4303
4304impl Default for WorkerRuntime {
4305 fn default() -> Self {
4306 Self::new()
4307 }
4308}