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