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