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