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(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
2515 }
2516 }
2517 result.truncate(Self::MAX_GLOB_RESULTS);
2518 result
2519 }
2520
2521 fn expand_globs(&mut self, argv: Vec<String>) -> Vec<String> {
2522 if self.vm.state.get_var("SHOPT_f").as_deref() == Some("1") {
2523 return argv;
2524 }
2525 let nullglob = self.get_shopt_value("nullglob");
2526 let dotglob = self.get_shopt_value("dotglob");
2527 let globstar = self.get_shopt_value("globstar");
2528 let extglob = self.get_shopt_value("extglob");
2529
2530 let mut result = Vec::new();
2531 for arg in argv {
2532 result.extend(self.expand_glob_arg(arg, nullglob, dotglob, globstar, extglob));
2533 }
2534 result.truncate(Self::MAX_GLOB_RESULTS);
2535 result
2536 }
2537
2538 #[allow(clippy::fn_params_excessive_bools)]
2539 fn expand_glob_arg(
2540 &self,
2541 arg: String,
2542 nullglob: bool,
2543 dotglob: bool,
2544 globstar: bool,
2545 extglob: bool,
2546 ) -> Vec<String> {
2547 if !Self::is_glob_pattern(&arg, extglob) {
2548 return vec![arg];
2549 }
2550 if globstar && arg.contains("**") {
2551 return self.expand_globstar_arg(arg, nullglob, dotglob, extglob);
2552 }
2553 self.expand_standard_glob_arg(arg, nullglob, dotglob, extglob)
2554 }
2555
2556 fn is_glob_pattern(arg: &str, extglob: bool) -> bool {
2557 let has_bracket_class = arg.contains('[') && arg.contains(']');
2558 arg.contains('*')
2559 || arg.contains('?')
2560 || has_bracket_class
2561 || (extglob && has_extglob_pattern(arg))
2562 }
2563
2564 fn expand_globstar_arg(
2565 &self,
2566 arg: String,
2567 nullglob: bool,
2568 dotglob: bool,
2569 extglob: bool,
2570 ) -> Vec<String> {
2571 let mut matches = self.expand_globstar(&arg, dotglob, extglob);
2572 matches.sort();
2573 self.finalize_glob_matches(arg, matches, nullglob)
2574 }
2575
2576 fn expand_standard_glob_arg(
2577 &self,
2578 arg: String,
2579 nullglob: bool,
2580 dotglob: bool,
2581 extglob: bool,
2582 ) -> Vec<String> {
2583 let Some((dir, pattern, prefix)) = self.split_glob_search(&arg) else {
2584 return self.finalize_glob_matches(arg.clone(), Vec::new(), nullglob);
2585 };
2586 let matches = self.read_glob_matches(&dir, &pattern, prefix.as_deref(), dotglob, extglob);
2587 self.finalize_glob_matches(arg, matches, nullglob)
2588 }
2589
2590 fn split_glob_search(&self, arg: &str) -> Option<(String, String, Option<String>)> {
2591 let Some(slash_pos) = arg.rfind('/') else {
2592 return Some((self.vm.state.cwd.clone(), arg.to_string(), None));
2593 };
2594
2595 let dir_part = &arg[..=slash_pos];
2596 if Self::path_segment_has_glob(dir_part) {
2597 return None;
2598 }
2599
2600 Some((
2601 self.resolve_cwd_path(dir_part),
2602 arg[slash_pos + 1..].to_string(),
2603 Some(dir_part.to_string()),
2604 ))
2605 }
2606
2607 fn path_segment_has_glob(path: &str) -> bool {
2608 path.contains('*') || path.contains('?') || path.contains('[')
2609 }
2610
2611 fn read_glob_matches(
2612 &self,
2613 dir: &str,
2614 pattern: &str,
2615 prefix: Option<&str>,
2616 dotglob: bool,
2617 extglob: bool,
2618 ) -> Vec<String> {
2619 let Ok(entries) = self.fs.read_dir(dir) else {
2620 return Vec::new();
2621 };
2622
2623 let mut matches: Vec<String> = entries
2624 .iter()
2625 .filter(|e| glob_match_ext(pattern, &e.name, dotglob, extglob))
2626 .map(|e| match prefix {
2627 Some(prefix) => format!("{prefix}{}", e.name),
2628 None => e.name.clone(),
2629 })
2630 .collect();
2631 matches.sort();
2632 matches
2633 }
2634
2635 #[allow(clippy::unused_self)]
2636 fn finalize_glob_matches(
2637 &self,
2638 arg: String,
2639 matches: Vec<String>,
2640 nullglob: bool,
2641 ) -> Vec<String> {
2642 if !matches.is_empty() {
2643 return matches;
2644 }
2645 if nullglob {
2646 Vec::new()
2647 } else {
2648 vec![arg]
2649 }
2650 }
2651
2652 fn expand_globstar(&self, pattern: &str, dotglob: bool, extglob: bool) -> Vec<String> {
2654 let segments: Vec<&str> = pattern.split('/').collect();
2656 let base_dir = self.vm.state.cwd.clone();
2657 let mut matches = Vec::new();
2658 self.globstar_walk(&base_dir, &segments, 0, "", dotglob, extglob, &mut matches);
2659 matches
2660 }
2661
2662 fn globstar_walk(
2664 &self,
2665 dir: &str,
2666 segments: &[&str],
2667 seg_idx: usize,
2668 prefix: &str,
2669 dotglob: bool,
2670 extglob: bool,
2671 matches: &mut Vec<String>,
2672 ) {
2673 if seg_idx >= segments.len() {
2674 return;
2675 }
2676
2677 let seg = segments[seg_idx];
2678 if seg == "**" {
2679 self.globstar_walk_wildcard(dir, segments, seg_idx, prefix, dotglob, extglob, matches);
2680 return;
2681 }
2682 self.globstar_walk_segment(
2683 dir, seg, segments, seg_idx, prefix, dotglob, extglob, matches,
2684 );
2685 }
2686
2687 fn globstar_walk_wildcard(
2688 &self,
2689 dir: &str,
2690 segments: &[&str],
2691 seg_idx: usize,
2692 prefix: &str,
2693 dotglob: bool,
2694 extglob: bool,
2695 matches: &mut Vec<String>,
2696 ) {
2697 if seg_idx + 1 < segments.len() {
2698 self.globstar_walk(
2699 dir,
2700 segments,
2701 seg_idx + 1,
2702 prefix,
2703 dotglob,
2704 extglob,
2705 matches,
2706 );
2707 }
2708
2709 let Ok(entries) = self.fs.read_dir(dir) else {
2710 return;
2711 };
2712 for entry in &entries {
2713 if !dotglob && entry.name.starts_with('.') {
2714 continue;
2715 }
2716 let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, &entry.name);
2717 if self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false) {
2718 self.globstar_walk(
2719 &child_path,
2720 segments,
2721 seg_idx,
2722 &child_prefix,
2723 dotglob,
2724 extglob,
2725 matches,
2726 );
2727 }
2728 }
2729 }
2730
2731 #[allow(clippy::too_many_arguments)]
2732 fn globstar_walk_segment(
2733 &self,
2734 dir: &str,
2735 seg: &str,
2736 segments: &[&str],
2737 seg_idx: usize,
2738 prefix: &str,
2739 dotglob: bool,
2740 extglob: bool,
2741 matches: &mut Vec<String>,
2742 ) {
2743 let Ok(entries) = self.fs.read_dir(dir) else {
2744 return;
2745 };
2746 let is_last = seg_idx == segments.len() - 1;
2747
2748 for entry in &entries {
2749 if !glob_match_ext(seg, &entry.name, dotglob, extglob) {
2750 continue;
2751 }
2752 self.globstar_handle_matched_entry(
2753 dir,
2754 segments,
2755 seg_idx,
2756 prefix,
2757 dotglob,
2758 extglob,
2759 matches,
2760 &entry.name,
2761 is_last,
2762 );
2763 }
2764 }
2765
2766 #[allow(clippy::too_many_arguments)]
2767 fn globstar_handle_matched_entry(
2768 &self,
2769 dir: &str,
2770 segments: &[&str],
2771 seg_idx: usize,
2772 prefix: &str,
2773 dotglob: bool,
2774 extglob: bool,
2775 matches: &mut Vec<String>,
2776 name: &str,
2777 is_last: bool,
2778 ) {
2779 let (child_path, child_prefix) = Self::globstar_child_paths(dir, prefix, name);
2780 if is_last {
2781 matches.push(child_prefix);
2782 return;
2783 }
2784 let is_dir = self.fs.stat(&child_path).map(|m| m.is_dir).unwrap_or(false);
2785 if is_dir {
2786 self.globstar_walk(
2787 &child_path,
2788 segments,
2789 seg_idx + 1,
2790 &child_prefix,
2791 dotglob,
2792 extglob,
2793 matches,
2794 );
2795 }
2796 }
2797
2798 fn globstar_child_paths(dir: &str, prefix: &str, name: &str) -> (String, String) {
2799 let child_path = if dir == "/" {
2800 format!("/{name}")
2801 } else {
2802 format!("{dir}/{name}")
2803 };
2804 let child_prefix = if prefix.is_empty() {
2805 name.to_string()
2806 } else {
2807 format!("{prefix}/{name}")
2808 };
2809 (child_path, child_prefix)
2810 }
2811
2812 fn write_to_file(&mut self, path: &str, target: &str, data: &[u8], opts: OpenOptions) {
2814 match self.fs.open(path, opts) {
2815 Ok(h) => {
2816 if let Err(e) = self.fs.write_file(h, data) {
2817 self.vm
2818 .stderr
2819 .extend_from_slice(format!("wasmsh: write error: {e}\n").as_bytes());
2820 }
2821 self.fs.close(h);
2822 }
2823 Err(e) => {
2824 self.vm
2825 .stderr
2826 .extend_from_slice(format!("wasmsh: {target}: {e}\n").as_bytes());
2827 }
2828 }
2829 }
2830
2831 fn capture_stdout(&mut self, from: usize) -> Vec<u8> {
2833 let data = self.vm.stdout[from..].to_vec();
2834 self.vm.stdout.truncate(from);
2835 data
2836 }
2837
2838 fn apply_redirections(&mut self, redirections: &[HirRedirection], stdout_before: usize) {
2842 for redir in redirections {
2843 let resolved = self.resolve_command_subst(std::slice::from_ref(&redir.target));
2845 let resolved_target = resolved.first().unwrap_or(&redir.target);
2846 let target = wasmsh_expand::expand_word(resolved_target, &mut self.vm.state);
2847 let path = self.resolve_cwd_path(&target);
2848 let fd = redir.fd.unwrap_or(1);
2849
2850 match redir.op {
2851 RedirectionOp::Output => {
2852 self.apply_output_redir(&path, &target, fd, stdout_before);
2853 }
2854 RedirectionOp::Append => {
2855 self.apply_append_redir(&path, &target, fd, stdout_before);
2856 }
2857 RedirectionOp::DupOutput => {
2858 let target_fd: u32 = target.parse().unwrap_or(1);
2859 let source_fd = redir.fd.unwrap_or(1);
2860 if source_fd == 2 && target_fd == 1 {
2861 let stderr_data = std::mem::take(&mut self.vm.stderr);
2862 self.vm.stdout.extend_from_slice(&stderr_data);
2863 } else if source_fd == 1 && target_fd == 2 {
2864 let stdout_data = self.capture_stdout(stdout_before);
2865 self.vm.stderr.extend_from_slice(&stdout_data);
2866 }
2867 }
2868 #[allow(unreachable_patterns)]
2869 _ => {}
2870 }
2871 }
2872 }
2873
2874 fn apply_output_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2876 let data = if fd == FD_BOTH {
2877 let mut combined = self.capture_stdout(stdout_before);
2878 combined.extend_from_slice(&std::mem::take(&mut self.vm.stderr));
2879 combined
2880 } else if fd == 2 {
2881 std::mem::take(&mut self.vm.stderr)
2882 } else {
2883 self.capture_stdout(stdout_before)
2884 };
2885 self.write_to_file(path, target, &data, OpenOptions::write());
2886 }
2887
2888 fn apply_append_redir(&mut self, path: &str, target: &str, fd: u32, stdout_before: usize) {
2890 let data = if fd == 2 {
2891 std::mem::take(&mut self.vm.stderr)
2892 } else {
2893 self.capture_stdout(stdout_before)
2894 };
2895 self.write_to_file(path, target, &data, OpenOptions::append());
2896 }
2897}
2898
2899fn convert_diag_level(level: DiagnosticLevel) -> wasmsh_vm::DiagLevel {
2901 match level {
2902 DiagnosticLevel::Trace => wasmsh_vm::DiagLevel::Trace,
2903 DiagnosticLevel::Warning => wasmsh_vm::DiagLevel::Warning,
2904 DiagnosticLevel::Error => wasmsh_vm::DiagLevel::Error,
2905 _ => wasmsh_vm::DiagLevel::Info,
2906 }
2907}
2908
2909fn dbl_bracket_eval_or(
2913 tokens: &[String],
2914 pos: &mut usize,
2915 fs: &BackendFs,
2916 state: &mut ShellState,
2917) -> bool {
2918 let mut result = dbl_bracket_eval_and(tokens, pos, fs, state);
2919 while *pos < tokens.len() && tokens[*pos] == "||" {
2920 *pos += 1;
2921 let rhs = dbl_bracket_eval_and(tokens, pos, fs, state);
2922 result = result || rhs;
2923 }
2924 result
2925}
2926
2927fn dbl_bracket_eval_and(
2929 tokens: &[String],
2930 pos: &mut usize,
2931 fs: &BackendFs,
2932 state: &mut ShellState,
2933) -> bool {
2934 let mut result = dbl_bracket_eval_not(tokens, pos, fs, state);
2935 while *pos < tokens.len() && tokens[*pos] == "&&" {
2936 *pos += 1;
2937 let rhs = dbl_bracket_eval_not(tokens, pos, fs, state);
2938 result = result && rhs;
2939 }
2940 result
2941}
2942
2943fn dbl_bracket_eval_not(
2945 tokens: &[String],
2946 pos: &mut usize,
2947 fs: &BackendFs,
2948 state: &mut ShellState,
2949) -> bool {
2950 if *pos < tokens.len() && tokens[*pos] == "!" {
2951 *pos += 1;
2952 return !dbl_bracket_eval_not(tokens, pos, fs, state);
2953 }
2954 dbl_bracket_eval_primary(tokens, pos, fs, state)
2955}
2956
2957fn dbl_bracket_eval_primary(
2959 tokens: &[String],
2960 pos: &mut usize,
2961 fs: &BackendFs,
2962 state: &mut ShellState,
2963) -> bool {
2964 if *pos >= tokens.len() {
2965 return false;
2966 }
2967 if let Some(result) = dbl_bracket_try_group(tokens, pos, fs, state) {
2968 return result;
2969 }
2970 if let Some(result) = dbl_bracket_try_unary(tokens, pos, fs) {
2971 return result;
2972 }
2973 if *pos + 1 == tokens.len() {
2974 return dbl_bracket_take_truthy_token(tokens, pos);
2975 }
2976 if let Some(result) = dbl_bracket_try_binary(tokens, pos, state) {
2977 return result;
2978 }
2979 dbl_bracket_take_truthy_token(tokens, pos)
2980}
2981
2982fn dbl_bracket_try_group(
2983 tokens: &[String],
2984 pos: &mut usize,
2985 fs: &BackendFs,
2986 state: &mut ShellState,
2987) -> Option<bool> {
2988 if tokens.get(*pos).map(String::as_str) != Some("(") {
2989 return None;
2990 }
2991
2992 *pos += 1;
2993 let result = dbl_bracket_eval_or(tokens, pos, fs, state);
2994 if tokens.get(*pos).map(String::as_str) == Some(")") {
2995 *pos += 1;
2996 }
2997 Some(result)
2998}
2999
3000fn dbl_bracket_take_truthy_token(tokens: &[String], pos: &mut usize) -> bool {
3001 let Some(token) = tokens.get(*pos) else {
3002 return false;
3003 };
3004 *pos += 1;
3005 !token.is_empty()
3006}
3007
3008fn dbl_bracket_try_unary(tokens: &[String], pos: &mut usize, fs: &BackendFs) -> Option<bool> {
3010 if *pos + 1 >= tokens.len() {
3011 return None;
3012 }
3013 let flag = dbl_bracket_parse_unary_flag(&tokens[*pos])?;
3014 match flag {
3015 b'z' | b'n' => Some(dbl_bracket_eval_string_test(tokens, pos, flag)),
3016 b'f' | b'd' | b'e' | b's' | b'r' | b'w' | b'x' => {
3017 dbl_bracket_eval_file_test(tokens, pos, flag, fs)
3018 }
3019 _ => None,
3020 }
3021}
3022
3023fn dbl_bracket_parse_unary_flag(op: &str) -> Option<u8> {
3024 if !op.starts_with('-') || op.len() != 2 {
3025 return None;
3026 }
3027 Some(op.as_bytes()[1])
3028}
3029
3030fn dbl_bracket_eval_string_test(tokens: &[String], pos: &mut usize, flag: u8) -> bool {
3031 *pos += 1;
3032 let arg = &tokens[*pos];
3033 *pos += 1;
3034 if flag == b'z' {
3035 arg.is_empty()
3036 } else {
3037 !arg.is_empty()
3038 }
3039}
3040
3041fn dbl_bracket_eval_file_test(
3042 tokens: &[String],
3043 pos: &mut usize,
3044 flag: u8,
3045 fs: &BackendFs,
3046) -> Option<bool> {
3047 if *pos + 2 < tokens.len() && is_binary_op(&tokens[*pos + 2]) {
3048 return None;
3049 }
3050 *pos += 1;
3051 let path_str = &tokens[*pos];
3052 *pos += 1;
3053 Some(eval_file_test(flag, path_str, fs))
3054}
3055
3056fn dbl_bracket_try_binary(
3058 tokens: &[String],
3059 pos: &mut usize,
3060 state: &mut ShellState,
3061) -> Option<bool> {
3062 if *pos + 2 > tokens.len() {
3063 return None;
3064 }
3065 let op_idx = *pos + 1;
3066 if op_idx >= tokens.len() || !is_binary_op(&tokens[op_idx]) {
3067 return None;
3068 }
3069 let lhs = tokens[*pos].clone();
3070 *pos += 1;
3071 let op = tokens[*pos].clone();
3072 *pos += 1;
3073
3074 let rhs = dbl_bracket_collect_rhs(tokens, pos, &op);
3075 Some(eval_binary_op(&lhs, &op, &rhs, state))
3076}
3077
3078fn dbl_bracket_collect_rhs(tokens: &[String], pos: &mut usize, op: &str) -> String {
3081 if *pos >= tokens.len() {
3082 return String::new();
3083 }
3084 if op == "=~" {
3085 return dbl_bracket_collect_regex_rhs(tokens, pos);
3086 }
3087 let rhs = tokens[*pos].clone();
3088 *pos += 1;
3089 rhs
3090}
3091
3092fn dbl_bracket_collect_regex_rhs(tokens: &[String], pos: &mut usize) -> String {
3093 let mut rhs = String::new();
3094 while *pos < tokens.len() && tokens[*pos] != "&&" && tokens[*pos] != "||" {
3095 rhs.push_str(&tokens[*pos]);
3096 *pos += 1;
3097 }
3098 rhs
3099}
3100
3101fn is_binary_op(s: &str) -> bool {
3103 matches!(
3104 s,
3105 "==" | "!=" | "=~" | "=" | "<" | ">" | "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge"
3106 )
3107}
3108
3109fn eval_binary_op(lhs: &str, op: &str, rhs: &str, state: &mut ShellState) -> bool {
3111 match op {
3112 "==" | "=" => glob_cmp(lhs, rhs, state, false),
3113 "!=" => !glob_cmp(lhs, rhs, state, false),
3114 "=~" => eval_regex_match(lhs, rhs, state),
3115 "<" => lhs < rhs,
3116 ">" => lhs > rhs,
3117 _ => eval_int_cmp(lhs, op, rhs),
3118 }
3119}
3120
3121fn glob_cmp(lhs: &str, rhs: &str, state: &ShellState, _negate: bool) -> bool {
3123 let nocasematch = state.get_var("SHOPT_nocasematch").as_deref() == Some("1");
3124 if nocasematch {
3125 glob_match_inner(rhs.to_lowercase().as_bytes(), lhs.to_lowercase().as_bytes())
3126 } else {
3127 glob_match_inner(rhs.as_bytes(), lhs.as_bytes())
3128 }
3129}
3130
3131fn eval_regex_match(lhs: &str, rhs: &str, state: &mut ShellState) -> bool {
3133 let captures = regex_match_with_captures(lhs, rhs);
3134 let br_name = smol_str::SmolStr::from("BASH_REMATCH");
3135 let Some(caps) = captures else {
3136 state.init_indexed_array(br_name);
3137 return false;
3138 };
3139 state.init_indexed_array(br_name.clone());
3140 for (i, cap) in caps.iter().enumerate() {
3141 state.set_array_element(
3142 br_name.clone(),
3143 &i.to_string(),
3144 smol_str::SmolStr::from(cap.as_str()),
3145 );
3146 }
3147 true
3148}
3149
3150fn eval_int_cmp(lhs: &str, op: &str, rhs: &str) -> bool {
3152 let a: i64 = lhs.trim().parse().unwrap_or(0);
3153 let b: i64 = rhs.trim().parse().unwrap_or(0);
3154 match op {
3155 "-eq" => a == b,
3156 "-ne" => a != b,
3157 "-lt" => a < b,
3158 "-le" => a <= b,
3159 "-gt" => a > b,
3160 "-ge" => a >= b,
3161 _ => false,
3162 }
3163}
3164
3165fn eval_file_test(flag: u8, path: &str, fs: &BackendFs) -> bool {
3167 use wasmsh_fs::Vfs;
3168 match fs.stat(path) {
3169 Ok(meta) => match flag {
3170 b'f' => !meta.is_dir,
3171 b'd' => meta.is_dir,
3172 b's' => meta.size > 0,
3173 b'e' | b'r' | b'w' | b'x' => true,
3175 _ => false,
3176 },
3177 Err(_) => false,
3178 }
3179}
3180
3181fn regex_strip_anchors(pattern: &str) -> (&str, bool, bool) {
3183 let anchored_start = pattern.starts_with('^');
3184 let anchored_end = pattern.ends_with('$') && !pattern.ends_with("\\$");
3185 let core = match (anchored_start, anchored_end) {
3186 (true, true) if pattern.len() >= 2 => &pattern[1..pattern.len() - 1],
3187 (true, _) => &pattern[1..],
3188 (_, true) => &pattern[..pattern.len() - 1],
3189 _ => pattern,
3190 };
3191 (core, anchored_start, anchored_end)
3192}
3193
3194fn has_regex_metachar(core: &str) -> bool {
3196 core.contains('.')
3197 || core.contains('+')
3198 || core.contains('*')
3199 || core.contains('?')
3200 || core.contains('[')
3201 || core.contains('(')
3202 || core.contains('|')
3203}
3204
3205fn literal_match_range(text: &str, core: &str, start: bool, end: bool) -> Option<(usize, usize)> {
3207 match (start, end) {
3208 (true, true) if text == core => Some((0, text.len())),
3209 (true, false) if text.starts_with(core) => Some((0, core.len())),
3210 (false, true) if text.ends_with(core) => Some((text.len() - core.len(), text.len())),
3211 (false, false) => text.find(core).map(|pos| (pos, pos + core.len())),
3212 _ => None,
3213 }
3214}
3215
3216fn regex_match_with_captures(text: &str, pattern: &str) -> Option<Vec<String>> {
3222 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3223
3224 if !has_regex_metachar(core) {
3225 return regex_match_literal_with_captures(text, core, anchored_start, anchored_end);
3226 }
3227
3228 regex_find_first_match(text, core, anchored_start, anchored_end)
3229}
3230
3231fn regex_find_first_match(
3232 text: &str,
3233 core: &str,
3234 anchored_start: bool,
3235 anchored_end: bool,
3236) -> Option<Vec<String>> {
3237 let end = if anchored_start { 0 } else { text.len() };
3238 for start in 0..=end {
3239 if let Some(result) = regex_match_from_start(text, core, anchored_end, start) {
3240 return Some(result);
3241 }
3242 }
3243 None
3244}
3245
3246fn regex_match_literal_with_captures(
3247 text: &str,
3248 core: &str,
3249 anchored_start: bool,
3250 anchored_end: bool,
3251) -> Option<Vec<String>> {
3252 literal_match_range(text, core, anchored_start, anchored_end)
3253 .map(|(s, e)| vec![text[s..e].to_string()])
3254}
3255
3256fn regex_match_from_start(
3257 text: &str,
3258 core: &str,
3259 anchored_end: bool,
3260 start: usize,
3261) -> Option<Vec<String>> {
3262 let mut group_caps: Vec<(usize, usize)> = Vec::new();
3263 let end = regex_match_capturing(
3264 text.as_bytes(),
3265 start,
3266 core.as_bytes(),
3267 0,
3268 anchored_end,
3269 &mut group_caps,
3270 )?;
3271 Some(regex_build_capture_list(text, start, end, &group_caps))
3272}
3273
3274fn regex_build_capture_list(
3275 text: &str,
3276 start: usize,
3277 end: usize,
3278 group_caps: &[(usize, usize)],
3279) -> Vec<String> {
3280 let mut result = vec![text[start..end].to_string()];
3281 for &(gs, ge) in group_caps {
3282 result.push(text[gs..ge].to_string());
3283 }
3284 result
3285}
3286
3287fn regex_match_capturing(
3291 text: &[u8],
3292 ti: usize,
3293 pat: &[u8],
3294 pi: usize,
3295 must_end: bool,
3296 captures: &mut Vec<(usize, usize)>,
3297) -> Option<usize> {
3298 if pi >= pat.len() {
3299 return regex_check_end(ti, text.len(), must_end);
3300 }
3301
3302 if pat[pi] == b'(' {
3303 return regex_match_group(text, ti, pat, pi, must_end, captures);
3304 }
3305
3306 regex_match_elem(text, ti, pat, pi, must_end, captures)
3307}
3308
3309fn regex_check_end(ti: usize, text_len: usize, must_end: bool) -> Option<usize> {
3311 if must_end && ti < text_len {
3312 None
3313 } else {
3314 Some(ti)
3315 }
3316}
3317
3318fn regex_match_group(
3320 text: &[u8],
3321 ti: usize,
3322 pat: &[u8],
3323 pi: usize,
3324 must_end: bool,
3325 captures: &mut Vec<(usize, usize)>,
3326) -> Option<usize> {
3327 let close = find_matching_paren_bytes(pat, pi + 1)?;
3328 let inner = &pat[pi + 1..close];
3329 let rest = &pat[close + 1..];
3330 let (quant, after_quant_offset) = parse_group_quantifier(pat, close);
3331 let after_quant = &pat[after_quant_offset..];
3332 let alternatives = split_alternatives_bytes(inner);
3333
3334 regex_dispatch_group_quant(
3335 text,
3336 ti,
3337 rest,
3338 after_quant,
3339 must_end,
3340 captures,
3341 &alternatives,
3342 quant,
3343 )
3344}
3345
3346fn parse_group_quantifier(pat: &[u8], close: usize) -> (u8, usize) {
3347 if close + 1 < pat.len() {
3348 match pat[close + 1] {
3349 q @ (b'*' | b'+' | b'?') => (q, close + 2),
3350 _ => (0, close + 1),
3351 }
3352 } else {
3353 (0, close + 1)
3354 }
3355}
3356
3357#[allow(clippy::too_many_arguments)]
3358fn regex_dispatch_group_quant(
3359 text: &[u8],
3360 ti: usize,
3361 rest: &[u8],
3362 after_quant: &[u8],
3363 must_end: bool,
3364 captures: &mut Vec<(usize, usize)>,
3365 alternatives: &[Vec<u8>],
3366 quant: u8,
3367) -> Option<usize> {
3368 match quant {
3369 b'+' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 1),
3370 b'*' => regex_match_group_rep(text, ti, after_quant, must_end, captures, alternatives, 0),
3371 b'?' => regex_match_group_opt(text, ti, after_quant, must_end, captures, alternatives),
3372 _ => regex_match_group_exact(text, ti, rest, must_end, captures, alternatives),
3373 }
3374}
3375
3376fn regex_match_group_rep(
3378 text: &[u8],
3379 ti: usize,
3380 after: &[u8],
3381 must_end: bool,
3382 captures: &mut Vec<(usize, usize)>,
3383 alternatives: &[Vec<u8>],
3384 min_reps: usize,
3385) -> Option<usize> {
3386 let save = captures.len();
3387 for end_pos in (ti..=text.len()).rev() {
3388 captures.truncate(save);
3389 if let Some(result) = regex_try_group_rep_at(
3390 text,
3391 ti,
3392 end_pos,
3393 after,
3394 must_end,
3395 captures,
3396 alternatives,
3397 min_reps,
3398 save,
3399 ) {
3400 return Some(result);
3401 }
3402 }
3403 captures.truncate(save);
3404 None
3405}
3406
3407#[allow(clippy::too_many_arguments)]
3408fn regex_try_group_rep_at(
3409 text: &[u8],
3410 ti: usize,
3411 end_pos: usize,
3412 after: &[u8],
3413 must_end: bool,
3414 captures: &mut Vec<(usize, usize)>,
3415 alternatives: &[Vec<u8>],
3416 min_reps: usize,
3417 save: usize,
3418) -> Option<usize> {
3419 if !regex_match_group_repeated(text, ti, end_pos, alternatives, min_reps) {
3420 return None;
3421 }
3422 let final_end = regex_match_capturing(text, end_pos, after, 0, must_end, captures)?;
3423 captures.insert(save, (ti, end_pos));
3424 Some(final_end)
3425}
3426
3427fn regex_match_group_opt(
3429 text: &[u8],
3430 ti: usize,
3431 after: &[u8],
3432 must_end: bool,
3433 captures: &mut Vec<(usize, usize)>,
3434 alternatives: &[Vec<u8>],
3435) -> Option<usize> {
3436 let save = captures.len();
3437 if let Some(result) =
3439 regex_try_group_one_alt(text, ti, after, must_end, captures, alternatives, save)
3440 {
3441 return Some(result);
3442 }
3443 captures.truncate(save);
3445 if let Some(final_end) = regex_match_capturing(text, ti, after, 0, must_end, captures) {
3446 captures.insert(save, (ti, ti));
3447 return Some(final_end);
3448 }
3449 captures.truncate(save);
3450 None
3451}
3452
3453fn regex_try_group_one_alt(
3454 text: &[u8],
3455 ti: usize,
3456 after: &[u8],
3457 must_end: bool,
3458 captures: &mut Vec<(usize, usize)>,
3459 alternatives: &[Vec<u8>],
3460 save: usize,
3461) -> Option<usize> {
3462 for alt in alternatives {
3463 captures.truncate(save);
3464 if let Some(result) =
3465 regex_try_alt_then_continue(text, ti, alt, after, must_end, captures, save)
3466 {
3467 return Some(result);
3468 }
3469 captures.truncate(save);
3470 }
3471 None
3472}
3473
3474fn regex_try_alt_then_continue(
3475 text: &[u8],
3476 ti: usize,
3477 alt: &[u8],
3478 after: &[u8],
3479 must_end: bool,
3480 captures: &mut Vec<(usize, usize)>,
3481 save: usize,
3482) -> Option<usize> {
3483 let end = regex_try_match_at(text, ti, alt)?;
3484 let final_end = regex_match_capturing(text, end, after, 0, must_end, captures)?;
3485 captures.insert(save, (ti, end));
3486 Some(final_end)
3487}
3488
3489fn regex_match_group_exact(
3491 text: &[u8],
3492 ti: usize,
3493 rest: &[u8],
3494 must_end: bool,
3495 captures: &mut Vec<(usize, usize)>,
3496 alternatives: &[Vec<u8>],
3497) -> Option<usize> {
3498 regex_try_group_one_alt(
3499 text,
3500 ti,
3501 rest,
3502 must_end,
3503 captures,
3504 alternatives,
3505 captures.len(),
3506 )
3507}
3508
3509fn parse_quantifier(pat: &[u8], pos: usize) -> (u8, usize) {
3511 if pos < pat.len() {
3512 match pat[pos] {
3513 b'*' => (b'*', pos + 1),
3514 b'+' => (b'+', pos + 1),
3515 b'?' => (b'?', pos + 1),
3516 _ => (0, pos),
3517 }
3518 } else {
3519 (0, pos)
3520 }
3521}
3522
3523fn regex_match_elem(
3525 text: &[u8],
3526 ti: usize,
3527 pat: &[u8],
3528 pi: usize,
3529 must_end: bool,
3530 captures: &mut Vec<(usize, usize)>,
3531) -> Option<usize> {
3532 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3533 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3534
3535 match quant {
3536 b'*' | b'+' => regex_match_repeated_elem(
3537 text,
3538 ti,
3539 pat,
3540 after_quant,
3541 quant,
3542 must_end,
3543 captures,
3544 &matches_fn,
3545 ),
3546 b'?' => {
3547 regex_match_optional_elem(text, ti, pat, after_quant, must_end, captures, &matches_fn)
3548 }
3549 _ => regex_match_single_elem(text, ti, pat, elem_end, must_end, captures, &matches_fn),
3550 }
3551}
3552
3553fn count_regex_matches(text: &[u8], ti: usize, matches_fn: &dyn Fn(u8) -> bool) -> usize {
3554 let mut count = 0;
3555 while ti + count < text.len() && matches_fn(text[ti + count]) {
3556 count += 1;
3557 }
3558 count
3559}
3560
3561fn regex_match_repeated_elem(
3562 text: &[u8],
3563 ti: usize,
3564 pat: &[u8],
3565 after_quant: usize,
3566 quant: u8,
3567 must_end: bool,
3568 captures: &mut Vec<(usize, usize)>,
3569 matches_fn: &dyn Fn(u8) -> bool,
3570) -> Option<usize> {
3571 let min = usize::from(quant == b'+');
3572 let count = count_regex_matches(text, ti, matches_fn);
3573 for c in (min..=count).rev() {
3574 if let Some(end) = regex_match_capturing(text, ti + c, pat, after_quant, must_end, captures)
3575 {
3576 return Some(end);
3577 }
3578 }
3579 None
3580}
3581
3582fn regex_match_optional_elem(
3583 text: &[u8],
3584 ti: usize,
3585 pat: &[u8],
3586 after_quant: usize,
3587 must_end: bool,
3588 captures: &mut Vec<(usize, usize)>,
3589 matches_fn: &dyn Fn(u8) -> bool,
3590) -> Option<usize> {
3591 if ti < text.len() && matches_fn(text[ti]) {
3592 if let Some(end) = regex_match_capturing(text, ti + 1, pat, after_quant, must_end, captures)
3593 {
3594 return Some(end);
3595 }
3596 }
3597 regex_match_capturing(text, ti, pat, after_quant, must_end, captures)
3598}
3599
3600fn regex_match_single_elem(
3601 text: &[u8],
3602 ti: usize,
3603 pat: &[u8],
3604 elem_end: usize,
3605 must_end: bool,
3606 captures: &mut Vec<(usize, usize)>,
3607 matches_fn: &dyn Fn(u8) -> bool,
3608) -> Option<usize> {
3609 if ti < text.len() && matches_fn(text[ti]) {
3610 regex_match_capturing(text, ti + 1, pat, elem_end, must_end, captures)
3611 } else {
3612 None
3613 }
3614}
3615
3616fn regex_try_match_at(text: &[u8], start: usize, pattern: &[u8]) -> Option<usize> {
3618 regex_try_match_inner(text, start, pattern, 0)
3619}
3620
3621fn regex_try_match_inner(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3623 if pi >= pat.len() {
3624 return Some(ti);
3625 }
3626 if pat[pi] == b'(' {
3627 return regex_try_match_group(text, ti, pat, pi);
3628 }
3629 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3630 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3631 regex_try_apply_quant(text, ti, pat, elem_end, after_quant, quant, &matches_fn)
3632}
3633
3634fn regex_try_match_group(text: &[u8], ti: usize, pat: &[u8], pi: usize) -> Option<usize> {
3636 let close = find_matching_paren_bytes(pat, pi + 1)?;
3637 let inner = &pat[pi + 1..close];
3638 let rest = &pat[close + 1..];
3639 let alternatives = split_alternatives_bytes(inner);
3640 for alt in &alternatives {
3641 if let Some(end) = regex_try_alt_and_rest(text, ti, alt, rest) {
3642 return Some(end);
3643 }
3644 }
3645 None
3646}
3647
3648fn regex_try_alt_and_rest(text: &[u8], ti: usize, alt: &[u8], rest: &[u8]) -> Option<usize> {
3649 let after_alt = regex_try_match_inner(text, ti, alt, 0)?;
3650 regex_try_match_inner(text, after_alt, rest, 0)
3651}
3652
3653fn regex_try_apply_quant(
3655 text: &[u8],
3656 ti: usize,
3657 pat: &[u8],
3658 elem_end: usize,
3659 after_quant: usize,
3660 quant: u8,
3661 matches_fn: &dyn Fn(u8) -> bool,
3662) -> Option<usize> {
3663 match quant {
3664 b'*' | b'+' => regex_try_match_repeated_elem(text, ti, pat, after_quant, quant, matches_fn),
3665 b'?' => regex_try_match_optional_elem(text, ti, pat, after_quant, matches_fn),
3666 _ => regex_try_match_single_elem(text, ti, pat, elem_end, matches_fn),
3667 }
3668}
3669
3670fn regex_try_match_repeated_elem(
3671 text: &[u8],
3672 ti: usize,
3673 pat: &[u8],
3674 after_quant: usize,
3675 quant: u8,
3676 matches_fn: &dyn Fn(u8) -> bool,
3677) -> Option<usize> {
3678 let min = usize::from(quant == b'+');
3679 let count = count_regex_matches(text, ti, matches_fn);
3680 for c in (min..=count).rev() {
3681 if let Some(end) = regex_try_match_inner(text, ti + c, pat, after_quant) {
3682 return Some(end);
3683 }
3684 }
3685 None
3686}
3687
3688fn regex_try_match_optional_elem(
3689 text: &[u8],
3690 ti: usize,
3691 pat: &[u8],
3692 after_quant: usize,
3693 matches_fn: &dyn Fn(u8) -> bool,
3694) -> Option<usize> {
3695 if ti < text.len() && matches_fn(text[ti]) {
3696 if let Some(end) = regex_try_match_inner(text, ti + 1, pat, after_quant) {
3697 return Some(end);
3698 }
3699 }
3700 regex_try_match_inner(text, ti, pat, after_quant)
3701}
3702
3703fn regex_try_match_single_elem(
3704 text: &[u8],
3705 ti: usize,
3706 pat: &[u8],
3707 elem_end: usize,
3708 matches_fn: &dyn Fn(u8) -> bool,
3709) -> Option<usize> {
3710 if ti < text.len() && matches_fn(text[ti]) {
3711 regex_try_match_inner(text, ti + 1, pat, elem_end)
3712 } else {
3713 None
3714 }
3715}
3716
3717fn regex_match_group_repeated(
3719 text: &[u8],
3720 start: usize,
3721 end: usize,
3722 alternatives: &[Vec<u8>],
3723 min_reps: usize,
3724) -> bool {
3725 if start == end {
3726 return min_reps == 0;
3727 }
3728 if start > end {
3729 return false;
3730 }
3731 for alt in alternatives {
3732 if regex_group_repetition_matches(text, start, end, alternatives, min_reps, alt) {
3733 return true;
3734 }
3735 }
3736 false
3737}
3738
3739fn regex_group_repetition_matches(
3740 text: &[u8],
3741 start: usize,
3742 end: usize,
3743 alternatives: &[Vec<u8>],
3744 min_reps: usize,
3745 alt: &[u8],
3746) -> bool {
3747 let Some(after) = regex_try_match_inner(text, start, alt, 0) else {
3748 return false;
3749 };
3750 if after <= start || after > end {
3751 return false;
3752 }
3753 if after == end && min_reps <= 1 {
3754 return true;
3755 }
3756 regex_match_group_repeated(text, after, end, alternatives, min_reps.saturating_sub(1))
3757}
3758
3759fn find_matching_paren_bytes(pat: &[u8], start: usize) -> Option<usize> {
3761 let mut depth = 1;
3762 let mut i = start;
3763 while i < pat.len() {
3764 if pat[i] == b'\\' {
3765 i += 2;
3766 continue;
3767 }
3768 if pat[i] == b'(' {
3769 depth += 1;
3770 } else if pat[i] == b')' {
3771 depth -= 1;
3772 if depth == 0 {
3773 return Some(i);
3774 }
3775 }
3776 i += 1;
3777 }
3778 None
3779}
3780
3781fn split_alternatives_bytes(pat: &[u8]) -> Vec<Vec<u8>> {
3783 let mut alternatives = Vec::new();
3784 let mut current = Vec::new();
3785 let mut depth = 0i32;
3786 let mut i = 0;
3787 while i < pat.len() {
3788 if pat[i] == b'\\' && i + 1 < pat.len() {
3789 current.push(pat[i]);
3790 current.push(pat[i + 1]);
3791 i += 2;
3792 continue;
3793 }
3794 split_alt_classify_byte(pat[i], &mut depth, &mut current, &mut alternatives);
3795 i += 1;
3796 }
3797 alternatives.push(current);
3798 alternatives
3799}
3800
3801fn split_alt_classify_byte(
3802 byte: u8,
3803 depth: &mut i32,
3804 current: &mut Vec<u8>,
3805 alternatives: &mut Vec<Vec<u8>>,
3806) {
3807 match byte {
3808 b'(' => {
3809 *depth += 1;
3810 current.push(byte);
3811 }
3812 b')' => {
3813 *depth -= 1;
3814 current.push(byte);
3815 }
3816 b'|' if *depth == 0 => {
3817 alternatives.push(std::mem::take(current));
3818 }
3819 _ => {
3820 current.push(byte);
3821 }
3822 }
3823}
3824
3825#[allow(dead_code)]
3830fn simple_regex_match(text: &str, pattern: &str) -> bool {
3831 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3832
3833 if has_regex_metachar(core) {
3834 return regex_like_match(text, pattern);
3835 }
3836
3837 literal_match_range(text, core, anchored_start, anchored_end).is_some()
3839}
3840
3841#[allow(dead_code)]
3846fn regex_like_match(text: &str, pattern: &str) -> bool {
3847 let (core, anchored_start, anchored_end) = regex_strip_anchors(pattern);
3848
3849 if anchored_start {
3850 regex_match_at(text, 0, core, anchored_end)
3851 } else {
3852 (0..=text.len()).any(|start| regex_match_at(text, start, core, anchored_end))
3853 }
3854}
3855
3856#[allow(dead_code)]
3859fn regex_match_at(text: &str, start: usize, core: &str, must_end: bool) -> bool {
3860 let text_bytes = text.as_bytes();
3861 let core_bytes = core.as_bytes();
3862 regex_backtrack(text_bytes, start, core_bytes, 0, must_end)
3863}
3864
3865#[allow(dead_code)]
3867fn regex_backtrack(text: &[u8], ti: usize, pat: &[u8], pi: usize, must_end: bool) -> bool {
3868 if pi >= pat.len() {
3869 return if must_end { ti >= text.len() } else { true };
3870 }
3871
3872 let (elem_end, matches_fn) = parse_regex_elem(pat, pi);
3873 let (quant, after_quant) = parse_quantifier(pat, elem_end);
3874
3875 match quant {
3876 b'*' => regex_backtrack_star(text, ti, pat, after_quant, must_end, &matches_fn),
3877 b'+' => regex_backtrack_plus(text, ti, pat, after_quant, must_end, &matches_fn),
3878 b'?' => regex_backtrack_optional(text, ti, pat, after_quant, must_end, &matches_fn),
3879 _ => regex_backtrack_single(text, ti, pat, elem_end, must_end, &matches_fn),
3880 }
3881}
3882
3883fn regex_backtrack_star(
3884 text: &[u8],
3885 ti: usize,
3886 pat: &[u8],
3887 after_quant: usize,
3888 must_end: bool,
3889 matches_fn: &dyn Fn(u8) -> bool,
3890) -> bool {
3891 let mut count = 0;
3892 loop {
3893 if regex_backtrack(text, ti + count, pat, after_quant, must_end) {
3894 return true;
3895 }
3896 if ti + count < text.len() && matches_fn(text[ti + count]) {
3897 count += 1;
3898 } else {
3899 return false;
3900 }
3901 }
3902}
3903
3904fn regex_backtrack_plus(
3905 text: &[u8],
3906 ti: usize,
3907 pat: &[u8],
3908 after_quant: usize,
3909 must_end: bool,
3910 matches_fn: &dyn Fn(u8) -> bool,
3911) -> bool {
3912 let count = count_regex_matches(text, ti, matches_fn);
3913 (1..=count).any(|matched| regex_backtrack(text, ti + matched, pat, after_quant, must_end))
3914}
3915
3916fn regex_backtrack_optional(
3917 text: &[u8],
3918 ti: usize,
3919 pat: &[u8],
3920 after_quant: usize,
3921 must_end: bool,
3922 matches_fn: &dyn Fn(u8) -> bool,
3923) -> bool {
3924 regex_backtrack(text, ti, pat, after_quant, must_end)
3925 || (ti < text.len()
3926 && matches_fn(text[ti])
3927 && regex_backtrack(text, ti + 1, pat, after_quant, must_end))
3928}
3929
3930fn regex_backtrack_single(
3931 text: &[u8],
3932 ti: usize,
3933 pat: &[u8],
3934 elem_end: usize,
3935 must_end: bool,
3936 matches_fn: &dyn Fn(u8) -> bool,
3937) -> bool {
3938 ti < text.len()
3939 && matches_fn(text[ti])
3940 && regex_backtrack(text, ti + 1, pat, elem_end, must_end)
3941}
3942
3943fn parse_regex_elem(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3946 match pat[pi] {
3947 b'.' => (pi + 1, Box::new(|_: u8| true)),
3948 b'[' => parse_regex_char_class(pat, pi),
3949 b'\\' if pi + 1 < pat.len() => {
3950 let escaped = pat[pi + 1];
3951 (pi + 2, Box::new(move |c: u8| c == escaped))
3952 }
3953 ch => (pi + 1, Box::new(move |c: u8| c == ch)),
3954 }
3955}
3956
3957fn parse_regex_char_class(pat: &[u8], pi: usize) -> (usize, Box<dyn Fn(u8) -> bool>) {
3958 let mut i = pi + 1;
3959 let negate = i < pat.len() && (pat[i] == b'^' || pat[i] == b'!');
3960 if negate {
3961 i += 1;
3962 }
3963 let mut chars = Vec::new();
3964 while i < pat.len() && pat[i] != b']' {
3965 if i + 2 < pat.len() && pat[i + 1] == b'-' {
3966 chars.extend(pat[i]..=pat[i + 2]);
3967 i += 3;
3968 } else {
3969 chars.push(pat[i]);
3970 i += 1;
3971 }
3972 }
3973 let end = if i < pat.len() { i + 1 } else { i };
3974 (
3975 end,
3976 Box::new(move |c: u8| regex_char_class_matches(&chars, negate, c)),
3977 )
3978}
3979
3980fn regex_char_class_matches(chars: &[u8], negate: bool, c: u8) -> bool {
3981 let found = chars.contains(&c);
3982 if negate {
3983 !found
3984 } else {
3985 found
3986 }
3987}
3988
3989fn glob_match_char_class(pattern: &[u8], mut pi: usize, ch: u8) -> (usize, bool) {
3992 let negate = pi < pattern.len() && (pattern[pi] == b'!' || pattern[pi] == b'^');
3993 if negate {
3994 pi += 1;
3995 }
3996 let mut matched = false;
3997 let mut first = true;
3998 while pi < pattern.len() && (first || pattern[pi] != b']') {
3999 first = false;
4000 let (next_pi, item_matched) = glob_match_char_class_item(pattern, pi, ch);
4001 matched |= item_matched;
4002 pi = next_pi;
4003 }
4004 if pi < pattern.len() && pattern[pi] == b']' {
4005 pi += 1;
4006 }
4007 (pi, matched != negate)
4008}
4009
4010fn glob_match_char_class_item(pattern: &[u8], pi: usize, ch: u8) -> (usize, bool) {
4011 if pi + 2 < pattern.len() && pattern[pi + 1] == b'-' {
4012 let lo = pattern[pi];
4013 let hi = pattern[pi + 2];
4014 return (pi + 3, ch >= lo && ch <= hi);
4015 }
4016 (pi + 1, pattern[pi] == ch)
4017}
4018
4019enum GlobPatternStep {
4020 Consume(usize),
4021 Star,
4022 Class(usize, bool),
4023 Mismatch,
4024}
4025
4026fn glob_step(pattern: &[u8], pi: usize, ch: u8) -> GlobPatternStep {
4027 if pi >= pattern.len() {
4028 return GlobPatternStep::Mismatch;
4029 }
4030
4031 match pattern[pi] {
4032 b'?' => GlobPatternStep::Consume(pi + 1),
4033 b'*' => GlobPatternStep::Star,
4034 b'[' => {
4035 let (new_pi, matched) = glob_match_char_class(pattern, pi + 1, ch);
4036 GlobPatternStep::Class(new_pi, matched)
4037 }
4038 literal if literal == ch => GlobPatternStep::Consume(pi + 1),
4039 _ => GlobPatternStep::Mismatch,
4040 }
4041}
4042
4043fn glob_backtrack(pi: &mut usize, ni: &mut usize, star_pi: usize, star_ni: &mut usize) -> bool {
4044 if star_pi == usize::MAX {
4045 return false;
4046 }
4047
4048 *pi = star_pi + 1;
4049 *star_ni += 1;
4050 *ni = *star_ni;
4051 true
4052}
4053
4054fn glob_match_inner(pattern: &[u8], name: &[u8]) -> bool {
4058 let mut pi = 0;
4059 let mut ni = 0;
4060 let mut star_pi = usize::MAX;
4061 let mut star_ni = usize::MAX;
4062
4063 while ni < name.len() {
4064 match glob_step(pattern, pi, name[ni]) {
4065 GlobPatternStep::Star => {
4066 star_pi = pi;
4067 star_ni = ni;
4068 pi += 1;
4069 }
4070 GlobPatternStep::Consume(new_pi) | GlobPatternStep::Class(new_pi, true) => {
4071 pi = new_pi;
4072 ni += 1;
4073 }
4074 GlobPatternStep::Class(_, false) | GlobPatternStep::Mismatch => {
4075 if !glob_backtrack(&mut pi, &mut ni, star_pi, &mut star_ni) {
4076 return false;
4077 }
4078 }
4079 }
4080 }
4081
4082 while pi < pattern.len() && pattern[pi] == b'*' {
4084 pi += 1;
4085 }
4086
4087 pi == pattern.len()
4088}
4089
4090fn glob_match_ext(pattern: &str, name: &str, dotglob: bool, extglob: bool) -> bool {
4092 if name.starts_with('.') && !pattern.starts_with('.') && !dotglob {
4094 return false;
4095 }
4096 if extglob && has_extglob_pattern(pattern) {
4097 return extglob_match(pattern, name);
4098 }
4099 glob_match_inner(pattern.as_bytes(), name.as_bytes())
4100}
4101
4102fn has_extglob_pattern(pattern: &str) -> bool {
4104 let bytes = pattern.as_bytes();
4105 for i in 0..bytes.len().saturating_sub(1) {
4106 if bytes[i + 1] == b'(' && matches!(bytes[i], b'?' | b'*' | b'+' | b'@' | b'!') {
4107 return true;
4108 }
4109 }
4110 false
4111}
4112
4113pub fn extglob_match(pattern: &str, name: &str) -> bool {
4118 extglob_match_recursive(pattern.as_bytes(), name.as_bytes())
4119}
4120
4121fn extglob_match_recursive(pattern: &[u8], name: &[u8]) -> bool {
4122 let Some((pi, op, close)) = find_extglob_operator(pattern) else {
4124 return glob_match_inner(pattern, name);
4125 };
4126
4127 let open = pi + 2;
4128 let alternatives = split_alternatives(&pattern[open..close]);
4129 let prefix = &pattern[..pi];
4130 let suffix = &pattern[close + 1..];
4131
4132 match op {
4133 b'@' | b'?' => extglob_match_at_or_opt(op, prefix, &alternatives, suffix, name),
4134 b'*' => extglob_star(prefix, &alternatives, suffix, name, 0),
4135 b'+' => extglob_plus(prefix, &alternatives, suffix, name, 0),
4136 b'!' => extglob_match_negate(prefix, &alternatives, suffix, name),
4137 _ => unreachable!(),
4138 }
4139}
4140
4141fn find_extglob_operator(pattern: &[u8]) -> Option<(usize, u8, usize)> {
4143 let mut pi = 0;
4144 while pi < pattern.len() {
4145 if pi + 1 < pattern.len()
4146 && pattern[pi + 1] == b'('
4147 && matches!(pattern[pi], b'?' | b'*' | b'+' | b'@' | b'!')
4148 {
4149 if let Some(close) = find_matching_paren(pattern, pi + 2) {
4150 return Some((pi, pattern[pi], close));
4151 }
4152 }
4153 pi += 1;
4154 }
4155 None
4156}
4157
4158fn build_combined(prefix: &[u8], mid: &[u8], suffix: &[u8]) -> Vec<u8> {
4160 let mut combined = Vec::with_capacity(prefix.len() + mid.len() + suffix.len());
4161 combined.extend_from_slice(prefix);
4162 combined.extend_from_slice(mid);
4163 combined.extend_from_slice(suffix);
4164 combined
4165}
4166
4167fn extglob_match_at_or_opt(
4169 op: u8,
4170 prefix: &[u8],
4171 alternatives: &[Vec<u8>],
4172 suffix: &[u8],
4173 name: &[u8],
4174) -> bool {
4175 if op == b'?' && extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4177 return true;
4178 }
4179 for alt in alternatives {
4181 if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4182 return true;
4183 }
4184 }
4185 false
4186}
4187
4188fn extglob_match_negate(
4190 prefix: &[u8],
4191 alternatives: &[Vec<u8>],
4192 suffix: &[u8],
4193 name: &[u8],
4194) -> bool {
4195 for alt in alternatives {
4196 if extglob_match_recursive(&build_combined(prefix, alt, suffix), name) {
4197 return false;
4198 }
4199 }
4200 let wildcard = build_combined(prefix, b"*", suffix);
4201 glob_match_inner(&wildcard, name)
4202}
4203
4204fn extglob_star(
4206 prefix: &[u8],
4207 alternatives: &[Vec<u8>],
4208 suffix: &[u8],
4209 name: &[u8],
4210 depth: u32,
4211) -> bool {
4212 if depth > 20 {
4213 return false;
4214 }
4215 if extglob_match_recursive(&build_combined(prefix, &[], suffix), name) {
4217 return true;
4218 }
4219 extglob_try_extend(prefix, alternatives, suffix, name, depth)
4221}
4222
4223fn extglob_try_extend(
4224 prefix: &[u8],
4225 alternatives: &[Vec<u8>],
4226 suffix: &[u8],
4227 name: &[u8],
4228 depth: u32,
4229) -> bool {
4230 let prefix_len = prefix.len();
4231 for alt in alternatives {
4232 let new_prefix = build_combined(prefix, alt, &[]);
4233 if new_prefix.len() > prefix_len
4234 && extglob_star(&new_prefix, alternatives, suffix, name, depth + 1)
4235 {
4236 return true;
4237 }
4238 }
4239 false
4240}
4241
4242fn extglob_plus(
4244 prefix: &[u8],
4245 alternatives: &[Vec<u8>],
4246 suffix: &[u8],
4247 name: &[u8],
4248 depth: u32,
4249) -> bool {
4250 if depth > 20 {
4251 return false;
4252 }
4253 for alt in alternatives {
4254 let new_prefix = build_combined(prefix, alt, &[]);
4255 if extglob_star(&new_prefix, alternatives, suffix, name, depth + 1) {
4256 return true;
4257 }
4258 }
4259 false
4260}
4261
4262fn find_matching_paren(pattern: &[u8], open: usize) -> Option<usize> {
4264 let mut depth: u32 = 1;
4265 let mut i = open;
4266 while i < pattern.len() {
4267 if pattern[i] == b'(' {
4268 depth += 1;
4269 } else if pattern[i] == b')' {
4270 depth -= 1;
4271 if depth == 0 {
4272 return Some(i);
4273 }
4274 }
4275 i += 1;
4276 }
4277 None
4278}
4279
4280fn split_alternatives(pat: &[u8]) -> Vec<Vec<u8>> {
4282 let mut result = Vec::new();
4283 let mut current = Vec::new();
4284 let mut depth: u32 = 0;
4285 for &b in pat {
4286 if b == b'(' {
4287 depth += 1;
4288 current.push(b);
4289 } else if b == b')' {
4290 depth -= 1;
4291 current.push(b);
4292 } else if b == b'|' && depth == 0 {
4293 result.push(std::mem::take(&mut current));
4294 } else {
4295 current.push(b);
4296 }
4297 }
4298 result.push(current);
4299 result
4300}
4301
4302impl Default for WorkerRuntime {
4303 fn default() -> Self {
4304 Self::new()
4305 }
4306}