1#![allow(clippy::unwrap_used)]
12
13mod jobs;
14mod state;
15
16#[allow(unused_imports)]
17pub use jobs::{JobTable, SharedJobTable};
18pub use state::{ControlFlow, ExecResult};
19use std::collections::{HashMap, HashSet};
22use std::panic::AssertUnwindSafe;
23use std::path::{Path, PathBuf};
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::Arc;
26
27static PROC_SUB_COUNTER: AtomicU64 = AtomicU64::new(0);
29
30use futures::FutureExt;
31
32use crate::builtins::{self, Builtin};
33#[cfg(feature = "failpoints")]
34use crate::error::Error;
35use crate::error::Result;
36use crate::fs::FileSystem;
37use crate::limits::{ExecutionCounters, ExecutionLimits};
38
39pub type OutputCallback = Box<dyn FnMut(&str, &str) + Send + Sync>;
47use crate::parser::{
48 ArithmeticForCommand, AssignmentValue, CaseCommand, Command, CommandList, CompoundCommand,
49 ForCommand, FunctionDef, IfCommand, ListOperator, ParameterOp, Parser, Pipeline, Redirect,
50 RedirectKind, Script, SelectCommand, SimpleCommand, Span, TimeCommand, UntilCommand,
51 WhileCommand, Word, WordPart,
52};
53
54#[cfg(feature = "failpoints")]
55use fail::fail_point;
56
57const DEV_NULL: &str = "/dev/null";
60
61fn is_keyword(name: &str) -> bool {
63 matches!(
64 name,
65 "if" | "then"
66 | "else"
67 | "elif"
68 | "fi"
69 | "for"
70 | "while"
71 | "until"
72 | "do"
73 | "done"
74 | "case"
75 | "esac"
76 | "in"
77 | "function"
78 | "select"
79 | "time"
80 | "{"
81 | "}"
82 | "[["
83 | "]]"
84 | "!"
85 )
86}
87
88fn levenshtein(a: &str, b: &str) -> usize {
90 let a: Vec<char> = a.chars().collect();
91 let b: Vec<char> = b.chars().collect();
92 let n = b.len();
93 let mut prev = (0..=n).collect::<Vec<_>>();
94 let mut curr = vec![0; n + 1];
95 for (i, ca) in a.iter().enumerate() {
96 curr[0] = i + 1;
97 for (j, cb) in b.iter().enumerate() {
98 let cost = if ca == cb { 0 } else { 1 };
99 curr[j + 1] = (prev[j + 1] + 1).min(curr[j] + 1).min(prev[j] + cost);
100 }
101 std::mem::swap(&mut prev, &mut curr);
102 }
103 prev[n]
104}
105
106fn unavailable_command_hint(name: &str) -> Option<&'static str> {
108 match name {
109 "pip" | "pip3" | "pip2" => Some("Package managers are not available in the sandbox."),
110 "apt" | "apt-get" | "yum" | "dnf" | "pacman" | "brew" | "apk" => {
111 Some("Package managers are not available in the sandbox.")
112 }
113 "npm" | "yarn" | "pnpm" | "bun" => {
114 Some("Package managers are not available in the sandbox.")
115 }
116 "sudo" | "su" | "doas" => Some("All commands run without privilege restrictions."),
117 "ssh" | "scp" | "sftp" | "rsync" => Some("Network access is limited to curl/wget."),
118 "docker" | "podman" | "kubectl" | "systemctl" | "service" => {
119 Some("Container and service management is not available in the sandbox.")
120 }
121 "make" | "cmake" | "gcc" | "g++" | "clang" | "rustc" | "cargo" | "go" | "javac"
122 | "node" => Some("Compilers and build tools are not available in the sandbox."),
123 "vi" | "vim" | "nano" | "emacs" => {
124 Some("Interactive editors are not available. Use echo/printf/cat to write files.")
125 }
126 "man" | "info" => Some("Manual pages are not available in the sandbox."),
127 _ => None,
128 }
129}
130
131fn command_not_found_message(name: &str, known_commands: &[&str]) -> String {
133 let mut msg = format!("bash: {}: command not found", name);
134
135 if let Some(hint) = unavailable_command_hint(name) {
137 msg.push_str(&format!(". {}", hint));
138 return msg;
139 }
140
141 let max_dist = if name.len() <= 3 { 1 } else { 2 };
143 let mut suggestions: Vec<(&str, usize)> = known_commands
144 .iter()
145 .filter_map(|cmd| {
146 let d = levenshtein(name, cmd);
147 if d > 0 && d <= max_dist {
148 Some((*cmd, d))
149 } else {
150 None
151 }
152 })
153 .collect();
154 suggestions.sort_by_key(|(_, d)| *d);
155 suggestions.truncate(3);
156
157 if !suggestions.is_empty() {
158 let names: Vec<&str> = suggestions.iter().map(|(s, _)| *s).collect();
159 msg.push_str(&format!(". Did you mean: {}?", names.join(", ")));
160 }
161
162 msg
163}
164
165fn is_dev_null(path: &Path) -> bool {
168 let mut normalized = PathBuf::new();
170 for component in path.components() {
171 match component {
172 std::path::Component::RootDir => normalized.push("/"),
173 std::path::Component::Normal(name) => normalized.push(name),
174 std::path::Component::ParentDir => {
175 normalized.pop();
176 }
177 std::path::Component::CurDir => {}
178 std::path::Component::Prefix(_) => {}
179 }
180 }
181 if normalized.as_os_str().is_empty() {
182 normalized.push("/");
183 }
184 normalized == Path::new(DEV_NULL)
185}
186
187pub(crate) fn is_internal_variable(name: &str) -> bool {
190 name.starts_with("_NAMEREF_")
191 || name.starts_with("_READONLY_")
192 || name.starts_with("_UPPER_")
193 || name.starts_with("_LOWER_")
194 || name.starts_with("_ARRAY_READ_")
195 || name == "_EVAL_CMD"
196 || name == "_SHIFT_COUNT"
197 || name == "_SET_POSITIONAL"
198}
199
200#[derive(Debug, Clone)]
202struct CallFrame {
203 name: String,
205 locals: HashMap<String, String>,
207 positional: Vec<String>,
209}
210
211#[derive(Debug, Clone, Default)]
213pub struct ShellOptions {
214 pub errexit: bool,
216 pub xtrace: bool,
218 pub pipefail: bool,
220}
221
222#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
228pub struct ShellState {
229 pub env: HashMap<String, String>,
231 pub variables: HashMap<String, String>,
233 pub arrays: HashMap<String, HashMap<usize, String>>,
235 pub assoc_arrays: HashMap<String, HashMap<String, String>>,
237 pub cwd: PathBuf,
239 pub last_exit_code: i32,
241 pub aliases: HashMap<String, String>,
243 pub traps: HashMap<String, String>,
245 pub errexit: bool,
247 pub xtrace: bool,
249 pub pipefail: bool,
251}
252
253pub struct Interpreter {
255 fs: Arc<dyn FileSystem>,
256 env: HashMap<String, String>,
257 variables: HashMap<String, String>,
258 arrays: HashMap<String, HashMap<usize, String>>,
260 assoc_arrays: HashMap<String, HashMap<String, String>>,
262 cwd: PathBuf,
263 last_exit_code: i32,
264 builtins: HashMap<String, Box<dyn Builtin>>,
266 functions: HashMap<String, FunctionDef>,
268 call_stack: Vec<CallFrame>,
270 limits: ExecutionLimits,
272 counters: ExecutionCounters,
274 #[allow(dead_code)]
276 jobs: JobTable,
277 options: ShellOptions,
279 current_line: usize,
281 #[cfg(feature = "http_client")]
283 http_client: Option<crate::network::HttpClient>,
284 #[cfg(feature = "git")]
286 git_client: Option<crate::git::GitClient>,
287 pipeline_stdin: Option<String>,
290 output_callback: Option<OutputCallback>,
294 output_emit_count: u64,
297 nounset_error: Option<String>,
299 traps: HashMap<String, String>,
301 pipestatus: Vec<i32>,
303 aliases: HashMap<String, String>,
305 expanding_aliases: HashSet<String>,
308}
309
310impl Interpreter {
311 const MAX_GLOB_DEPTH: usize = 50;
312
313 pub fn new(fs: Arc<dyn FileSystem>) -> Self {
315 Self::with_config(fs, None, None, None, HashMap::new())
316 }
317
318 pub fn with_config(
327 fs: Arc<dyn FileSystem>,
328 username: Option<String>,
329 hostname: Option<String>,
330 fixed_epoch: Option<i64>,
331 custom_builtins: HashMap<String, Box<dyn Builtin>>,
332 ) -> Self {
333 let mut builtins: HashMap<String, Box<dyn Builtin>> = HashMap::new();
334
335 builtins.insert("echo".to_string(), Box::new(builtins::Echo));
337 builtins.insert("true".to_string(), Box::new(builtins::True));
338 builtins.insert("false".to_string(), Box::new(builtins::False));
339 builtins.insert("exit".to_string(), Box::new(builtins::Exit));
340 builtins.insert("cd".to_string(), Box::new(builtins::Cd));
341 builtins.insert("pwd".to_string(), Box::new(builtins::Pwd));
342 builtins.insert("cat".to_string(), Box::new(builtins::Cat));
343 builtins.insert("break".to_string(), Box::new(builtins::Break));
344 builtins.insert("continue".to_string(), Box::new(builtins::Continue));
345 builtins.insert("return".to_string(), Box::new(builtins::Return));
346 builtins.insert("test".to_string(), Box::new(builtins::Test));
347 builtins.insert("[".to_string(), Box::new(builtins::Bracket));
348 builtins.insert("printf".to_string(), Box::new(builtins::Printf));
349 builtins.insert("export".to_string(), Box::new(builtins::Export));
350 builtins.insert("read".to_string(), Box::new(builtins::Read));
351 builtins.insert("set".to_string(), Box::new(builtins::Set));
352 builtins.insert("unset".to_string(), Box::new(builtins::Unset));
353 builtins.insert("shift".to_string(), Box::new(builtins::Shift));
354 builtins.insert("local".to_string(), Box::new(builtins::Local));
355 builtins.insert(":".to_string(), Box::new(builtins::Colon));
357 builtins.insert("readonly".to_string(), Box::new(builtins::Readonly));
358 builtins.insert("times".to_string(), Box::new(builtins::Times));
359 builtins.insert("eval".to_string(), Box::new(builtins::Eval));
360 builtins.insert(
361 "source".to_string(),
362 Box::new(builtins::Source::new(fs.clone())),
363 );
364 builtins.insert(".".to_string(), Box::new(builtins::Source::new(fs.clone())));
365 builtins.insert("jq".to_string(), Box::new(builtins::Jq));
366 builtins.insert("grep".to_string(), Box::new(builtins::Grep));
367 builtins.insert("sed".to_string(), Box::new(builtins::Sed));
368 builtins.insert("awk".to_string(), Box::new(builtins::Awk));
369 builtins.insert("sleep".to_string(), Box::new(builtins::Sleep));
370 builtins.insert("head".to_string(), Box::new(builtins::Head));
371 builtins.insert("tail".to_string(), Box::new(builtins::Tail));
372 builtins.insert("basename".to_string(), Box::new(builtins::Basename));
373 builtins.insert("dirname".to_string(), Box::new(builtins::Dirname));
374 builtins.insert("realpath".to_string(), Box::new(builtins::Realpath));
375 builtins.insert("mkdir".to_string(), Box::new(builtins::Mkdir));
376 builtins.insert("mktemp".to_string(), Box::new(builtins::Mktemp));
377 builtins.insert("rm".to_string(), Box::new(builtins::Rm));
378 builtins.insert("cp".to_string(), Box::new(builtins::Cp));
379 builtins.insert("mv".to_string(), Box::new(builtins::Mv));
380 builtins.insert("touch".to_string(), Box::new(builtins::Touch));
381 builtins.insert("chmod".to_string(), Box::new(builtins::Chmod));
382 builtins.insert("ln".to_string(), Box::new(builtins::Ln));
383 builtins.insert("chown".to_string(), Box::new(builtins::Chown));
384 builtins.insert("kill".to_string(), Box::new(builtins::Kill));
385 builtins.insert("wc".to_string(), Box::new(builtins::Wc));
386 builtins.insert("nl".to_string(), Box::new(builtins::Nl));
387 builtins.insert("paste".to_string(), Box::new(builtins::Paste));
388 builtins.insert("column".to_string(), Box::new(builtins::Column));
389 builtins.insert("comm".to_string(), Box::new(builtins::Comm));
390 builtins.insert("diff".to_string(), Box::new(builtins::Diff));
391 builtins.insert("strings".to_string(), Box::new(builtins::Strings));
392 builtins.insert("od".to_string(), Box::new(builtins::Od));
393 builtins.insert("xxd".to_string(), Box::new(builtins::Xxd));
394 builtins.insert("hexdump".to_string(), Box::new(builtins::Hexdump));
395 builtins.insert("base64".to_string(), Box::new(builtins::Base64));
396 builtins.insert("md5sum".to_string(), Box::new(builtins::Md5sum));
397 builtins.insert("sha1sum".to_string(), Box::new(builtins::Sha1sum));
398 builtins.insert("sha256sum".to_string(), Box::new(builtins::Sha256sum));
399 builtins.insert("seq".to_string(), Box::new(builtins::Seq));
400 builtins.insert("tac".to_string(), Box::new(builtins::Tac));
401 builtins.insert("rev".to_string(), Box::new(builtins::Rev));
402 builtins.insert("yes".to_string(), Box::new(builtins::Yes));
403 builtins.insert("expr".to_string(), Box::new(builtins::Expr));
404 builtins.insert("bc".to_string(), Box::new(builtins::Bc));
405 builtins.insert("pushd".to_string(), Box::new(builtins::Pushd));
406 builtins.insert("popd".to_string(), Box::new(builtins::Popd));
407 builtins.insert("dirs".to_string(), Box::new(builtins::Dirs));
408 builtins.insert("sort".to_string(), Box::new(builtins::Sort));
409 builtins.insert("uniq".to_string(), Box::new(builtins::Uniq));
410 builtins.insert("cut".to_string(), Box::new(builtins::Cut));
411 builtins.insert("tr".to_string(), Box::new(builtins::Tr));
412 builtins.insert(
414 "date".to_string(),
415 Box::new(if let Some(epoch) = fixed_epoch {
416 use chrono::DateTime;
417 builtins::Date::with_fixed_epoch(
418 DateTime::from_timestamp(epoch, 0).unwrap_or_default(),
419 )
420 } else {
421 builtins::Date::new()
422 }),
423 );
424 builtins.insert("wait".to_string(), Box::new(builtins::Wait));
425 builtins.insert("curl".to_string(), Box::new(builtins::Curl));
426 builtins.insert("wget".to_string(), Box::new(builtins::Wget));
427 #[cfg(feature = "git")]
429 builtins.insert("git".to_string(), Box::new(builtins::Git));
430 builtins.insert("timeout".to_string(), Box::new(builtins::Timeout));
433 let hostname_val = hostname.unwrap_or_else(|| builtins::DEFAULT_HOSTNAME.to_string());
435 let username_val = username.unwrap_or_else(|| builtins::DEFAULT_USERNAME.to_string());
436 builtins.insert(
437 "hostname".to_string(),
438 Box::new(builtins::Hostname::with_hostname(&hostname_val)),
439 );
440 builtins.insert(
441 "uname".to_string(),
442 Box::new(builtins::Uname::with_hostname(&hostname_val)),
443 );
444 builtins.insert(
445 "whoami".to_string(),
446 Box::new(builtins::Whoami::with_username(&username_val)),
447 );
448 builtins.insert(
449 "id".to_string(),
450 Box::new(builtins::Id::with_username(&username_val)),
451 );
452 builtins.insert("ls".to_string(), Box::new(builtins::Ls));
454 builtins.insert("find".to_string(), Box::new(builtins::Find));
455 builtins.insert("rmdir".to_string(), Box::new(builtins::Rmdir));
456 builtins.insert("less".to_string(), Box::new(builtins::Less));
458 builtins.insert("file".to_string(), Box::new(builtins::File));
459 builtins.insert("stat".to_string(), Box::new(builtins::Stat));
460 builtins.insert("tar".to_string(), Box::new(builtins::Tar));
462 builtins.insert("gzip".to_string(), Box::new(builtins::Gzip));
463 builtins.insert("gunzip".to_string(), Box::new(builtins::Gunzip));
464 builtins.insert("du".to_string(), Box::new(builtins::Du));
466 builtins.insert("df".to_string(), Box::new(builtins::Df));
467 builtins.insert("env".to_string(), Box::new(builtins::Env));
469 builtins.insert("printenv".to_string(), Box::new(builtins::Printenv));
470 builtins.insert("history".to_string(), Box::new(builtins::History));
471 builtins.insert("xargs".to_string(), Box::new(builtins::Xargs));
473 builtins.insert("tee".to_string(), Box::new(builtins::Tee));
474 builtins.insert("watch".to_string(), Box::new(builtins::Watch));
475 builtins.insert("shopt".to_string(), Box::new(builtins::Shopt));
476
477 for (name, builtin) in custom_builtins {
479 builtins.insert(name, builtin);
480 }
481
482 let mut variables = HashMap::new();
484 variables.insert("HOME".to_string(), format!("/home/{}", &username_val));
485 variables.insert("USER".to_string(), username_val.clone());
486 variables.insert("UID".to_string(), "1000".to_string());
487 variables.insert("EUID".to_string(), "1000".to_string());
488 variables.insert("HOSTNAME".to_string(), hostname_val.clone());
489
490 let version = env!("CARGO_PKG_VERSION");
492 let parts: Vec<&str> = version.split('.').collect();
493 let mut bash_versinfo = HashMap::new();
494 bash_versinfo.insert(0, parts.first().unwrap_or(&"0").to_string());
495 bash_versinfo.insert(1, parts.get(1).unwrap_or(&"0").to_string());
496 bash_versinfo.insert(2, parts.get(2).unwrap_or(&"0").to_string());
497 bash_versinfo.insert(3, "0".to_string());
498 bash_versinfo.insert(4, "release".to_string());
499 bash_versinfo.insert(5, "virtual".to_string());
500
501 let mut arrays = HashMap::new();
502 arrays.insert("BASH_VERSINFO".to_string(), bash_versinfo);
503
504 Self {
505 fs,
506 env: HashMap::new(),
507 variables,
508 arrays,
509 assoc_arrays: HashMap::new(),
510 cwd: PathBuf::from("/home/user"),
511 last_exit_code: 0,
512 builtins,
513 functions: HashMap::new(),
514 call_stack: Vec::new(),
515 limits: ExecutionLimits::default(),
516 counters: ExecutionCounters::new(),
517 jobs: JobTable::new(),
518 options: ShellOptions::default(),
519 current_line: 1,
520 #[cfg(feature = "http_client")]
521 http_client: None,
522 #[cfg(feature = "git")]
523 git_client: None,
524 pipeline_stdin: None,
525 output_callback: None,
526 output_emit_count: 0,
527 nounset_error: None,
528 traps: HashMap::new(),
529 pipestatus: Vec::new(),
530 aliases: HashMap::new(),
531 expanding_aliases: HashSet::new(),
532 }
533 }
534
535 #[allow(dead_code)]
537 pub fn options_mut(&mut self) -> &mut ShellOptions {
538 &mut self.options
539 }
540
541 #[allow(dead_code)]
543 pub fn options(&self) -> &ShellOptions {
544 &self.options
545 }
546
547 fn is_errexit_enabled(&self) -> bool {
551 self.options.errexit
552 || self
553 .variables
554 .get("SHOPT_e")
555 .map(|v| v == "1")
556 .unwrap_or(false)
557 }
558
559 fn is_xtrace_enabled(&self) -> bool {
561 self.options.xtrace
562 || self
563 .variables
564 .get("SHOPT_x")
565 .map(|v| v == "1")
566 .unwrap_or(false)
567 }
568
569 pub fn set_limits(&mut self, limits: ExecutionLimits) {
571 self.limits = limits;
572 }
573
574 pub fn set_env(&mut self, key: &str, value: &str) {
576 self.env.insert(key.to_string(), value.to_string());
577 }
578
579 pub fn set_var(&mut self, key: &str, value: &str) {
581 self.variables.insert(key.to_string(), value.to_string());
582 }
583
584 pub fn set_cwd(&mut self, cwd: PathBuf) {
586 self.cwd = cwd;
587 }
588
589 pub fn shell_state(&self) -> ShellState {
591 ShellState {
592 env: self.env.clone(),
593 variables: self.variables.clone(),
594 arrays: self.arrays.clone(),
595 assoc_arrays: self.assoc_arrays.clone(),
596 cwd: self.cwd.clone(),
597 last_exit_code: self.last_exit_code,
598 aliases: self.aliases.clone(),
599 traps: self.traps.clone(),
600 errexit: self.options.errexit,
601 xtrace: self.options.xtrace,
602 pipefail: self.options.pipefail,
603 }
604 }
605
606 pub fn restore_shell_state(&mut self, state: &ShellState) {
608 self.env = state.env.clone();
609 self.variables = state.variables.clone();
610 self.arrays = state.arrays.clone();
611 self.assoc_arrays = state.assoc_arrays.clone();
612 self.cwd = state.cwd.clone();
613 self.last_exit_code = state.last_exit_code;
614 self.aliases = state.aliases.clone();
615 self.traps = state.traps.clone();
616 self.options.errexit = state.errexit;
617 self.options.xtrace = state.xtrace;
618 self.options.pipefail = state.pipefail;
619 }
620
621 pub fn set_output_callback(&mut self, callback: OutputCallback) {
627 self.output_callback = Some(callback);
628 self.output_emit_count = 0;
629 }
630
631 pub fn clear_output_callback(&mut self) {
633 self.output_callback = None;
634 self.output_emit_count = 0;
635 }
636
637 fn maybe_emit_output(&mut self, stdout: &str, stderr: &str, emit_count_before: u64) -> bool {
644 if self.output_callback.is_none() {
645 return false;
646 }
647 if self.output_emit_count != emit_count_before {
649 return false;
650 }
651 if stdout.is_empty() && stderr.is_empty() {
652 return false;
653 }
654 if let Some(ref mut cb) = self.output_callback {
655 cb(stdout, stderr);
656 self.output_emit_count += 1;
657 }
658 true
659 }
660
661 #[cfg(feature = "http_client")]
665 pub fn set_http_client(&mut self, client: crate::network::HttpClient) {
666 self.http_client = Some(client);
667 }
668
669 #[cfg(feature = "git")]
673 pub fn set_git_client(&mut self, client: crate::git::GitClient) {
674 self.git_client = Some(client);
675 }
676
677 pub async fn execute(&mut self, script: &Script) -> Result<ExecResult> {
679 self.counters.reset_for_execution();
682
683 let mut stdout = String::new();
684 let mut stderr = String::new();
685 let mut exit_code = 0;
686
687 for command in &script.commands {
688 let emit_before = self.output_emit_count;
689 let result = self.execute_command(command).await?;
690 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
691 stdout.push_str(&result.stdout);
692 stderr.push_str(&result.stderr);
693 exit_code = result.exit_code;
694 self.last_exit_code = exit_code;
695
696 if result.control_flow != ControlFlow::None {
698 break;
699 }
700
701 if exit_code != 0 {
703 let suppressed = matches!(command, Command::List(_))
704 || matches!(command, Command::Pipeline(p) if p.negated);
705 if !suppressed {
706 self.run_err_trap(&mut stdout, &mut stderr).await;
707 }
708 }
709
710 if self.is_errexit_enabled() && exit_code != 0 {
714 let suppressed = matches!(command, Command::List(_))
715 || matches!(command, Command::Pipeline(p) if p.negated);
716 if !suppressed {
717 break;
718 }
719 }
720 }
721
722 if let Some(trap_cmd) = self.traps.get("EXIT").cloned() {
724 if let Ok(trap_script) = Parser::with_limits(
726 &trap_cmd,
727 self.limits.max_ast_depth,
728 self.limits.max_parser_operations,
729 )
730 .parse()
731 {
732 let emit_before = self.output_emit_count;
733 if let Ok(trap_result) = self.execute_command_sequence(&trap_script.commands).await
734 {
735 self.maybe_emit_output(&trap_result.stdout, &trap_result.stderr, emit_before);
736 stdout.push_str(&trap_result.stdout);
737 stderr.push_str(&trap_result.stderr);
738 }
739 }
740 }
741
742 Ok(ExecResult {
743 stdout,
744 stderr,
745 exit_code,
746 control_flow: ControlFlow::None,
747 })
748 }
749
750 fn command_line(command: &Command) -> usize {
752 match command {
753 Command::Simple(c) => c.span.line(),
754 Command::Pipeline(c) => c.span.line(),
755 Command::List(c) => c.span.line(),
756 Command::Compound(c, _) => match c {
757 CompoundCommand::If(cmd) => cmd.span.line(),
758 CompoundCommand::For(cmd) => cmd.span.line(),
759 CompoundCommand::ArithmeticFor(cmd) => cmd.span.line(),
760 CompoundCommand::While(cmd) => cmd.span.line(),
761 CompoundCommand::Until(cmd) => cmd.span.line(),
762 CompoundCommand::Case(cmd) => cmd.span.line(),
763 CompoundCommand::Select(cmd) => cmd.span.line(),
764 CompoundCommand::Time(cmd) => cmd.span.line(),
765 CompoundCommand::Subshell(_) | CompoundCommand::BraceGroup(_) => 1,
766 CompoundCommand::Arithmetic(_) | CompoundCommand::Conditional(_) => 1,
767 },
768 Command::Function(c) => c.span.line(),
769 }
770 }
771
772 fn execute_command<'a>(
773 &'a mut self,
774 command: &'a Command,
775 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ExecResult>> + Send + 'a>> {
776 Box::pin(async move {
777 self.current_line = Self::command_line(command);
779
780 #[cfg(feature = "failpoints")]
782 fail_point!("interp::execute_command", |action| {
783 match action.as_deref() {
784 Some("panic") => {
785 panic!("injected panic in execute_command");
787 }
788 Some("error") => {
789 return Err(Error::Execution("injected execution error".to_string()));
790 }
791 Some("exit_nonzero") => {
792 return Ok(ExecResult {
794 stdout: String::new(),
795 stderr: "injected failure".to_string(),
796 exit_code: 127,
797 control_flow: ControlFlow::None,
798 });
799 }
800 _ => {}
801 }
802 Ok(ExecResult::ok(String::new()))
803 });
804
805 self.counters.tick_command(&self.limits)?;
807
808 match command {
809 Command::Simple(simple) => self.execute_simple_command(simple, None).await,
810 Command::Pipeline(pipeline) => self.execute_pipeline(pipeline).await,
811 Command::List(list) => self.execute_list(list).await,
812 Command::Compound(compound, redirects) => {
813 let stdin = self.process_input_redirections(None, redirects).await?;
815 let prev_pipeline_stdin = if stdin.is_some() {
816 let prev = self.pipeline_stdin.take();
817 self.pipeline_stdin = stdin;
818 Some(prev)
819 } else {
820 None
821 };
822 let result = self.execute_compound(compound).await?;
823 if let Some(prev) = prev_pipeline_stdin {
824 self.pipeline_stdin = prev;
825 }
826 if redirects.is_empty() {
827 Ok(result)
828 } else {
829 self.apply_redirections(result, redirects).await
830 }
831 }
832 Command::Function(func_def) => {
833 self.functions
835 .insert(func_def.name.clone(), func_def.clone());
836 Ok(ExecResult::ok(String::new()))
837 }
838 }
839 })
840 }
841
842 async fn execute_compound(&mut self, compound: &CompoundCommand) -> Result<ExecResult> {
844 match compound {
845 CompoundCommand::If(if_cmd) => self.execute_if(if_cmd).await,
846 CompoundCommand::For(for_cmd) => self.execute_for(for_cmd).await,
847 CompoundCommand::ArithmeticFor(arith_for) => {
848 self.execute_arithmetic_for(arith_for).await
849 }
850 CompoundCommand::While(while_cmd) => self.execute_while(while_cmd).await,
851 CompoundCommand::Until(until_cmd) => self.execute_until(until_cmd).await,
852 CompoundCommand::Subshell(commands) => {
853 let saved_vars = self.variables.clone();
857 let saved_arrays = self.arrays.clone();
858 let saved_assoc = self.assoc_arrays.clone();
859 let saved_functions = self.functions.clone();
860 let saved_cwd = self.cwd.clone();
861 let saved_traps = self.traps.clone();
862 let saved_call_stack = self.call_stack.clone();
863 let saved_exit = self.last_exit_code;
864 let saved_options = self.options.clone();
865 let saved_aliases = self.aliases.clone();
866
867 let mut result = self.execute_command_sequence(commands).await;
868
869 if let Some(trap_cmd) = self.traps.get("EXIT").cloned() {
871 let parent_had_same = saved_traps.get("EXIT") == Some(&trap_cmd);
873 if !parent_had_same {
874 if let Ok(trap_script) = Parser::with_limits(
876 &trap_cmd,
877 self.limits.max_ast_depth,
878 self.limits.max_parser_operations,
879 )
880 .parse()
881 {
882 let emit_before = self.output_emit_count;
883 if let Ok(ref mut res) = result {
884 if let Ok(trap_result) =
885 self.execute_command_sequence(&trap_script.commands).await
886 {
887 self.maybe_emit_output(
888 &trap_result.stdout,
889 &trap_result.stderr,
890 emit_before,
891 );
892 res.stdout.push_str(&trap_result.stdout);
893 res.stderr.push_str(&trap_result.stderr);
894 }
895 }
896 }
897 }
898 }
899
900 self.variables = saved_vars;
901 self.arrays = saved_arrays;
902 self.assoc_arrays = saved_assoc;
903 self.functions = saved_functions;
904 self.cwd = saved_cwd;
905 self.traps = saved_traps;
906 self.call_stack = saved_call_stack;
907 self.last_exit_code = saved_exit;
908 self.options = saved_options;
909 self.aliases = saved_aliases;
910 result
911 }
912 CompoundCommand::BraceGroup(commands) => self.execute_command_sequence(commands).await,
913 CompoundCommand::Case(case_cmd) => self.execute_case(case_cmd).await,
914 CompoundCommand::Select(select_cmd) => self.execute_select(select_cmd).await,
915 CompoundCommand::Arithmetic(expr) => self.execute_arithmetic_command(expr).await,
916 CompoundCommand::Time(time_cmd) => self.execute_time(time_cmd).await,
917 CompoundCommand::Conditional(words) => self.execute_conditional(words).await,
918 }
919 }
920
921 async fn execute_if(&mut self, if_cmd: &IfCommand) -> Result<ExecResult> {
923 let condition_result = self.execute_condition_sequence(&if_cmd.condition).await?;
925
926 if condition_result.exit_code == 0 {
927 return self.execute_command_sequence(&if_cmd.then_branch).await;
929 }
930
931 for (elif_condition, elif_body) in &if_cmd.elif_branches {
933 let elif_result = self.execute_condition_sequence(elif_condition).await?;
934 if elif_result.exit_code == 0 {
935 return self.execute_command_sequence(elif_body).await;
936 }
937 }
938
939 if let Some(else_branch) = &if_cmd.else_branch {
941 return self.execute_command_sequence(else_branch).await;
942 }
943
944 Ok(ExecResult::ok(String::new()))
946 }
947
948 async fn execute_for(&mut self, for_cmd: &ForCommand) -> Result<ExecResult> {
950 if !Self::is_valid_var_name(&for_cmd.variable) {
952 return Ok(ExecResult::err(
953 format!("bash: `{}': not a valid identifier\n", for_cmd.variable),
954 1,
955 ));
956 }
957
958 let mut stdout = String::new();
959 let mut stderr = String::new();
960 let mut exit_code = 0;
961
962 let values: Vec<String> = if let Some(words) = &for_cmd.words {
964 let mut vals = Vec::new();
965 for w in words {
966 let fields = self.expand_word_to_fields(w).await?;
967
968 if w.quoted {
970 vals.extend(fields);
971 continue;
972 }
973
974 for expanded in fields {
975 let brace_expanded = self.expand_braces(&expanded);
976 for item in brace_expanded {
977 match self.expand_glob_item(&item).await {
978 Ok(items) => vals.extend(items),
979 Err(pat) => {
980 self.last_exit_code = 1;
981 return Ok(ExecResult::err(
982 format!("-bash: no match: {}\n", pat),
983 1,
984 ));
985 }
986 }
987 }
988 }
989 }
990 vals
991 } else {
992 self.call_stack
994 .last()
995 .map(|frame| frame.positional.clone())
996 .unwrap_or_default()
997 };
998
999 self.counters.reset_loop();
1001
1002 for value in values {
1003 self.counters.tick_loop(&self.limits)?;
1005
1006 self.set_variable(for_cmd.variable.clone(), value.clone());
1008
1009 let emit_before = self.output_emit_count;
1011 let result = self.execute_command_sequence(&for_cmd.body).await?;
1012 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
1013 stdout.push_str(&result.stdout);
1014 stderr.push_str(&result.stderr);
1015 exit_code = result.exit_code;
1016
1017 match result.control_flow {
1019 ControlFlow::Break(n) => {
1020 if n <= 1 {
1021 break;
1022 } else {
1023 return Ok(ExecResult {
1025 stdout,
1026 stderr,
1027 exit_code,
1028 control_flow: ControlFlow::Break(n - 1),
1029 });
1030 }
1031 }
1032 ControlFlow::Continue(n) => {
1033 if n <= 1 {
1034 continue;
1035 } else {
1036 return Ok(ExecResult {
1038 stdout,
1039 stderr,
1040 exit_code,
1041 control_flow: ControlFlow::Continue(n - 1),
1042 });
1043 }
1044 }
1045 ControlFlow::Return(code) => {
1046 return Ok(ExecResult {
1048 stdout,
1049 stderr,
1050 exit_code: code,
1051 control_flow: ControlFlow::Return(code),
1052 });
1053 }
1054 ControlFlow::None => {
1055 if self.is_errexit_enabled() && exit_code != 0 {
1057 return Ok(ExecResult {
1058 stdout,
1059 stderr,
1060 exit_code,
1061 control_flow: ControlFlow::None,
1062 });
1063 }
1064 }
1065 }
1066 }
1067
1068 Ok(ExecResult {
1069 stdout,
1070 stderr,
1071 exit_code,
1072 control_flow: ControlFlow::None,
1073 })
1074 }
1075
1076 async fn execute_select(&mut self, select_cmd: &SelectCommand) -> Result<ExecResult> {
1083 let mut stdout = String::new();
1084 let mut stderr = String::new();
1085 let mut exit_code = 0;
1086
1087 let mut values = Vec::new();
1089 for w in &select_cmd.words {
1090 let fields = self.expand_word_to_fields(w).await?;
1091 if w.quoted {
1092 values.extend(fields);
1093 } else {
1094 for expanded in fields {
1095 let brace_expanded = self.expand_braces(&expanded);
1096 for item in brace_expanded {
1097 match self.expand_glob_item(&item).await {
1098 Ok(items) => values.extend(items),
1099 Err(pat) => {
1100 self.last_exit_code = 1;
1101 return Ok(ExecResult::err(
1102 format!("-bash: no match: {}\n", pat),
1103 1,
1104 ));
1105 }
1106 }
1107 }
1108 }
1109 }
1110 }
1111
1112 if values.is_empty() {
1113 return Ok(ExecResult {
1114 stdout,
1115 stderr,
1116 exit_code,
1117 control_flow: ControlFlow::None,
1118 });
1119 }
1120
1121 let menu: String = values
1123 .iter()
1124 .enumerate()
1125 .map(|(i, v)| format!("{}) {}", i + 1, v))
1126 .collect::<Vec<_>>()
1127 .join("\n");
1128
1129 let ps3 = self
1130 .variables
1131 .get("PS3")
1132 .cloned()
1133 .unwrap_or_else(|| "#? ".to_string());
1134
1135 self.counters.reset_loop();
1137
1138 loop {
1139 self.counters.tick_loop(&self.limits)?;
1140
1141 stderr.push_str(&menu);
1143 stderr.push('\n');
1144 stderr.push_str(&ps3);
1145
1146 let line = if let Some(ref ps) = self.pipeline_stdin {
1148 if ps.is_empty() {
1149 stdout.push('\n');
1151 exit_code = 1;
1152 break;
1153 }
1154 let data = ps.clone();
1155 if let Some(newline_pos) = data.find('\n') {
1156 let line = data[..newline_pos].to_string();
1157 self.pipeline_stdin = Some(data[newline_pos + 1..].to_string());
1158 line
1159 } else {
1160 self.pipeline_stdin = Some(String::new());
1161 data
1162 }
1163 } else {
1164 stdout.push('\n');
1166 exit_code = 1;
1167 break;
1168 };
1169
1170 self.variables.insert("REPLY".to_string(), line.clone());
1172
1173 let selected = line
1175 .trim()
1176 .parse::<usize>()
1177 .ok()
1178 .and_then(|n| {
1179 if n >= 1 && n <= values.len() {
1180 Some(values[n - 1].clone())
1181 } else {
1182 None
1183 }
1184 })
1185 .unwrap_or_default();
1186
1187 self.variables.insert(select_cmd.variable.clone(), selected);
1188
1189 let emit_before = self.output_emit_count;
1191 let result = self.execute_command_sequence(&select_cmd.body).await?;
1192 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
1193 stdout.push_str(&result.stdout);
1194 stderr.push_str(&result.stderr);
1195 exit_code = result.exit_code;
1196
1197 match result.control_flow {
1199 ControlFlow::Break(n) => {
1200 if n <= 1 {
1201 break;
1202 } else {
1203 return Ok(ExecResult {
1204 stdout,
1205 stderr,
1206 exit_code,
1207 control_flow: ControlFlow::Break(n - 1),
1208 });
1209 }
1210 }
1211 ControlFlow::Continue(n) => {
1212 if n <= 1 {
1213 continue;
1214 } else {
1215 return Ok(ExecResult {
1216 stdout,
1217 stderr,
1218 exit_code,
1219 control_flow: ControlFlow::Continue(n - 1),
1220 });
1221 }
1222 }
1223 ControlFlow::Return(code) => {
1224 return Ok(ExecResult {
1225 stdout,
1226 stderr,
1227 exit_code: code,
1228 control_flow: ControlFlow::Return(code),
1229 });
1230 }
1231 ControlFlow::None => {}
1232 }
1233 }
1234
1235 Ok(ExecResult {
1236 stdout,
1237 stderr,
1238 exit_code,
1239 control_flow: ControlFlow::None,
1240 })
1241 }
1242
1243 async fn execute_arithmetic_for(
1245 &mut self,
1246 arith_for: &ArithmeticForCommand,
1247 ) -> Result<ExecResult> {
1248 let mut stdout = String::new();
1249 let mut stderr = String::new();
1250 let mut exit_code = 0;
1251
1252 if !arith_for.init.is_empty() {
1254 self.execute_arithmetic_with_side_effects(&arith_for.init);
1255 }
1256
1257 self.counters.reset_loop();
1259
1260 loop {
1261 self.counters.tick_loop(&self.limits)?;
1263
1264 if !arith_for.condition.is_empty() {
1266 let cond_result = self.evaluate_arithmetic(&arith_for.condition);
1267 if cond_result == 0 {
1268 break;
1269 }
1270 }
1271
1272 let emit_before = self.output_emit_count;
1274 let result = self.execute_command_sequence(&arith_for.body).await?;
1275 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
1276 stdout.push_str(&result.stdout);
1277 stderr.push_str(&result.stderr);
1278 exit_code = result.exit_code;
1279
1280 match result.control_flow {
1282 ControlFlow::Break(n) => {
1283 if n <= 1 {
1284 break;
1285 } else {
1286 return Ok(ExecResult {
1287 stdout,
1288 stderr,
1289 exit_code,
1290 control_flow: ControlFlow::Break(n - 1),
1291 });
1292 }
1293 }
1294 ControlFlow::Continue(n) => {
1295 if n > 1 {
1296 return Ok(ExecResult {
1297 stdout,
1298 stderr,
1299 exit_code,
1300 control_flow: ControlFlow::Continue(n - 1),
1301 });
1302 }
1303 }
1305 ControlFlow::Return(code) => {
1306 return Ok(ExecResult {
1307 stdout,
1308 stderr,
1309 exit_code: code,
1310 control_flow: ControlFlow::Return(code),
1311 });
1312 }
1313 ControlFlow::None => {
1314 if self.is_errexit_enabled() && exit_code != 0 {
1316 return Ok(ExecResult {
1317 stdout,
1318 stderr,
1319 exit_code,
1320 control_flow: ControlFlow::None,
1321 });
1322 }
1323 }
1324 }
1325
1326 if !arith_for.step.is_empty() {
1328 self.execute_arithmetic_with_side_effects(&arith_for.step);
1329 }
1330 }
1331
1332 Ok(ExecResult {
1333 stdout,
1334 stderr,
1335 exit_code,
1336 control_flow: ControlFlow::None,
1337 })
1338 }
1339
1340 async fn execute_conditional(&mut self, words: &[Word]) -> Result<ExecResult> {
1344 let mut expanded = Vec::new();
1346 for word in words {
1347 expanded.push(self.expand_word(word).await?);
1348 }
1349
1350 let result = self.evaluate_conditional(&expanded).await;
1351 let exit_code = if result { 0 } else { 1 };
1352 self.last_exit_code = exit_code;
1353
1354 Ok(ExecResult {
1355 stdout: String::new(),
1356 stderr: String::new(),
1357 exit_code,
1358 control_flow: ControlFlow::None,
1359 })
1360 }
1361
1362 fn evaluate_conditional<'a>(
1364 &'a mut self,
1365 args: &'a [String],
1366 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + 'a>> {
1367 Box::pin(async move {
1368 if args.is_empty() {
1369 return false;
1370 }
1371
1372 if args[0] == "!" {
1374 return !self.evaluate_conditional(&args[1..]).await;
1375 }
1376
1377 if args.first().map(|s| s.as_str()) == Some("(")
1379 && args.last().map(|s| s.as_str()) == Some(")")
1380 {
1381 return self.evaluate_conditional(&args[1..args.len() - 1]).await;
1382 }
1383
1384 for i in (0..args.len()).rev() {
1386 if args[i] == "&&" && i > 0 {
1387 return self.evaluate_conditional(&args[..i]).await
1388 && self.evaluate_conditional(&args[i + 1..]).await;
1389 }
1390 }
1391 for i in (0..args.len()).rev() {
1392 if args[i] == "||" && i > 0 {
1393 return self.evaluate_conditional(&args[..i]).await
1394 || self.evaluate_conditional(&args[i + 1..]).await;
1395 }
1396 }
1397
1398 match args.len() {
1399 1 => !args[0].is_empty(),
1400 2 => {
1401 let resolve = |p: &str| -> std::path::PathBuf {
1403 let path = std::path::Path::new(p);
1404 if path.is_absolute() {
1405 path.to_path_buf()
1406 } else {
1407 self.cwd.join(path)
1408 }
1409 };
1410 match args[0].as_str() {
1411 "-z" => args[1].is_empty(),
1412 "-n" => !args[1].is_empty(),
1413 "-e" | "-a" => self.fs.exists(&resolve(&args[1])).await.unwrap_or(false),
1414 "-f" => self
1415 .fs
1416 .stat(&resolve(&args[1]))
1417 .await
1418 .map(|m| m.file_type.is_file())
1419 .unwrap_or(false),
1420 "-d" => self
1421 .fs
1422 .stat(&resolve(&args[1]))
1423 .await
1424 .map(|m| m.file_type.is_dir())
1425 .unwrap_or(false),
1426 "-r" | "-w" | "-x" => {
1427 self.fs.exists(&resolve(&args[1])).await.unwrap_or(false)
1428 }
1429 "-s" => self
1430 .fs
1431 .stat(&resolve(&args[1]))
1432 .await
1433 .map(|m| m.size > 0)
1434 .unwrap_or(false),
1435 _ => !args[0].is_empty(),
1436 }
1437 }
1438 3 => {
1439 match args[1].as_str() {
1441 "=" | "==" => self.pattern_matches(&args[0], &args[2]),
1442 "!=" => !self.pattern_matches(&args[0], &args[2]),
1443 "<" => args[0] < args[2],
1444 ">" => args[0] > args[2],
1445 "-eq" => {
1446 args[0].parse::<i64>().unwrap_or(0)
1447 == args[2].parse::<i64>().unwrap_or(0)
1448 }
1449 "-ne" => {
1450 args[0].parse::<i64>().unwrap_or(0)
1451 != args[2].parse::<i64>().unwrap_or(0)
1452 }
1453 "-lt" => {
1454 args[0].parse::<i64>().unwrap_or(0)
1455 < args[2].parse::<i64>().unwrap_or(0)
1456 }
1457 "-le" => {
1458 args[0].parse::<i64>().unwrap_or(0)
1459 <= args[2].parse::<i64>().unwrap_or(0)
1460 }
1461 "-gt" => {
1462 args[0].parse::<i64>().unwrap_or(0)
1463 > args[2].parse::<i64>().unwrap_or(0)
1464 }
1465 "-ge" => {
1466 args[0].parse::<i64>().unwrap_or(0)
1467 >= args[2].parse::<i64>().unwrap_or(0)
1468 }
1469 "=~" => self.regex_match(&args[0], &args[2]),
1470 "-nt" => {
1471 let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
1472 let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
1473 match (lm, rm) {
1474 (Ok(l), Ok(r)) => l.modified > r.modified,
1475 (Ok(_), Err(_)) => true,
1476 _ => false,
1477 }
1478 }
1479 "-ot" => {
1480 let lm = self.fs.stat(std::path::Path::new(&args[0])).await;
1481 let rm = self.fs.stat(std::path::Path::new(&args[2])).await;
1482 match (lm, rm) {
1483 (Ok(l), Ok(r)) => l.modified < r.modified,
1484 (Err(_), Ok(_)) => true,
1485 _ => false,
1486 }
1487 }
1488 "-ef" => {
1489 let lp = crate::builtins::resolve_path(
1490 &std::path::PathBuf::from("/"),
1491 &args[0],
1492 );
1493 let rp = crate::builtins::resolve_path(
1494 &std::path::PathBuf::from("/"),
1495 &args[2],
1496 );
1497 lp == rp
1498 }
1499 _ => false,
1500 }
1501 }
1502 _ => false,
1503 }
1504 })
1505 }
1506
1507 fn regex_match(&mut self, string: &str, pattern: &str) -> bool {
1509 match regex::Regex::new(pattern) {
1510 Ok(re) => {
1511 if let Some(captures) = re.captures(string) {
1512 let mut rematch = HashMap::new();
1514 for (i, m) in captures.iter().enumerate() {
1515 rematch.insert(i, m.map(|m| m.as_str().to_string()).unwrap_or_default());
1516 }
1517 self.arrays.insert("BASH_REMATCH".to_string(), rematch);
1518 true
1519 } else {
1520 self.arrays.remove("BASH_REMATCH");
1521 false
1522 }
1523 }
1524 Err(_) => {
1525 self.arrays.remove("BASH_REMATCH");
1526 false
1527 }
1528 }
1529 }
1530
1531 async fn execute_arithmetic_command(&mut self, expr: &str) -> Result<ExecResult> {
1532 let result = self.execute_arithmetic_with_side_effects(expr);
1533 let exit_code = if result != 0 { 0 } else { 1 };
1534
1535 Ok(ExecResult {
1536 stdout: String::new(),
1537 stderr: String::new(),
1538 exit_code,
1539 control_flow: ControlFlow::None,
1540 })
1541 }
1542
1543 fn execute_arithmetic_with_side_effects(&mut self, expr: &str) -> i64 {
1545 let expr = expr.trim();
1546
1547 if expr.contains(',') {
1549 let parts: Vec<&str> = expr.split(',').collect();
1550 let mut result = 0;
1551 for part in parts {
1552 result = self.execute_arithmetic_with_side_effects(part.trim());
1553 }
1554 return result;
1555 }
1556
1557 if let Some(eq_pos) = expr.find('=') {
1559 let before_eq = &expr[..eq_pos];
1562 let before = before_eq.chars().last();
1563 let after = expr[eq_pos + 1..].chars().next();
1564
1565 if after != Some('=') && !matches!(before, Some('!' | '<' | '>' | '=')) {
1566 let lhs = expr[..eq_pos].trim();
1568 let rhs = expr[eq_pos + 1..].trim();
1569
1570 let (var_name, op, effective_rhs) = if lhs.ends_with('+')
1572 || lhs.ends_with('-')
1573 || lhs.ends_with('*')
1574 || lhs.ends_with('/')
1575 || lhs.ends_with('%')
1576 {
1577 let op = lhs.chars().last().unwrap();
1578 let name = lhs[..lhs.len() - 1].trim();
1579 (name, Some(op), rhs)
1580 } else {
1581 (lhs, None, rhs)
1582 };
1583
1584 let rhs_value = self.execute_arithmetic_with_side_effects(effective_rhs);
1585 let final_value = if let Some(op) = op {
1586 let current = self.evaluate_arithmetic(var_name);
1587 match op {
1589 '+' => current.wrapping_add(rhs_value),
1590 '-' => current.wrapping_sub(rhs_value),
1591 '*' => current.wrapping_mul(rhs_value),
1592 '/' => {
1593 if rhs_value != 0 && !(current == i64::MIN && rhs_value == -1) {
1594 current / rhs_value
1595 } else {
1596 0
1597 }
1598 }
1599 '%' => {
1600 if rhs_value != 0 && !(current == i64::MIN && rhs_value == -1) {
1601 current % rhs_value
1602 } else {
1603 0
1604 }
1605 }
1606 _ => rhs_value,
1607 }
1608 } else {
1609 rhs_value
1610 };
1611
1612 self.set_variable(var_name.to_string(), final_value.to_string());
1613 return final_value;
1614 }
1615 }
1616
1617 if let Some(stripped) = expr.strip_prefix("++") {
1619 let var_name = stripped.trim();
1620 let current = self.evaluate_arithmetic(var_name);
1621 let new_value = current + 1;
1622 self.set_variable(var_name.to_string(), new_value.to_string());
1623 return new_value;
1624 }
1625 if let Some(stripped) = expr.strip_prefix("--") {
1626 let var_name = stripped.trim();
1627 let current = self.evaluate_arithmetic(var_name);
1628 let new_value = current - 1;
1629 self.set_variable(var_name.to_string(), new_value.to_string());
1630 return new_value;
1631 }
1632
1633 if let Some(stripped) = expr.strip_suffix("++") {
1635 let var_name = stripped.trim();
1636 let current = self.evaluate_arithmetic(var_name);
1637 let new_value = current + 1;
1638 self.set_variable(var_name.to_string(), new_value.to_string());
1639 return current; }
1641 if let Some(stripped) = expr.strip_suffix("--") {
1642 let var_name = stripped.trim();
1643 let current = self.evaluate_arithmetic(var_name);
1644 let new_value = current - 1;
1645 self.set_variable(var_name.to_string(), new_value.to_string());
1646 return current; }
1648
1649 self.evaluate_arithmetic(expr)
1651 }
1652
1653 async fn execute_while(&mut self, while_cmd: &WhileCommand) -> Result<ExecResult> {
1655 let mut stdout = String::new();
1656 let mut stderr = String::new();
1657 let mut exit_code = 0;
1658
1659 self.counters.reset_loop();
1661
1662 loop {
1663 self.counters.tick_loop(&self.limits)?;
1665
1666 let emit_before_cond = self.output_emit_count;
1668 let condition_result = self
1669 .execute_condition_sequence(&while_cmd.condition)
1670 .await?;
1671 self.maybe_emit_output(
1673 &condition_result.stdout,
1674 &condition_result.stderr,
1675 emit_before_cond,
1676 );
1677 stdout.push_str(&condition_result.stdout);
1678 stderr.push_str(&condition_result.stderr);
1679 if condition_result.exit_code != 0 {
1680 break;
1681 }
1682
1683 let emit_before = self.output_emit_count;
1685 let result = self.execute_command_sequence(&while_cmd.body).await?;
1686 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
1687 stdout.push_str(&result.stdout);
1688 stderr.push_str(&result.stderr);
1689 exit_code = result.exit_code;
1690
1691 match result.control_flow {
1693 ControlFlow::Break(n) => {
1694 if n <= 1 {
1695 break;
1696 } else {
1697 return Ok(ExecResult {
1698 stdout,
1699 stderr,
1700 exit_code,
1701 control_flow: ControlFlow::Break(n - 1),
1702 });
1703 }
1704 }
1705 ControlFlow::Continue(n) => {
1706 if n <= 1 {
1707 continue;
1708 } else {
1709 return Ok(ExecResult {
1710 stdout,
1711 stderr,
1712 exit_code,
1713 control_flow: ControlFlow::Continue(n - 1),
1714 });
1715 }
1716 }
1717 ControlFlow::Return(code) => {
1718 return Ok(ExecResult {
1719 stdout,
1720 stderr,
1721 exit_code: code,
1722 control_flow: ControlFlow::Return(code),
1723 });
1724 }
1725 ControlFlow::None => {
1726 if self.is_errexit_enabled() && exit_code != 0 {
1728 return Ok(ExecResult {
1729 stdout,
1730 stderr,
1731 exit_code,
1732 control_flow: ControlFlow::None,
1733 });
1734 }
1735 }
1736 }
1737 }
1738
1739 Ok(ExecResult {
1740 stdout,
1741 stderr,
1742 exit_code,
1743 control_flow: ControlFlow::None,
1744 })
1745 }
1746
1747 async fn execute_until(&mut self, until_cmd: &UntilCommand) -> Result<ExecResult> {
1749 let mut stdout = String::new();
1750 let mut stderr = String::new();
1751 let mut exit_code = 0;
1752
1753 self.counters.reset_loop();
1755
1756 loop {
1757 self.counters.tick_loop(&self.limits)?;
1759
1760 let emit_before_cond = self.output_emit_count;
1762 let condition_result = self
1763 .execute_condition_sequence(&until_cmd.condition)
1764 .await?;
1765 self.maybe_emit_output(
1767 &condition_result.stdout,
1768 &condition_result.stderr,
1769 emit_before_cond,
1770 );
1771 stdout.push_str(&condition_result.stdout);
1772 stderr.push_str(&condition_result.stderr);
1773 if condition_result.exit_code == 0 {
1774 break;
1775 }
1776
1777 let emit_before = self.output_emit_count;
1779 let result = self.execute_command_sequence(&until_cmd.body).await?;
1780 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
1781 stdout.push_str(&result.stdout);
1782 stderr.push_str(&result.stderr);
1783 exit_code = result.exit_code;
1784
1785 match result.control_flow {
1787 ControlFlow::Break(n) => {
1788 if n <= 1 {
1789 break;
1790 } else {
1791 return Ok(ExecResult {
1792 stdout,
1793 stderr,
1794 exit_code,
1795 control_flow: ControlFlow::Break(n - 1),
1796 });
1797 }
1798 }
1799 ControlFlow::Continue(n) => {
1800 if n <= 1 {
1801 continue;
1802 } else {
1803 return Ok(ExecResult {
1804 stdout,
1805 stderr,
1806 exit_code,
1807 control_flow: ControlFlow::Continue(n - 1),
1808 });
1809 }
1810 }
1811 ControlFlow::Return(code) => {
1812 return Ok(ExecResult {
1813 stdout,
1814 stderr,
1815 exit_code: code,
1816 control_flow: ControlFlow::Return(code),
1817 });
1818 }
1819 ControlFlow::None => {
1820 if self.is_errexit_enabled() && exit_code != 0 {
1822 return Ok(ExecResult {
1823 stdout,
1824 stderr,
1825 exit_code,
1826 control_flow: ControlFlow::None,
1827 });
1828 }
1829 }
1830 }
1831 }
1832
1833 Ok(ExecResult {
1834 stdout,
1835 stderr,
1836 exit_code,
1837 control_flow: ControlFlow::None,
1838 })
1839 }
1840
1841 async fn execute_case(&mut self, case_cmd: &CaseCommand) -> Result<ExecResult> {
1843 use crate::parser::CaseTerminator;
1844 let word_value = self.expand_word(&case_cmd.word).await?;
1845
1846 let mut stdout = String::new();
1847 let mut stderr = String::new();
1848 let mut exit_code = 0;
1849 let mut fallthrough = false;
1850
1851 for case_item in &case_cmd.cases {
1852 let matched = if fallthrough {
1853 true
1854 } else {
1855 let mut m = false;
1856 for pattern in &case_item.patterns {
1857 let pattern_str = self.expand_word(pattern).await?;
1858 if self.pattern_matches(&word_value, &pattern_str) {
1859 m = true;
1860 break;
1861 }
1862 }
1863 m
1864 };
1865
1866 if matched {
1867 let r = self.execute_command_sequence(&case_item.commands).await?;
1868 stdout.push_str(&r.stdout);
1869 stderr.push_str(&r.stderr);
1870 exit_code = r.exit_code;
1871 match case_item.terminator {
1872 CaseTerminator::Break => {
1873 return Ok(ExecResult {
1874 stdout,
1875 stderr,
1876 exit_code,
1877 control_flow: r.control_flow,
1878 });
1879 }
1880 CaseTerminator::FallThrough => {
1881 fallthrough = true;
1882 }
1883 CaseTerminator::Continue => {
1884 fallthrough = false;
1885 }
1886 }
1887 }
1888 }
1889
1890 Ok(ExecResult {
1891 stdout,
1892 stderr,
1893 exit_code,
1894 control_flow: ControlFlow::None,
1895 })
1896 }
1897
1898 async fn execute_time(&mut self, time_cmd: &TimeCommand) -> Result<ExecResult> {
1904 use std::time::Instant;
1905
1906 let start = Instant::now();
1907
1908 let mut result = if let Some(cmd) = &time_cmd.command {
1910 self.execute_command(cmd).await?
1911 } else {
1912 ExecResult::ok(String::new())
1914 };
1915
1916 let elapsed = start.elapsed();
1917
1918 let total_secs = elapsed.as_secs_f64();
1920 let minutes = (total_secs / 60.0).floor() as u64;
1921 let seconds = total_secs % 60.0;
1922
1923 let timing = if time_cmd.posix_format {
1925 format!("real {:.2}\nuser 0.00\nsys 0.00\n", total_secs)
1927 } else {
1928 format!(
1930 "\nreal\t{}m{:.3}s\nuser\t0m0.000s\nsys\t0m0.000s\n",
1931 minutes, seconds
1932 )
1933 };
1934
1935 result.stderr.push_str(&timing);
1937
1938 Ok(result)
1939 }
1940
1941 async fn execute_timeout(
1955 &mut self,
1956 args: &[String],
1957 stdin: Option<String>,
1958 redirects: &[Redirect],
1959 ) -> Result<ExecResult> {
1960 use std::time::Duration;
1961 use tokio::time::timeout;
1962
1963 const MAX_TIMEOUT_SECONDS: u64 = 300; if args.is_empty() {
1966 return Ok(ExecResult::err(
1967 "timeout: missing operand\nUsage: timeout DURATION COMMAND [ARGS...]\n".to_string(),
1968 125,
1969 ));
1970 }
1971
1972 let mut preserve_status = false;
1974 let mut arg_idx = 0;
1975
1976 while arg_idx < args.len() {
1977 let arg = &args[arg_idx];
1978 match arg.as_str() {
1979 "--preserve-status" => {
1980 preserve_status = true;
1981 arg_idx += 1;
1982 }
1983 "-k" | "-s" => {
1984 arg_idx += 2;
1986 }
1987 s if s.starts_with('-')
1988 && !s.chars().nth(1).is_some_and(|c| c.is_ascii_digit()) =>
1989 {
1990 arg_idx += 1;
1992 }
1993 _ => break, }
1995 }
1996
1997 if arg_idx >= args.len() {
1998 return Ok(ExecResult::err(
1999 "timeout: missing operand\nUsage: timeout DURATION COMMAND [ARGS...]\n".to_string(),
2000 125,
2001 ));
2002 }
2003
2004 let duration_str = &args[arg_idx];
2006 let max_duration = Duration::from_secs(MAX_TIMEOUT_SECONDS);
2007 let duration = match Self::parse_timeout_duration(duration_str) {
2008 Some(d) => {
2009 if d > max_duration {
2011 max_duration
2012 } else {
2013 d
2014 }
2015 }
2016 None => {
2017 return Ok(ExecResult::err(
2018 format!("timeout: invalid time interval '{}'\n", duration_str),
2019 125,
2020 ));
2021 }
2022 };
2023
2024 arg_idx += 1;
2025
2026 if arg_idx >= args.len() {
2027 return Ok(ExecResult::err(
2028 "timeout: missing command\nUsage: timeout DURATION COMMAND [ARGS...]\n".to_string(),
2029 125,
2030 ));
2031 }
2032
2033 let cmd_name = &args[arg_idx];
2035 let cmd_args: Vec<String> = args[arg_idx + 1..].to_vec();
2036
2037 let inner_redirects = if let Some(ref stdin_data) = stdin {
2039 vec![Redirect {
2040 fd: None,
2041 kind: RedirectKind::HereString,
2042 target: Word::literal(stdin_data.trim_end_matches('\n').to_string()),
2043 }]
2044 } else {
2045 Vec::new()
2046 };
2047
2048 let inner_cmd = Command::Simple(SimpleCommand {
2050 name: Word::literal(cmd_name.clone()),
2051 args: cmd_args.iter().map(|s| Word::literal(s.clone())).collect(),
2052 redirects: inner_redirects,
2053 assignments: Vec::new(),
2054 span: Span::new(),
2055 });
2056
2057 let exec_future = self.execute_command(&inner_cmd);
2059 let result = match timeout(duration, exec_future).await {
2060 Ok(Ok(result)) => result,
2061 Ok(Err(e)) => return Err(e),
2062 Err(_) => {
2063 if preserve_status {
2065 ExecResult::err(String::new(), 124)
2070 } else {
2071 ExecResult::err(String::new(), 124)
2072 }
2073 }
2074 };
2075
2076 self.apply_redirections(result, redirects).await
2078 }
2079
2080 fn parse_timeout_duration(s: &str) -> Option<std::time::Duration> {
2082 use std::time::Duration;
2083
2084 let s = s.trim();
2085 if s.is_empty() {
2086 return None;
2087 }
2088
2089 let (num_str, multiplier) = if let Some(stripped) = s.strip_suffix('s') {
2091 (stripped, 1u64)
2092 } else if let Some(stripped) = s.strip_suffix('m') {
2093 (stripped, 60u64)
2094 } else if let Some(stripped) = s.strip_suffix('h') {
2095 (stripped, 3600u64)
2096 } else if let Some(stripped) = s.strip_suffix('d') {
2097 (stripped, 86400u64)
2098 } else {
2099 (s, 1u64) };
2101
2102 let seconds: f64 = num_str.parse().ok()?;
2104 if seconds < 0.0 {
2105 return None;
2106 }
2107
2108 let total_secs_f64 = seconds * multiplier as f64;
2109 Some(Duration::from_secs_f64(total_secs_f64))
2110 }
2111
2112 async fn execute_xargs(
2117 &mut self,
2118 args: &[String],
2119 stdin: Option<String>,
2120 redirects: &[Redirect],
2121 ) -> Result<ExecResult> {
2122 let mut replace_str: Option<String> = None;
2123 let mut max_args: Option<usize> = None;
2124 let mut delimiter: Option<char> = None;
2125 let mut command: Vec<String> = Vec::new();
2126
2127 let mut i = 0;
2129 while i < args.len() {
2130 let arg = &args[i];
2131 match arg.as_str() {
2132 "-I" => {
2133 i += 1;
2134 if i >= args.len() {
2135 return Ok(ExecResult::err(
2136 "xargs: option requires an argument -- 'I'\n".to_string(),
2137 1,
2138 ));
2139 }
2140 replace_str = Some(args[i].clone());
2141 max_args = Some(1); }
2143 "-n" => {
2144 i += 1;
2145 if i >= args.len() {
2146 return Ok(ExecResult::err(
2147 "xargs: option requires an argument -- 'n'\n".to_string(),
2148 1,
2149 ));
2150 }
2151 match args[i].parse::<usize>() {
2152 Ok(n) if n > 0 => max_args = Some(n),
2153 _ => {
2154 return Ok(ExecResult::err(
2155 format!("xargs: invalid number: '{}'\n", args[i]),
2156 1,
2157 ));
2158 }
2159 }
2160 }
2161 "-d" => {
2162 i += 1;
2163 if i >= args.len() {
2164 return Ok(ExecResult::err(
2165 "xargs: option requires an argument -- 'd'\n".to_string(),
2166 1,
2167 ));
2168 }
2169 delimiter = args[i].chars().next();
2170 }
2171 "-0" => {
2172 delimiter = Some('\0');
2173 }
2174 s if s.starts_with('-') && s != "-" => {
2175 return Ok(ExecResult::err(
2176 format!("xargs: invalid option -- '{}'\n", &s[1..]),
2177 1,
2178 ));
2179 }
2180 _ => {
2181 command.extend(args[i..].iter().cloned());
2183 break;
2184 }
2185 }
2186 i += 1;
2187 }
2188
2189 if command.is_empty() {
2191 command.push("echo".to_string());
2192 }
2193
2194 let input = stdin.as_deref().unwrap_or("");
2196 if input.is_empty() {
2197 let result = ExecResult::ok(String::new());
2198 return self.apply_redirections(result, redirects).await;
2199 }
2200
2201 let items: Vec<&str> = if let Some(delim) = delimiter {
2203 input.split(delim).filter(|s| !s.is_empty()).collect()
2204 } else {
2205 input.split_whitespace().collect()
2206 };
2207
2208 if items.is_empty() {
2209 let result = ExecResult::ok(String::new());
2210 return self.apply_redirections(result, redirects).await;
2211 }
2212
2213 let mut combined_stdout = String::new();
2214 let mut combined_stderr = String::new();
2215 let mut last_exit_code = 0;
2216
2217 let chunk_size = max_args.unwrap_or(items.len());
2219 let chunks: Vec<Vec<&str>> = items.chunks(chunk_size).map(|c| c.to_vec()).collect();
2220
2221 for chunk in chunks {
2222 let cmd_args: Vec<String> = if let Some(ref repl) = replace_str {
2223 let item = chunk.first().unwrap_or(&"");
2225 command.iter().map(|arg| arg.replace(repl, item)).collect()
2226 } else {
2227 let mut full = command.clone();
2229 full.extend(chunk.iter().map(|s| s.to_string()));
2230 full
2231 };
2232
2233 let cmd_name = cmd_args[0].clone();
2235 let cmd_rest: Vec<Word> = cmd_args[1..]
2236 .iter()
2237 .map(|s| Word::literal(s.clone()))
2238 .collect();
2239
2240 let inner_cmd = Command::Simple(SimpleCommand {
2241 name: Word::literal(cmd_name),
2242 args: cmd_rest,
2243 redirects: Vec::new(),
2244 assignments: Vec::new(),
2245 span: Span::new(),
2246 });
2247
2248 let result = self.execute_command(&inner_cmd).await?;
2249 combined_stdout.push_str(&result.stdout);
2250 combined_stderr.push_str(&result.stderr);
2251 last_exit_code = result.exit_code;
2252 }
2253
2254 let mut result = ExecResult {
2255 stdout: combined_stdout,
2256 stderr: combined_stderr,
2257 exit_code: last_exit_code,
2258 control_flow: ControlFlow::None,
2259 };
2260
2261 result = self.apply_redirections(result, redirects).await?;
2262 Ok(result)
2263 }
2264
2265 async fn execute_find(
2273 &mut self,
2274 args: &[String],
2275 redirects: &[Redirect],
2276 ) -> Result<ExecResult> {
2277 let mut search_paths: Vec<String> = Vec::new();
2278 let mut name_pattern: Option<String> = None;
2279 let mut type_filter: Option<char> = None;
2280 let mut max_depth: Option<usize> = None;
2281 let mut exec_args: Vec<String> = Vec::new();
2282 let mut exec_batch = false;
2283
2284 let mut i = 0;
2286 while i < args.len() {
2287 let arg = &args[i];
2288 match arg.as_str() {
2289 "-name" => {
2290 i += 1;
2291 if i >= args.len() {
2292 return Ok(ExecResult::err(
2293 "find: missing argument to '-name'\n".to_string(),
2294 1,
2295 ));
2296 }
2297 name_pattern = Some(args[i].clone());
2298 }
2299 "-type" => {
2300 i += 1;
2301 if i >= args.len() {
2302 return Ok(ExecResult::err(
2303 "find: missing argument to '-type'\n".to_string(),
2304 1,
2305 ));
2306 }
2307 match args[i].as_str() {
2308 "f" | "d" | "l" => type_filter = Some(args[i].chars().next().unwrap()),
2309 t => {
2310 return Ok(ExecResult::err(format!("find: unknown type '{}'\n", t), 1));
2311 }
2312 }
2313 }
2314 "-maxdepth" => {
2315 i += 1;
2316 if i >= args.len() {
2317 return Ok(ExecResult::err(
2318 "find: missing argument to '-maxdepth'\n".to_string(),
2319 1,
2320 ));
2321 }
2322 match args[i].parse::<usize>() {
2323 Ok(n) => max_depth = Some(n),
2324 Err(_) => {
2325 return Ok(ExecResult::err(
2326 format!("find: invalid maxdepth value '{}'\n", args[i]),
2327 1,
2328 ));
2329 }
2330 }
2331 }
2332 "-print" | "-print0" => {}
2333 "-exec" | "-execdir" => {
2334 i += 1;
2335 while i < args.len() {
2336 let a = &args[i];
2337 if a == ";" || a == "\\;" {
2338 break;
2339 }
2340 if a == "+" {
2341 exec_batch = true;
2342 break;
2343 }
2344 exec_args.push(a.clone());
2345 i += 1;
2346 }
2347 }
2348 "-not" | "!" => {}
2349 s if s.starts_with('-') => {
2350 return Ok(ExecResult::err(
2351 format!("find: unknown predicate '{}'\n", s),
2352 1,
2353 ));
2354 }
2355 _ => {
2356 search_paths.push(arg.clone());
2357 }
2358 }
2359 i += 1;
2360 }
2361
2362 if search_paths.is_empty() {
2363 search_paths.push(".".to_string());
2364 }
2365
2366 let mut matched_paths: Vec<String> = Vec::new();
2368 for path_str in &search_paths {
2369 let path = self.resolve_path(path_str);
2370 if !self.fs.exists(&path).await.unwrap_or(false) {
2371 return Ok(ExecResult::err(
2372 format!("find: '{}': No such file or directory\n", path_str),
2373 1,
2374 ));
2375 }
2376 self.find_collect(
2377 &path,
2378 path_str,
2379 &name_pattern,
2380 type_filter,
2381 max_depth,
2382 0,
2383 &mut matched_paths,
2384 )
2385 .await?;
2386 }
2387
2388 if exec_args.is_empty() {
2390 let output =
2392 matched_paths.join("\n") + if matched_paths.is_empty() { "" } else { "\n" };
2393 let result = ExecResult::ok(output);
2394 return self.apply_redirections(result, redirects).await;
2395 }
2396
2397 let mut combined_stdout = String::new();
2398 let mut combined_stderr = String::new();
2399 let mut last_exit_code = 0;
2400
2401 if exec_batch {
2402 let cmd_args: Vec<String> = exec_args
2405 .iter()
2406 .flat_map(|arg| {
2407 if arg == "{}" {
2408 matched_paths.clone()
2409 } else {
2410 vec![arg.clone()]
2411 }
2412 })
2413 .collect();
2414
2415 if !cmd_args.is_empty() {
2416 let cmd_name = cmd_args[0].clone();
2417 let cmd_rest: Vec<Word> = cmd_args[1..]
2418 .iter()
2419 .map(|s| Word::literal(s.clone()))
2420 .collect();
2421
2422 let inner_cmd = Command::Simple(SimpleCommand {
2423 name: Word::literal(cmd_name),
2424 args: cmd_rest,
2425 redirects: Vec::new(),
2426 assignments: Vec::new(),
2427 span: Span::new(),
2428 });
2429
2430 let result = self.execute_command(&inner_cmd).await?;
2431 combined_stdout.push_str(&result.stdout);
2432 combined_stderr.push_str(&result.stderr);
2433 last_exit_code = result.exit_code;
2434 }
2435 } else {
2436 for found_path in &matched_paths {
2438 let cmd_args: Vec<String> = exec_args
2439 .iter()
2440 .map(|arg| arg.replace("{}", found_path))
2441 .collect();
2442
2443 if cmd_args.is_empty() {
2444 continue;
2445 }
2446
2447 let cmd_name = cmd_args[0].clone();
2448 let cmd_rest: Vec<Word> = cmd_args[1..]
2449 .iter()
2450 .map(|s| Word::literal(s.clone()))
2451 .collect();
2452
2453 let inner_cmd = Command::Simple(SimpleCommand {
2454 name: Word::literal(cmd_name),
2455 args: cmd_rest,
2456 redirects: Vec::new(),
2457 assignments: Vec::new(),
2458 span: Span::new(),
2459 });
2460
2461 let result = self.execute_command(&inner_cmd).await?;
2462 combined_stdout.push_str(&result.stdout);
2463 combined_stderr.push_str(&result.stderr);
2464 last_exit_code = result.exit_code;
2465 }
2466 }
2467
2468 let mut result = ExecResult {
2469 stdout: combined_stdout,
2470 stderr: combined_stderr,
2471 exit_code: last_exit_code,
2472 control_flow: ControlFlow::None,
2473 };
2474
2475 result = self.apply_redirections(result, redirects).await?;
2476 Ok(result)
2477 }
2478
2479 #[allow(clippy::too_many_arguments)]
2484 fn find_collect<'a>(
2485 &'a self,
2486 path: &'a Path,
2487 display_path: &'a str,
2488 name_pattern: &'a Option<String>,
2489 type_filter: Option<char>,
2490 max_depth: Option<usize>,
2491 current_depth: usize,
2492 results: &'a mut Vec<String>,
2493 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
2494 Box::pin(async move {
2495 use crate::builtins::glob_match;
2496
2497 let metadata = self.fs.stat(path).await?;
2498 let entry_name = Path::new(display_path)
2499 .file_name()
2500 .map(|s| s.to_string_lossy().to_string())
2501 .unwrap_or_else(|| display_path.to_string());
2502
2503 let type_matches = match type_filter {
2504 Some('f') => metadata.file_type.is_file(),
2505 Some('d') => metadata.file_type.is_dir(),
2506 Some('l') => metadata.file_type.is_symlink(),
2507 _ => true,
2508 };
2509
2510 let name_matches = match name_pattern {
2511 Some(pattern) => glob_match(&entry_name, pattern),
2512 None => true,
2513 };
2514
2515 if type_matches && name_matches {
2516 results.push(display_path.to_string());
2517 }
2518
2519 if metadata.file_type.is_dir() {
2520 if let Some(max) = max_depth {
2521 if current_depth >= max {
2522 return Ok(());
2523 }
2524 }
2525
2526 let entries = self.fs.read_dir(path).await?;
2527 let mut sorted_entries = entries;
2528 sorted_entries.sort_by(|a, b| a.name.cmp(&b.name));
2529
2530 for entry in sorted_entries {
2531 let child_path = path.join(&entry.name);
2532 let child_display = if display_path == "." {
2533 format!("./{}", entry.name)
2534 } else {
2535 format!("{}/{}", display_path, entry.name)
2536 };
2537
2538 self.find_collect(
2539 &child_path,
2540 &child_display,
2541 name_pattern,
2542 type_filter,
2543 max_depth,
2544 current_depth + 1,
2545 results,
2546 )
2547 .await?;
2548 }
2549 }
2550
2551 Ok(())
2552 })
2553 }
2554
2555 async fn execute_shell(
2567 &mut self,
2568 shell_name: &str,
2569 args: &[String],
2570 stdin: Option<String>,
2571 redirects: &[Redirect],
2572 ) -> Result<ExecResult> {
2573 let mut command_string: Option<String> = None;
2575 let mut script_file: Option<String> = None;
2576 let mut script_args: Vec<String> = Vec::new();
2577 let mut noexec = false; let mut shell_opts: Vec<(&str, &str)> = Vec::new();
2580 let mut idx = 0;
2581
2582 while idx < args.len() {
2583 let arg = &args[idx];
2584 match arg.as_str() {
2585 "--version" => {
2586 return Ok(ExecResult::ok(format!(
2588 "Bashkit {} (virtual {} interpreter)\n",
2589 env!("CARGO_PKG_VERSION"),
2590 shell_name
2591 )));
2592 }
2593 "--help" => {
2594 return Ok(ExecResult::ok(format!(
2595 "Usage: {} [option] ... [file [argument] ...]\n\
2596 Virtual shell interpreter (not GNU bash)\n\n\
2597 Options:\n\
2598 \t-c string\tExecute commands from string\n\
2599 \t-n\t\tCheck syntax without executing (noexec)\n\
2600 \t-e\t\tExit on error (errexit)\n\
2601 \t-x\t\tPrint commands before execution (xtrace)\n\
2602 \t-u\t\tError on unset variables (nounset)\n\
2603 \t-o option\tSet option by name\n\
2604 \t--version\tShow version\n\
2605 \t--help\t\tShow this help\n",
2606 shell_name
2607 )));
2608 }
2609 "-c" => {
2610 idx += 1;
2612 if idx >= args.len() {
2613 return Ok(ExecResult::err(
2614 format!("{}: -c: option requires an argument\n", shell_name),
2615 2,
2616 ));
2617 }
2618 command_string = Some(args[idx].clone());
2619 idx += 1;
2620 script_args = args[idx..].to_vec();
2622 break;
2623 }
2624 "-n" => {
2625 noexec = true;
2626 idx += 1;
2627 }
2628 "-e" => {
2629 shell_opts.push(("SHOPT_e", "1"));
2630 idx += 1;
2631 }
2632 "-x" => {
2633 shell_opts.push(("SHOPT_x", "1"));
2634 idx += 1;
2635 }
2636 "-u" => {
2637 shell_opts.push(("SHOPT_u", "1"));
2638 idx += 1;
2639 }
2640 "-v" => {
2641 shell_opts.push(("SHOPT_v", "1"));
2642 idx += 1;
2643 }
2644 "-f" => {
2645 shell_opts.push(("SHOPT_f", "1"));
2646 idx += 1;
2647 }
2648 "-o" => {
2649 idx += 1;
2650 if idx >= args.len() {
2651 return Ok(ExecResult::err(
2652 format!("{}: -o: option requires an argument\n", shell_name),
2653 2,
2654 ));
2655 }
2656 let opt = &args[idx];
2657 match opt.as_str() {
2658 "errexit" => shell_opts.push(("SHOPT_e", "1")),
2659 "nounset" => shell_opts.push(("SHOPT_u", "1")),
2660 "xtrace" => shell_opts.push(("SHOPT_x", "1")),
2661 "verbose" => shell_opts.push(("SHOPT_v", "1")),
2662 "pipefail" => shell_opts.push(("SHOPT_pipefail", "1")),
2663 "noglob" => shell_opts.push(("SHOPT_f", "1")),
2664 "noclobber" => shell_opts.push(("SHOPT_C", "1")),
2665 _ => {
2666 return Ok(ExecResult::err(
2667 format!("{}: set: {}: invalid option name\n", shell_name, opt),
2668 2,
2669 ));
2670 }
2671 }
2672 idx += 1;
2673 }
2674 "-i" | "-s" => {
2678 idx += 1;
2679 }
2680 "--" => {
2681 idx += 1;
2682 if idx < args.len() {
2684 script_file = Some(args[idx].clone());
2685 idx += 1;
2686 script_args = args[idx..].to_vec();
2687 }
2688 break;
2689 }
2690 s if s.starts_with("--") => {
2691 idx += 1;
2693 }
2694 s if s.starts_with('-') && s.len() > 1 => {
2695 let chars: Vec<char> = s.chars().skip(1).collect();
2697 let mut ci = 0;
2698 while ci < chars.len() {
2699 match chars[ci] {
2700 'n' => noexec = true,
2701 'e' => shell_opts.push(("SHOPT_e", "1")),
2702 'x' => shell_opts.push(("SHOPT_x", "1")),
2703 'u' => shell_opts.push(("SHOPT_u", "1")),
2704 'v' => shell_opts.push(("SHOPT_v", "1")),
2705 'f' => shell_opts.push(("SHOPT_f", "1")),
2706 'o' => {
2707 idx += 1;
2709 if idx < args.len() {
2710 match args[idx].as_str() {
2711 "errexit" => shell_opts.push(("SHOPT_e", "1")),
2712 "nounset" => shell_opts.push(("SHOPT_u", "1")),
2713 "xtrace" => shell_opts.push(("SHOPT_x", "1")),
2714 "verbose" => shell_opts.push(("SHOPT_v", "1")),
2715 "pipefail" => shell_opts.push(("SHOPT_pipefail", "1")),
2716 "noglob" => shell_opts.push(("SHOPT_f", "1")),
2717 "noclobber" => shell_opts.push(("SHOPT_C", "1")),
2718 _ => {}
2719 }
2720 }
2721 }
2722 _ => {} }
2724 ci += 1;
2725 }
2726 idx += 1;
2727 }
2728 _ => {
2729 script_file = Some(arg.clone());
2731 idx += 1;
2732 script_args = args[idx..].to_vec();
2734 break;
2735 }
2736 }
2737 }
2738
2739 let is_command_mode = command_string.is_some();
2741 let script_content = if let Some(cmd) = command_string {
2742 cmd
2744 } else if let Some(ref file) = script_file {
2745 let path = self.resolve_path(file);
2747 match self.fs.read_file(&path).await {
2748 Ok(content) => String::from_utf8_lossy(&content).to_string(),
2749 Err(_) => {
2750 return Ok(ExecResult::err(
2751 format!("{}: {}: No such file or directory\n", shell_name, file),
2752 127,
2753 ));
2754 }
2755 }
2756 } else if let Some(ref stdin_content) = stdin {
2757 stdin_content.clone()
2759 } else {
2760 return Ok(ExecResult::ok(String::new()));
2762 };
2763
2764 let parser = Parser::with_limits(
2766 &script_content,
2767 self.limits.max_ast_depth,
2768 self.limits.max_parser_operations,
2769 );
2770 let script = match parser.parse() {
2771 Ok(s) => s,
2772 Err(e) => {
2773 return Ok(ExecResult::err(
2774 format!("{}: syntax error: {}\n", shell_name, e),
2775 2,
2776 ));
2777 }
2778 };
2779
2780 if noexec {
2782 return Ok(ExecResult::ok(String::new()));
2783 }
2784
2785 let (name_arg, positional_args) = if is_command_mode {
2789 if script_args.is_empty() {
2791 (shell_name.to_string(), Vec::new())
2792 } else {
2793 let name = script_args[0].clone();
2794 let positional = script_args[1..].to_vec();
2795 (name, positional)
2796 }
2797 } else if let Some(ref file) = script_file {
2798 (file.clone(), script_args)
2800 } else {
2801 (shell_name.to_string(), Vec::new())
2803 };
2804
2805 self.call_stack.push(CallFrame {
2807 name: name_arg,
2808 locals: HashMap::new(),
2809 positional: positional_args,
2810 });
2811
2812 let mut saved_opts: Vec<(String, Option<String>)> = Vec::new();
2815 for (var, val) in &shell_opts {
2816 let prev = self.variables.get(*var).cloned();
2817 saved_opts.push((var.to_string(), prev));
2818 self.variables.insert(var.to_string(), val.to_string());
2819 }
2820 let saved_optind = self.variables.get("OPTIND").cloned();
2821 let saved_optchar = self.variables.get("_OPTCHAR_IDX").cloned();
2822 self.variables.insert("OPTIND".to_string(), "1".to_string());
2823 self.variables.remove("_OPTCHAR_IDX");
2824
2825 let result = self.execute(&script).await;
2827
2828 if let Some(val) = saved_optind {
2830 self.variables.insert("OPTIND".to_string(), val);
2831 } else {
2832 self.variables.remove("OPTIND");
2833 }
2834 if let Some(val) = saved_optchar {
2835 self.variables.insert("_OPTCHAR_IDX".to_string(), val);
2836 } else {
2837 self.variables.remove("_OPTCHAR_IDX");
2838 }
2839
2840 for (var, prev) in saved_opts {
2842 if let Some(val) = prev {
2843 self.variables.insert(var, val);
2844 } else {
2845 self.variables.remove(&var);
2846 }
2847 }
2848
2849 self.call_stack.pop();
2851
2852 match result {
2854 Ok(exec_result) => self.apply_redirections(exec_result, redirects).await,
2855 Err(e) => Err(e),
2856 }
2857 }
2858
2859 fn contains_extglob(&self, s: &str) -> bool {
2861 if !self.is_extglob() {
2862 return false;
2863 }
2864 let bytes = s.as_bytes();
2865 for i in 0..bytes.len().saturating_sub(1) {
2866 if matches!(bytes[i], b'@' | b'?' | b'*' | b'+' | b'!') && bytes[i + 1] == b'(' {
2867 return true;
2868 }
2869 }
2870 false
2871 }
2872
2873 fn pattern_matches(&self, value: &str, pattern: &str) -> bool {
2875 if pattern == "*" {
2877 return true;
2878 }
2879
2880 if pattern.contains('*')
2882 || pattern.contains('?')
2883 || pattern.contains('[')
2884 || self.contains_extglob(pattern)
2885 {
2886 self.glob_match(value, pattern)
2887 } else {
2888 value == pattern
2890 }
2891 }
2892
2893 fn glob_match(&self, value: &str, pattern: &str) -> bool {
2895 self.glob_match_impl(value, pattern, false, 0)
2896 }
2897
2898 fn parse_extglob_pattern_list(pattern: &str) -> Option<(Vec<String>, String)> {
2901 let mut depth = 1;
2902 let mut end = 0;
2903 let chars: Vec<char> = pattern.chars().collect();
2904 while end < chars.len() {
2905 match chars[end] {
2906 '(' => depth += 1,
2907 ')' => {
2908 depth -= 1;
2909 if depth == 0 {
2910 let inner: String = chars[..end].iter().collect();
2911 let rest: String = chars[end + 1..].iter().collect();
2912 let mut alts = Vec::new();
2914 let mut current = String::new();
2915 let mut d = 0;
2916 for c in inner.chars() {
2917 match c {
2918 '(' => {
2919 d += 1;
2920 current.push(c);
2921 }
2922 ')' => {
2923 d -= 1;
2924 current.push(c);
2925 }
2926 '|' if d == 0 => {
2927 alts.push(current.clone());
2928 current.clear();
2929 }
2930 _ => current.push(c),
2931 }
2932 }
2933 alts.push(current);
2934 return Some((alts, rest));
2935 }
2936 }
2937 '\\' => {
2938 end += 1; }
2940 _ => {}
2941 }
2942 end += 1;
2943 }
2944 None }
2946
2947 fn glob_match_impl(&self, value: &str, pattern: &str, nocase: bool, depth: usize) -> bool {
2949 if depth >= Self::MAX_GLOB_DEPTH {
2951 return false;
2952 }
2953
2954 let extglob = self.is_extglob();
2955
2956 if extglob && pattern.len() >= 2 {
2958 let bytes = pattern.as_bytes();
2959 if matches!(bytes[0], b'@' | b'?' | b'*' | b'+' | b'!') && bytes[1] == b'(' {
2960 let op = bytes[0];
2961 if let Some((alts, rest)) = Self::parse_extglob_pattern_list(&pattern[2..]) {
2962 return self.match_extglob(op, &alts, &rest, value, nocase, depth + 1);
2963 }
2964 }
2965 }
2966
2967 let mut value_chars = value.chars().peekable();
2968 let mut pattern_chars = pattern.chars().peekable();
2969
2970 loop {
2971 match (pattern_chars.peek().copied(), value_chars.peek().copied()) {
2972 (None, None) => return true,
2973 (None, Some(_)) => return false,
2974 (Some('*'), _) => {
2975 let mut pc_clone = pattern_chars.clone();
2977 pc_clone.next();
2978 if extglob && pc_clone.peek() == Some(&'(') {
2979 let remaining_pattern: String = pattern_chars.collect();
2981 let remaining_value: String = value_chars.collect();
2982 return self.glob_match_impl(
2983 &remaining_value,
2984 &remaining_pattern,
2985 nocase,
2986 depth + 1,
2987 );
2988 }
2989 pattern_chars.next();
2990 if pattern_chars.peek().is_none() {
2992 return true; }
2994 while value_chars.peek().is_some() {
2996 let remaining_value: String = value_chars.clone().collect();
2997 let remaining_pattern: String = pattern_chars.clone().collect();
2998 if self.glob_match_impl(
2999 &remaining_value,
3000 &remaining_pattern,
3001 nocase,
3002 depth + 1,
3003 ) {
3004 return true;
3005 }
3006 value_chars.next();
3007 }
3008 let remaining_pattern: String = pattern_chars.collect();
3010 return self.glob_match_impl("", &remaining_pattern, nocase, depth + 1);
3011 }
3012 (Some('?'), _) => {
3013 let mut pc_clone = pattern_chars.clone();
3015 pc_clone.next();
3016 if extglob && pc_clone.peek() == Some(&'(') {
3017 let remaining_pattern: String = pattern_chars.collect();
3018 let remaining_value: String = value_chars.collect();
3019 return self.glob_match_impl(
3020 &remaining_value,
3021 &remaining_pattern,
3022 nocase,
3023 depth + 1,
3024 );
3025 }
3026 if value_chars.peek().is_some() {
3027 pattern_chars.next();
3028 value_chars.next();
3029 } else {
3030 return false;
3031 }
3032 }
3033 (Some('['), Some(v)) => {
3034 pattern_chars.next(); let match_char = if nocase { v.to_ascii_lowercase() } else { v };
3036 if let Some(matched) =
3037 self.match_bracket_expr(&mut pattern_chars, match_char, nocase)
3038 {
3039 if matched {
3040 value_chars.next();
3041 } else {
3042 return false;
3043 }
3044 } else {
3045 return false;
3047 }
3048 }
3049 (Some('['), None) => return false,
3050 (Some(p), Some(v)) => {
3051 if extglob && matches!(p, '@' | '+' | '!') {
3053 let mut pc_clone = pattern_chars.clone();
3054 pc_clone.next();
3055 if pc_clone.peek() == Some(&'(') {
3056 let remaining_pattern: String = pattern_chars.collect();
3057 let remaining_value: String = value_chars.collect();
3058 return self.glob_match_impl(
3059 &remaining_value,
3060 &remaining_pattern,
3061 nocase,
3062 depth + 1,
3063 );
3064 }
3065 }
3066 let matches = if nocase {
3067 p.eq_ignore_ascii_case(&v)
3068 } else {
3069 p == v
3070 };
3071 if matches {
3072 pattern_chars.next();
3073 value_chars.next();
3074 } else {
3075 return false;
3076 }
3077 }
3078 (Some(_), None) => return false,
3079 }
3080 }
3081 }
3082
3083 fn match_extglob(
3088 &self,
3089 op: u8,
3090 alts: &[String],
3091 rest: &str,
3092 value: &str,
3093 nocase: bool,
3094 depth: usize,
3095 ) -> bool {
3096 if depth >= Self::MAX_GLOB_DEPTH {
3098 return false;
3099 }
3100
3101 match op {
3102 b'@' => {
3103 for alt in alts {
3105 let full = format!("{}{}", alt, rest);
3106 if self.glob_match_impl(value, &full, nocase, depth + 1) {
3107 return true;
3108 }
3109 }
3110 false
3111 }
3112 b'?' => {
3113 if self.glob_match_impl(value, rest, nocase, depth + 1) {
3116 return true;
3117 }
3118 for alt in alts {
3120 let full = format!("{}{}", alt, rest);
3121 if self.glob_match_impl(value, &full, nocase, depth + 1) {
3122 return true;
3123 }
3124 }
3125 false
3126 }
3127 b'+' => {
3128 for alt in alts {
3130 let full = format!("{}{}", alt, rest);
3131 if self.glob_match_impl(value, &full, nocase, depth + 1) {
3132 return true;
3133 }
3134 for split in 1..=value.len() {
3137 let prefix = &value[..split];
3138 let suffix = &value[split..];
3139 if self.glob_match_impl(prefix, alt, nocase, depth + 1) {
3140 let inner = alts.join("|");
3142 let re_pattern = format!("+({}){}", inner, rest);
3143 if self.glob_match_impl(suffix, &re_pattern, nocase, depth + 1) {
3144 return true;
3145 }
3146 }
3147 }
3148 }
3149 false
3150 }
3151 b'*' => {
3152 if self.glob_match_impl(value, rest, nocase, depth + 1) {
3155 return true;
3156 }
3157 for alt in alts {
3159 let full = format!("{}{}", alt, rest);
3160 if self.glob_match_impl(value, &full, nocase, depth + 1) {
3161 return true;
3162 }
3163 for split in 1..=value.len() {
3164 let prefix = &value[..split];
3165 let suffix = &value[split..];
3166 if self.glob_match_impl(prefix, alt, nocase, depth + 1) {
3167 let inner = alts.join("|");
3168 let re_pattern = format!("*({}){}", inner, rest);
3169 if self.glob_match_impl(suffix, &re_pattern, nocase, depth + 1) {
3170 return true;
3171 }
3172 }
3173 }
3174 }
3175 false
3176 }
3177 b'!' => {
3178 let inner = alts.join("|");
3182 let positive = format!("@({}){}", inner, rest);
3183 !self.glob_match_impl(value, &positive, nocase, depth + 1)
3184 && self.glob_match_impl(value, rest, nocase, depth + 1)
3185 || {
3186 for split in 1..=value.len() {
3188 let prefix = &value[..split];
3189 let suffix = &value[split..];
3190 let prefix_matches_any = alts
3192 .iter()
3193 .any(|a| self.glob_match_impl(prefix, a, nocase, depth + 1));
3194 if !prefix_matches_any
3195 && self.glob_match_impl(suffix, rest, nocase, depth + 1)
3196 {
3197 return true;
3198 }
3199 }
3200 false
3201 }
3202 }
3203 _ => false,
3204 }
3205 }
3206
3207 fn match_bracket_expr(
3210 &self,
3211 pattern_chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
3212 value_char: char,
3213 nocase: bool,
3214 ) -> Option<bool> {
3215 let mut chars_in_class = Vec::new();
3216 let mut negate = false;
3217
3218 if matches!(pattern_chars.peek(), Some('!') | Some('^')) {
3220 negate = true;
3221 pattern_chars.next();
3222 }
3223
3224 loop {
3226 match pattern_chars.next() {
3227 Some(']') if !chars_in_class.is_empty() => break,
3228 Some(']') if chars_in_class.is_empty() => {
3229 chars_in_class.push(']');
3231 }
3232 Some('-') if !chars_in_class.is_empty() => {
3233 if let Some(&next) = pattern_chars.peek() {
3235 if next == ']' {
3236 chars_in_class.push('-');
3238 } else {
3239 pattern_chars.next();
3241 if let Some(&prev) = chars_in_class.last() {
3242 for c in prev..=next {
3243 chars_in_class.push(c);
3244 }
3245 }
3246 }
3247 } else {
3248 return None; }
3250 }
3251 Some(c) => chars_in_class.push(c),
3252 None => return None, }
3254 }
3255
3256 let matched = if nocase {
3257 let lc = value_char.to_ascii_lowercase();
3258 chars_in_class.iter().any(|&c| c.to_ascii_lowercase() == lc)
3259 } else {
3260 chars_in_class.contains(&value_char)
3261 };
3262 Some(if negate { !matched } else { matched })
3263 }
3264
3265 async fn execute_command_sequence(&mut self, commands: &[Command]) -> Result<ExecResult> {
3267 self.execute_command_sequence_impl(commands, true).await
3268 }
3269
3270 async fn execute_condition_sequence(&mut self, commands: &[Command]) -> Result<ExecResult> {
3273 self.execute_command_sequence_impl(commands, false).await
3274 }
3275
3276 async fn execute_command_sequence_impl(
3278 &mut self,
3279 commands: &[Command],
3280 check_errexit: bool,
3281 ) -> Result<ExecResult> {
3282 let mut stdout = String::new();
3283 let mut stderr = String::new();
3284 let mut exit_code = 0;
3285
3286 for command in commands {
3287 let emit_before = self.output_emit_count;
3288 let result = self.execute_command(command).await?;
3289 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
3290 stdout.push_str(&result.stdout);
3291 stderr.push_str(&result.stderr);
3292 exit_code = result.exit_code;
3293 self.last_exit_code = exit_code;
3294
3295 if result.control_flow != ControlFlow::None {
3297 return Ok(ExecResult {
3298 stdout,
3299 stderr,
3300 exit_code,
3301 control_flow: result.control_flow,
3302 });
3303 }
3304
3305 if check_errexit && self.is_errexit_enabled() && exit_code != 0 {
3307 return Ok(ExecResult {
3308 stdout,
3309 stderr,
3310 exit_code,
3311 control_flow: ControlFlow::None,
3312 });
3313 }
3314 }
3315
3316 Ok(ExecResult {
3317 stdout,
3318 stderr,
3319 exit_code,
3320 control_flow: ControlFlow::None,
3321 })
3322 }
3323
3324 async fn execute_pipeline(&mut self, pipeline: &Pipeline) -> Result<ExecResult> {
3326 let mut stdin_data: Option<String> = None;
3327 let mut last_result = ExecResult::ok(String::new());
3328 let mut pipe_statuses = Vec::new();
3329
3330 for (i, command) in pipeline.commands.iter().enumerate() {
3331 let is_last = i == pipeline.commands.len() - 1;
3332
3333 let result = match command {
3334 Command::Simple(simple) => {
3335 self.execute_simple_command(simple, stdin_data.take())
3336 .await?
3337 }
3338 _ => {
3339 let prev_pipeline_stdin = self.pipeline_stdin.take();
3342 self.pipeline_stdin = stdin_data.take();
3343 let result = self.execute_command(command).await?;
3344 self.pipeline_stdin = prev_pipeline_stdin;
3345 result
3346 }
3347 };
3348
3349 pipe_statuses.push(result.exit_code);
3350
3351 if is_last {
3352 last_result = result;
3353 } else {
3354 stdin_data = Some(result.stdout);
3355 }
3356 }
3357
3358 self.pipestatus = pipe_statuses.clone();
3360 let mut ps_arr = HashMap::new();
3361 for (i, code) in pipe_statuses.iter().enumerate() {
3362 ps_arr.insert(i, code.to_string());
3363 }
3364 self.arrays.insert("PIPESTATUS".to_string(), ps_arr);
3365
3366 if self.is_pipefail() {
3368 if let Some(&nonzero) = pipe_statuses.iter().rev().find(|&&c| c != 0) {
3369 last_result.exit_code = nonzero;
3370 }
3371 }
3372
3373 if pipeline.negated {
3375 last_result.exit_code = if last_result.exit_code == 0 { 1 } else { 0 };
3376 }
3377
3378 Ok(last_result)
3379 }
3380
3381 async fn execute_list(&mut self, list: &CommandList) -> Result<ExecResult> {
3383 let mut stdout = String::new();
3384 let mut stderr = String::new();
3385 let mut exit_code;
3386 let emit_before = self.output_emit_count;
3387 let result = self.execute_command(&list.first).await?;
3388 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
3389 stdout.push_str(&result.stdout);
3390 stderr.push_str(&result.stderr);
3391 exit_code = result.exit_code;
3392 self.last_exit_code = exit_code;
3393 let mut control_flow = result.control_flow;
3394
3395 if control_flow != ControlFlow::None {
3397 return Ok(ExecResult {
3398 stdout,
3399 stderr,
3400 exit_code,
3401 control_flow,
3402 });
3403 }
3404
3405 let first_op_is_semicolon = list
3408 .rest
3409 .first()
3410 .is_some_and(|(op, _)| matches!(op, ListOperator::Semicolon));
3411 if exit_code != 0 && first_op_is_semicolon {
3412 self.run_err_trap(&mut stdout, &mut stderr).await;
3413 }
3414
3415 let has_conditional_operators = list
3418 .rest
3419 .iter()
3420 .any(|(op, _)| matches!(op, ListOperator::And | ListOperator::Or));
3421
3422 let mut just_exited_conditional_chain = false;
3424
3425 for (i, (op, cmd)) in list.rest.iter().enumerate() {
3426 let next_op = list.rest.get(i + 1).map(|(op, _)| op);
3428 let current_is_conditional = matches!(op, ListOperator::And | ListOperator::Or);
3429 let next_is_conditional =
3430 matches!(next_op, Some(ListOperator::And) | Some(ListOperator::Or));
3431
3432 let should_check_errexit = matches!(op, ListOperator::Semicolon)
3438 && !just_exited_conditional_chain
3439 && self.is_errexit_enabled()
3440 && exit_code != 0;
3441
3442 if should_check_errexit {
3443 return Ok(ExecResult {
3444 stdout,
3445 stderr,
3446 exit_code,
3447 control_flow: ControlFlow::None,
3448 });
3449 }
3450
3451 just_exited_conditional_chain = false;
3453
3454 if current_is_conditional && !next_is_conditional {
3457 just_exited_conditional_chain = true;
3458 }
3459
3460 let should_execute = match op {
3461 ListOperator::And => exit_code == 0,
3462 ListOperator::Or => exit_code != 0,
3463 ListOperator::Semicolon => true,
3464 ListOperator::Background => {
3465 true
3469 }
3470 };
3471
3472 if should_execute {
3473 let emit_before = self.output_emit_count;
3474 let result = self.execute_command(cmd).await?;
3475 self.maybe_emit_output(&result.stdout, &result.stderr, emit_before);
3476 stdout.push_str(&result.stdout);
3477 stderr.push_str(&result.stderr);
3478 exit_code = result.exit_code;
3479 self.last_exit_code = exit_code;
3480 control_flow = result.control_flow;
3481
3482 if control_flow != ControlFlow::None {
3484 return Ok(ExecResult {
3485 stdout,
3486 stderr,
3487 exit_code,
3488 control_flow,
3489 });
3490 }
3491
3492 if exit_code != 0 && !current_is_conditional {
3494 self.run_err_trap(&mut stdout, &mut stderr).await;
3495 }
3496 }
3497 }
3498
3499 let should_final_errexit_check =
3504 !has_conditional_operators && self.is_errexit_enabled() && exit_code != 0;
3505
3506 if should_final_errexit_check {
3507 return Ok(ExecResult {
3508 stdout,
3509 stderr,
3510 exit_code,
3511 control_flow: ControlFlow::None,
3512 });
3513 }
3514
3515 Ok(ExecResult {
3516 stdout,
3517 stderr,
3518 exit_code,
3519 control_flow: ControlFlow::None,
3520 })
3521 }
3522
3523 async fn execute_simple_command(
3524 &mut self,
3525 command: &SimpleCommand,
3526 stdin: Option<String>,
3527 ) -> Result<ExecResult> {
3528 let var_saves: Vec<(String, Option<String>)> = command
3532 .assignments
3533 .iter()
3534 .map(|a| (a.name.clone(), self.variables.get(&a.name).cloned()))
3535 .collect();
3536
3537 for assignment in &command.assignments {
3539 match &assignment.value {
3540 AssignmentValue::Scalar(word) => {
3541 let value = self.expand_word(word).await?;
3542 if let Some(index_str) = &assignment.index {
3543 let resolved_name = self.resolve_nameref(&assignment.name).to_string();
3545 if self.assoc_arrays.contains_key(&resolved_name) {
3546 let key = self.expand_variable_or_literal(index_str);
3548 let arr = self.assoc_arrays.entry(resolved_name).or_default();
3549 if assignment.append {
3550 let existing = arr.get(&key).cloned().unwrap_or_default();
3551 arr.insert(key, existing + &value);
3552 } else {
3553 arr.insert(key, value);
3554 }
3555 } else {
3556 let raw_idx = self.evaluate_arithmetic(index_str);
3558 let index = if raw_idx < 0 {
3559 let len = self
3560 .arrays
3561 .get(&resolved_name)
3562 .and_then(|a| a.keys().max().map(|m| m + 1))
3563 .unwrap_or(0) as i64;
3564 (len + raw_idx).max(0) as usize
3565 } else {
3566 raw_idx as usize
3567 };
3568 let arr = self.arrays.entry(resolved_name).or_default();
3569 if assignment.append {
3570 let existing = arr.get(&index).cloned().unwrap_or_default();
3571 arr.insert(index, existing + &value);
3572 } else {
3573 arr.insert(index, value);
3574 }
3575 }
3576 } else if assignment.append {
3577 let existing = self.expand_variable(&assignment.name);
3579 self.set_variable(assignment.name.clone(), existing + &value);
3580 } else {
3581 self.set_variable(assignment.name.clone(), value);
3582 }
3583 }
3584 AssignmentValue::Array(words) => {
3585 let mut expanded_values = Vec::new();
3591 for word in words.iter() {
3592 let has_command_subst = word
3593 .parts
3594 .iter()
3595 .any(|p| matches!(p, WordPart::CommandSubstitution(_)));
3596 let value = self.expand_word(word).await?;
3597 expanded_values.push((value, has_command_subst));
3598 }
3599
3600 let arr = self.arrays.entry(assignment.name.clone()).or_default();
3602
3603 let mut idx = if assignment.append {
3605 arr.keys().max().map(|k| k + 1).unwrap_or(0)
3606 } else {
3607 arr.clear();
3608 0
3609 };
3610
3611 for (value, has_command_subst) in expanded_values {
3612 if has_command_subst && !value.is_empty() {
3613 for part in value.split_whitespace() {
3615 arr.insert(idx, part.to_string());
3616 idx += 1;
3617 }
3618 } else if !value.is_empty() || !has_command_subst {
3619 arr.insert(idx, value);
3620 idx += 1;
3621 }
3622 }
3623 }
3624 }
3625 }
3626
3627 let name = self.expand_word(&command.name).await?;
3628
3629 if let Some(err_msg) = self.nounset_error.take() {
3631 for (name, old) in var_saves.into_iter().rev() {
3633 match old {
3634 Some(v) => {
3635 self.variables.insert(name, v);
3636 }
3637 None => {
3638 self.variables.remove(&name);
3639 }
3640 }
3641 }
3642 self.last_exit_code = 1;
3643 return Ok(ExecResult {
3644 stdout: String::new(),
3645 stderr: err_msg,
3646 exit_code: 1,
3647 control_flow: ControlFlow::Return(1),
3648 });
3649 }
3650
3651 let is_plain_literal = !command.name.quoted
3656 && command
3657 .name
3658 .parts
3659 .iter()
3660 .all(|p| matches!(p, WordPart::Literal(_)));
3661 if is_plain_literal
3662 && self.is_expand_aliases_enabled()
3663 && !self.expanding_aliases.contains(&name)
3664 {
3665 if let Some(expansion) = self.aliases.get(&name).cloned() {
3666 for (vname, old) in var_saves.into_iter().rev() {
3669 match old {
3670 Some(v) => {
3671 self.variables.insert(vname, v);
3672 }
3673 None => {
3674 self.variables.remove(&vname);
3675 }
3676 }
3677 }
3678
3679 let mut expanded_cmd = expansion.clone();
3683 let trailing_space = expanded_cmd.ends_with(' ');
3684 let mut args_iter = command.args.iter();
3685 if trailing_space {
3686 if let Some(first_arg) = args_iter.next() {
3687 let arg_str = format!("{}", first_arg);
3688 if let Some(arg_expansion) = self.aliases.get(&arg_str).cloned() {
3689 expanded_cmd.push_str(&arg_expansion);
3690 } else {
3691 expanded_cmd.push_str(&arg_str);
3692 }
3693 }
3694 }
3695 for word in args_iter {
3696 expanded_cmd.push(' ');
3697 expanded_cmd.push_str(&format!("{}", word));
3698 }
3699 for redir in &command.redirects {
3701 expanded_cmd.push(' ');
3702 expanded_cmd.push_str(&Self::format_redirect(redir));
3703 }
3704
3705 self.expanding_aliases.insert(name.clone());
3707
3708 let prev_pipeline_stdin = self.pipeline_stdin.take();
3710 if stdin.is_some() {
3711 self.pipeline_stdin = stdin;
3712 }
3713
3714 let parser = Parser::with_limits(
3716 &expanded_cmd,
3717 self.limits.max_ast_depth,
3718 self.limits.max_parser_operations,
3719 );
3720 let result = match parser.parse() {
3721 Ok(s) => self.execute(&s).await,
3722 Err(e) => Ok(ExecResult::err(
3723 format!("bash: alias expansion: parse error: {}\n", e),
3724 1,
3725 )),
3726 };
3727
3728 self.pipeline_stdin = prev_pipeline_stdin;
3729 self.expanding_aliases.remove(&name);
3730 return result;
3731 }
3732 }
3733
3734 if name.is_empty() {
3739 if command.name.quoted && command.assignments.is_empty() {
3740 self.last_exit_code = 127;
3742 return Ok(ExecResult::err(
3743 "bash: : command not found\n".to_string(),
3744 127,
3745 ));
3746 }
3747 return Ok(ExecResult {
3748 stdout: String::new(),
3749 stderr: String::new(),
3750 exit_code: self.last_exit_code,
3751 control_flow: crate::interpreter::ControlFlow::None,
3752 });
3753 }
3754
3755 let mut env_saves: Vec<(String, Option<String>)> = Vec::new();
3759 for assignment in &command.assignments {
3760 if assignment.index.is_none() {
3761 if let Some(value) = self.variables.get(&assignment.name).cloned() {
3762 let old = self.env.insert(assignment.name.clone(), value);
3763 env_saves.push((assignment.name.clone(), old));
3764 }
3765 }
3766 }
3767
3768 let xtrace_line = if self.is_xtrace_enabled() {
3770 let ps4 = self
3771 .variables
3772 .get("PS4")
3773 .cloned()
3774 .unwrap_or_else(|| "+ ".to_string());
3775 let mut trace = ps4;
3776 trace.push_str(&name);
3777 for word in &command.args {
3778 let expanded = self.expand_word(word).await.unwrap_or_default();
3779 trace.push(' ');
3780 if expanded.contains(' ') || expanded.contains('\t') || expanded.is_empty() {
3781 trace.push('\'');
3782 trace.push_str(&expanded.replace('\'', "'\\''"));
3783 trace.push('\'');
3784 } else {
3785 trace.push_str(&expanded);
3786 }
3787 }
3788 trace.push('\n');
3789 Some(trace)
3790 } else {
3791 None
3792 };
3793
3794 let result = self.execute_dispatched_command(&name, command, stdin).await;
3796
3797 for (name, old) in env_saves {
3799 match old {
3800 Some(v) => {
3801 self.env.insert(name, v);
3802 }
3803 None => {
3804 self.env.remove(&name);
3805 }
3806 }
3807 }
3808
3809 for (name, old) in var_saves {
3811 match old {
3812 Some(v) => {
3813 self.variables.insert(name, v);
3814 }
3815 None => {
3816 self.variables.remove(&name);
3817 }
3818 }
3819 }
3820
3821 if let Some(trace) = xtrace_line {
3824 result.map(|mut r| {
3825 r.stderr = trace + &r.stderr;
3826 r
3827 })
3828 } else {
3829 result
3830 }
3831 }
3832
3833 async fn execute_dispatched_command(
3838 &mut self,
3839 name: &str,
3840 command: &SimpleCommand,
3841 stdin: Option<String>,
3842 ) -> Result<ExecResult> {
3843 let mut args: Vec<String> = Vec::new();
3845 for word in &command.args {
3846 let fields = self.expand_word_to_fields(word).await?;
3848
3849 if word.quoted {
3851 args.extend(fields);
3852 continue;
3853 }
3854
3855 for expanded in fields {
3857 let brace_expanded = self.expand_braces(&expanded);
3859
3860 for item in brace_expanded {
3862 match self.expand_glob_item(&item).await {
3863 Ok(items) => args.extend(items),
3864 Err(pat) => {
3865 self.last_exit_code = 1;
3866 return Ok(ExecResult::err(format!("-bash: no match: {}\n", pat), 1));
3867 }
3868 }
3869 }
3870 }
3871 }
3872
3873 if let Some(err_msg) = self.nounset_error.take() {
3875 self.last_exit_code = 1;
3876 return Ok(ExecResult {
3877 stdout: String::new(),
3878 stderr: err_msg,
3879 exit_code: 1,
3880 control_flow: ControlFlow::Return(1),
3881 });
3882 }
3883
3884 let stdin = self
3886 .process_input_redirections(stdin, &command.redirects)
3887 .await?;
3888
3889 let stdin = if stdin.is_some() {
3892 stdin
3893 } else if let Some(ref ps) = self.pipeline_stdin {
3894 if !ps.is_empty() {
3895 if name == "read" {
3896 let data = ps.clone();
3898 if let Some(newline_pos) = data.find('\n') {
3899 let line = data[..=newline_pos].to_string();
3900 self.pipeline_stdin = Some(data[newline_pos + 1..].to_string());
3901 Some(line)
3902 } else {
3903 self.pipeline_stdin = Some(String::new());
3905 Some(data)
3906 }
3907 } else {
3908 Some(ps.clone())
3909 }
3910 } else {
3911 None
3912 }
3913 } else {
3914 None
3915 };
3916
3917 if let Some(func_def) = self.functions.get(name).cloned() {
3919 return self
3920 .execute_function_call(name, &func_def, args, stdin, &command.redirects)
3921 .await;
3922 }
3923
3924 if name == "local" {
3926 return self.execute_local_builtin(&args, &command.redirects).await;
3927 }
3928
3929 if name == "timeout" {
3931 return self.execute_timeout(&args, stdin, &command.redirects).await;
3932 }
3933
3934 if name == "xargs" {
3936 return self.execute_xargs(&args, stdin, &command.redirects).await;
3937 }
3938
3939 if name == "find" && args.iter().any(|a| a == "-exec" || a == "-execdir") {
3941 return self.execute_find(&args, &command.redirects).await;
3942 }
3943
3944 if name == "bash" || name == "sh" {
3946 return self
3947 .execute_shell(name, &args, stdin, &command.redirects)
3948 .await;
3949 }
3950
3951 if name == "source" || name == "." {
3954 return self.execute_source(&args, &command.redirects).await;
3955 }
3956
3957 if name == "eval" {
3958 return self.execute_eval(&args, stdin, &command.redirects).await;
3959 }
3960
3961 if name == "command" {
3963 return self
3964 .execute_command_builtin(&args, stdin, &command.redirects)
3965 .await;
3966 }
3967
3968 if name == "type" {
3970 return self.execute_type_builtin(&args, &command.redirects).await;
3971 }
3972 if name == "which" {
3973 return self.execute_which_builtin(&args, &command.redirects).await;
3974 }
3975 if name == "hash" {
3976 let mut result = ExecResult::ok(String::new());
3978 result = self.apply_redirections(result, &command.redirects).await?;
3979 return Ok(result);
3980 }
3981
3982 if name == "trap" {
3984 return self.execute_trap_builtin(&args, &command.redirects).await;
3985 }
3986
3987 if name == "declare" || name == "typeset" {
3989 return self
3990 .execute_declare_builtin(&args, &command.redirects)
3991 .await;
3992 }
3993
3994 if name == "let" {
3996 return self.execute_let_builtin(&args, &command.redirects).await;
3997 }
3998
3999 if name == "unset" {
4001 return self.execute_unset_builtin(&args, &command.redirects).await;
4002 }
4003
4004 if name == "getopts" {
4006 return self.execute_getopts(&args, &command.redirects).await;
4007 }
4008
4009 if name == "caller" {
4011 return self.execute_caller_builtin(&args, &command.redirects).await;
4012 }
4013
4014 if name == "mapfile" || name == "readarray" {
4016 return self.execute_mapfile(&args, stdin.as_deref()).await;
4017 }
4018
4019 if name == "alias" {
4021 return self.execute_alias_builtin(&args, &command.redirects).await;
4022 }
4023
4024 if name == "unalias" {
4026 return self
4027 .execute_unalias_builtin(&args, &command.redirects)
4028 .await;
4029 }
4030
4031 if let Some(builtin) = self.builtins.get(name) {
4033 let ctx = builtins::Context {
4034 args: &args,
4035 env: &self.env,
4036 variables: &mut self.variables,
4037 cwd: &mut self.cwd,
4038 fs: Arc::clone(&self.fs),
4039 stdin: stdin.as_deref(),
4040 #[cfg(feature = "http_client")]
4041 http_client: self.http_client.as_ref(),
4042 #[cfg(feature = "git")]
4043 git_client: self.git_client.as_ref(),
4044 };
4045
4046 let result = AssertUnwindSafe(builtin.execute(ctx)).catch_unwind().await;
4053
4054 let result = match result {
4055 Ok(Ok(exec_result)) => exec_result,
4056 Ok(Err(e)) => return Err(e),
4057 Err(_panic) => {
4058 ExecResult::err(format!("bash: {}: builtin failed unexpectedly\n", name), 1)
4064 }
4065 };
4066
4067 let markers: Vec<(String, String)> = self
4069 .variables
4070 .iter()
4071 .filter(|(k, _)| k.starts_with("_ARRAY_READ_"))
4072 .map(|(k, v)| (k.clone(), v.clone()))
4073 .collect();
4074 for (marker, value) in markers {
4075 let arr_name = marker.strip_prefix("_ARRAY_READ_").unwrap();
4076 let mut arr = HashMap::new();
4077 for (i, word) in value.split('\x1F').enumerate() {
4078 if !word.is_empty() {
4079 arr.insert(i, word.to_string());
4080 }
4081 }
4082 self.arrays.insert(arr_name.to_string(), arr);
4083 self.variables.remove(&marker);
4084 }
4085
4086 if let Some(shift_str) = self.variables.remove("_SHIFT_COUNT") {
4088 let n: usize = shift_str.parse().unwrap_or(1);
4089 if let Some(frame) = self.call_stack.last_mut() {
4090 if n <= frame.positional.len() {
4091 frame.positional.drain(..n);
4092 } else {
4093 frame.positional.clear();
4094 }
4095 }
4096 }
4097
4098 if let Some(encoded) = self.variables.remove("_SET_POSITIONAL") {
4101 let parts: Vec<&str> = encoded.splitn(2, '\x1F').collect();
4102 let count: usize = parts[0].parse().unwrap_or(0);
4103 let new_positional: Vec<String> = if count == 0 {
4104 Vec::new()
4105 } else if parts.len() > 1 {
4106 parts[1].split('\x1F').map(|s| s.to_string()).collect()
4107 } else {
4108 Vec::new()
4109 };
4110 if let Some(frame) = self.call_stack.last_mut() {
4111 frame.positional = new_positional;
4112 } else {
4113 self.call_stack.push(CallFrame {
4114 name: String::new(),
4115 locals: HashMap::new(),
4116 positional: new_positional,
4117 });
4118 }
4119 }
4120
4121 return self.apply_redirections(result, &command.redirects).await;
4123 }
4124
4125 if name.contains('/') {
4127 let result = self
4128 .try_execute_script_by_path(name, &args, &command.redirects)
4129 .await?;
4130 return Ok(result);
4131 }
4132
4133 if let Some(result) = self
4135 .try_execute_script_via_path_search(name, &args, &command.redirects)
4136 .await?
4137 {
4138 return Ok(result);
4139 }
4140
4141 let known: Vec<&str> = self
4143 .builtins
4144 .keys()
4145 .map(|s| s.as_str())
4146 .chain(self.functions.keys().map(|s| s.as_str()))
4147 .chain(self.aliases.keys().map(|s| s.as_str()))
4148 .collect();
4149 let msg = command_not_found_message(name, &known);
4150 Ok(ExecResult::err(msg, 127))
4151 }
4152
4153 async fn try_execute_script_by_path(
4162 &mut self,
4163 name: &str,
4164 args: &[String],
4165 redirects: &[Redirect],
4166 ) -> Result<ExecResult> {
4167 let path = self.resolve_path(name);
4168
4169 let meta = match self.fs.stat(&path).await {
4171 Ok(m) => m,
4172 Err(_) => {
4173 return Ok(ExecResult::err(
4174 format!("bash: {}: No such file or directory", name),
4175 127,
4176 ));
4177 }
4178 };
4179
4180 if meta.file_type.is_dir() {
4182 return Ok(ExecResult::err(
4183 format!("bash: {}: Is a directory", name),
4184 126,
4185 ));
4186 }
4187
4188 if meta.mode & 0o111 == 0 {
4190 return Ok(ExecResult::err(
4191 format!("bash: {}: Permission denied", name),
4192 126,
4193 ));
4194 }
4195
4196 let content = match self.fs.read_file(&path).await {
4198 Ok(c) => String::from_utf8_lossy(&c).to_string(),
4199 Err(_) => {
4200 return Ok(ExecResult::err(
4201 format!("bash: {}: No such file or directory", name),
4202 127,
4203 ));
4204 }
4205 };
4206
4207 self.execute_script_content(name, &content, args, redirects)
4208 .await
4209 }
4210
4211 async fn try_execute_script_via_path_search(
4215 &mut self,
4216 name: &str,
4217 args: &[String],
4218 redirects: &[Redirect],
4219 ) -> Result<Option<ExecResult>> {
4220 let path_var = self
4221 .variables
4222 .get("PATH")
4223 .or_else(|| self.env.get("PATH"))
4224 .cloned()
4225 .unwrap_or_default();
4226
4227 for dir in path_var.split(':') {
4228 if dir.is_empty() {
4229 continue;
4230 }
4231 let candidate = PathBuf::from(dir).join(name);
4232 if let Ok(meta) = self.fs.stat(&candidate).await {
4233 if meta.file_type.is_dir() {
4234 continue;
4235 }
4236 if meta.mode & 0o111 == 0 {
4237 continue;
4238 }
4239 if let Ok(content) = self.fs.read_file(&candidate).await {
4240 let script_text = String::from_utf8_lossy(&content).to_string();
4241 let result = self
4242 .execute_script_content(name, &script_text, args, redirects)
4243 .await?;
4244 return Ok(Some(result));
4245 }
4246 }
4247 }
4248
4249 Ok(None)
4250 }
4251
4252 async fn execute_script_content(
4257 &mut self,
4258 name: &str,
4259 content: &str,
4260 args: &[String],
4261 redirects: &[Redirect],
4262 ) -> Result<ExecResult> {
4263 let script_text = if content.starts_with("#!") {
4265 content
4266 .find('\n')
4267 .map(|pos| &content[pos + 1..])
4268 .unwrap_or("")
4269 } else {
4270 content
4271 };
4272
4273 let parser = Parser::with_limits(
4274 script_text,
4275 self.limits.max_ast_depth,
4276 self.limits.max_parser_operations,
4277 );
4278 let script = match parser.parse() {
4279 Ok(s) => s,
4280 Err(e) => {
4281 return Ok(ExecResult::err(format!("bash: {}: {}\n", name, e), 2));
4282 }
4283 };
4284
4285 self.call_stack.push(CallFrame {
4287 name: name.to_string(),
4288 locals: HashMap::new(),
4289 positional: args.to_vec(),
4290 });
4291
4292 let result = self.execute(&script).await;
4293
4294 self.call_stack.pop();
4296
4297 match result {
4298 Ok(mut exec_result) => {
4299 if let ControlFlow::Return(code) = exec_result.control_flow {
4301 exec_result.exit_code = code;
4302 exec_result.control_flow = ControlFlow::None;
4303 }
4304 self.apply_redirections(exec_result, redirects).await
4305 }
4306 Err(e) => Err(e),
4307 }
4308 }
4309
4310 async fn execute_source(
4318 &mut self,
4319 args: &[String],
4320 redirects: &[Redirect],
4321 ) -> Result<ExecResult> {
4322 let filename = match args.first() {
4323 Some(f) => f,
4324 None => {
4325 return Ok(ExecResult::err("source: filename argument required", 1));
4326 }
4327 };
4328
4329 let content = if filename.contains('/') {
4333 let path = self.resolve_path(filename);
4334 match self.fs.read_file(&path).await {
4335 Ok(c) => String::from_utf8_lossy(&c).to_string(),
4336 Err(_) => {
4337 return Ok(ExecResult::err(
4338 format!("source: {}: No such file or directory", filename),
4339 1,
4340 ));
4341 }
4342 }
4343 } else {
4344 let mut found = None;
4346 let path_var = self
4347 .variables
4348 .get("PATH")
4349 .or_else(|| self.env.get("PATH"))
4350 .cloned()
4351 .unwrap_or_default();
4352 for dir in path_var.split(':') {
4353 if dir.is_empty() {
4354 continue;
4355 }
4356 let candidate = PathBuf::from(dir).join(filename);
4357 if let Ok(c) = self.fs.read_file(&candidate).await {
4358 found = Some(String::from_utf8_lossy(&c).to_string());
4359 break;
4360 }
4361 }
4362 if found.is_none() {
4364 let path = self.resolve_path(filename);
4365 if let Ok(c) = self.fs.read_file(&path).await {
4366 found = Some(String::from_utf8_lossy(&c).to_string());
4367 }
4368 }
4369 match found {
4370 Some(c) => c,
4371 None => {
4372 return Ok(ExecResult::err(
4373 format!("source: {}: No such file or directory", filename),
4374 1,
4375 ));
4376 }
4377 }
4378 };
4379
4380 let parser = Parser::with_limits(
4382 &content,
4383 self.limits.max_ast_depth,
4384 self.limits.max_parser_operations,
4385 );
4386 let script = match parser.parse() {
4387 Ok(s) => s,
4388 Err(e) => {
4389 return Ok(ExecResult::err(
4390 format!("source: {}: parse error: {}", filename, e),
4391 1,
4392 ));
4393 }
4394 };
4395
4396 let source_args: Vec<String> = args[1..].to_vec();
4399 let has_source_args = !source_args.is_empty();
4400
4401 let saved_positional = if has_source_args {
4402 let saved = self.call_stack.last().map(|frame| frame.positional.clone());
4403 if self.call_stack.is_empty() {
4405 self.call_stack.push(CallFrame {
4406 name: filename.clone(),
4407 locals: HashMap::new(),
4408 positional: source_args,
4409 });
4410 } else if let Some(frame) = self.call_stack.last_mut() {
4411 frame.positional = source_args;
4412 }
4413 saved
4414 } else {
4415 None
4416 };
4417
4418 let mut result = self.execute(&script).await?;
4420
4421 if has_source_args {
4423 if let Some(saved) = saved_positional {
4424 if let Some(frame) = self.call_stack.last_mut() {
4425 frame.positional = saved;
4426 }
4427 } else {
4428 self.call_stack.pop();
4430 }
4431 }
4432
4433 result = self.apply_redirections(result, redirects).await?;
4435 Ok(result)
4436 }
4437
4438 async fn execute_eval(
4440 &mut self,
4441 args: &[String],
4442 stdin: Option<String>,
4443 redirects: &[Redirect],
4444 ) -> Result<ExecResult> {
4445 if args.is_empty() {
4446 return Ok(ExecResult::ok(String::new()));
4447 }
4448
4449 let cmd = args.join(" ");
4450 let parser = Parser::with_limits(
4452 &cmd,
4453 self.limits.max_ast_depth,
4454 self.limits.max_parser_operations,
4455 );
4456 let script = match parser.parse() {
4457 Ok(s) => s,
4458 Err(e) => {
4459 return Ok(ExecResult::err(format!("eval: parse error: {}", e), 1));
4460 }
4461 };
4462
4463 let prev_pipeline_stdin = self.pipeline_stdin.take();
4465 if stdin.is_some() {
4466 self.pipeline_stdin = stdin;
4467 }
4468
4469 let mut result = self.execute(&script).await?;
4470
4471 self.pipeline_stdin = prev_pipeline_stdin;
4472
4473 result = self.apply_redirections(result, redirects).await?;
4474 Ok(result)
4475 }
4476
4477 fn is_expand_aliases_enabled(&self) -> bool {
4479 self.variables
4480 .get("SHOPT_expand_aliases")
4481 .map(|v| v == "1")
4482 .unwrap_or(false)
4483 }
4484
4485 fn format_redirect(redir: &Redirect) -> String {
4487 let fd_prefix = redir.fd.map(|fd| fd.to_string()).unwrap_or_default();
4488 let op = match redir.kind {
4489 RedirectKind::Output => ">",
4490 RedirectKind::Append => ">>",
4491 RedirectKind::Input => "<",
4492 RedirectKind::HereDoc => "<<",
4493 RedirectKind::HereDocStrip => "<<-",
4494 RedirectKind::HereString => "<<<",
4495 RedirectKind::DupOutput => ">&",
4496 RedirectKind::DupInput => "<&",
4497 RedirectKind::OutputBoth => "&>",
4498 };
4499 format!("{}{}{}", fd_prefix, op, redir.target)
4500 }
4501
4502 async fn execute_function_call(
4504 &mut self,
4505 name: &str,
4506 func_def: &FunctionDef,
4507 args: Vec<String>,
4508 stdin: Option<String>,
4509 redirects: &[Redirect],
4510 ) -> Result<ExecResult> {
4511 self.counters.push_function(&self.limits)?;
4513
4514 self.call_stack.push(CallFrame {
4516 name: name.to_string(),
4517 locals: HashMap::new(),
4518 positional: args,
4519 });
4520
4521 let funcname_arr: HashMap<usize, String> = self
4523 .call_stack
4524 .iter()
4525 .rev()
4526 .enumerate()
4527 .map(|(i, f)| (i, f.name.clone()))
4528 .collect();
4529 let prev_funcname = self.arrays.insert("FUNCNAME".to_string(), funcname_arr);
4530
4531 let prev_pipeline_stdin = self.pipeline_stdin.take();
4533 self.pipeline_stdin = stdin;
4534
4535 let mut result = self.execute_command(&func_def.body).await?;
4537
4538 self.pipeline_stdin = prev_pipeline_stdin;
4540
4541 self.call_stack.pop();
4543 self.counters.pop_function();
4544
4545 if self.call_stack.is_empty() {
4547 self.arrays.remove("FUNCNAME");
4548 } else if let Some(prev) = prev_funcname {
4549 self.arrays.insert("FUNCNAME".to_string(), prev);
4550 }
4551
4552 if let ControlFlow::Return(code) = result.control_flow {
4554 result.exit_code = code;
4555 result.control_flow = ControlFlow::None;
4556 }
4557
4558 self.apply_redirections(result, redirects).await
4559 }
4560
4561 async fn execute_local_builtin(
4563 &mut self,
4564 args: &[String],
4565 redirects: &[Redirect],
4566 ) -> Result<ExecResult> {
4567 let mut is_nameref = false;
4569 let mut var_args: Vec<&String> = Vec::new();
4570 for arg in args {
4571 if arg.starts_with('-') && !arg.contains('=') {
4572 for c in arg[1..].chars() {
4573 if c == 'n' {
4574 is_nameref = true;
4575 }
4576 }
4577 } else {
4578 var_args.push(arg);
4579 }
4580 }
4581
4582 if let Some(frame) = self.call_stack.last_mut() {
4583 for arg in &var_args {
4585 if let Some(eq_pos) = arg.find('=') {
4586 let var_name = &arg[..eq_pos];
4587 let value = &arg[eq_pos + 1..];
4588 if !Self::is_valid_var_name(var_name) {
4589 let result = ExecResult::err(
4590 format!("local: `{}': not a valid identifier\n", arg),
4591 1,
4592 );
4593 return self.apply_redirections(result, redirects).await;
4594 }
4595 if is_internal_variable(var_name) {
4597 continue;
4598 }
4599 if is_nameref {
4600 frame.locals.insert(var_name.to_string(), String::new());
4601 } else {
4602 frame.locals.insert(var_name.to_string(), value.to_string());
4603 }
4604 } else if !is_internal_variable(arg) {
4605 frame.locals.insert(arg.to_string(), String::new());
4606 }
4607 }
4608 if is_nameref {
4610 for arg in &var_args {
4611 if let Some(eq_pos) = arg.find('=') {
4612 let var_name = &arg[..eq_pos];
4613 let value = &arg[eq_pos + 1..];
4614 if !is_internal_variable(var_name) {
4615 self.variables
4616 .insert(format!("_NAMEREF_{}", var_name), value.to_string());
4617 }
4618 }
4619 }
4620 }
4621 } else {
4622 for arg in &var_args {
4624 if let Some(eq_pos) = arg.find('=') {
4625 let var_name = &arg[..eq_pos];
4626 let value = &arg[eq_pos + 1..];
4627 if is_internal_variable(var_name) {
4629 continue;
4630 }
4631 if is_nameref {
4632 self.variables
4633 .insert(format!("_NAMEREF_{}", var_name), value.to_string());
4634 } else {
4635 self.variables
4636 .insert(var_name.to_string(), value.to_string());
4637 }
4638 } else if !is_internal_variable(arg) {
4639 self.variables.insert(arg.to_string(), String::new());
4640 }
4641 }
4642 }
4643 Ok(ExecResult::ok(String::new()))
4644 }
4645
4646 async fn execute_trap_builtin(
4648 &mut self,
4649 args: &[String],
4650 redirects: &[Redirect],
4651 ) -> Result<ExecResult> {
4652 if args.is_empty() {
4653 let mut output = String::new();
4655 let mut sorted: Vec<_> = self.traps.iter().collect();
4656 sorted.sort_by_key(|(sig, _)| (*sig).clone());
4657 for (sig, cmd) in sorted {
4658 output.push_str(&format!("trap -- '{}' {}\n", cmd, sig));
4659 }
4660 let result = ExecResult::ok(output);
4661 return self.apply_redirections(result, redirects).await;
4662 }
4663 if args[0] == "-p" {
4665 let mut output = String::new();
4666 if args.len() == 1 {
4667 let mut sorted: Vec<_> = self.traps.iter().collect();
4668 sorted.sort_by_key(|(sig, _)| (*sig).clone());
4669 for (sig, cmd) in sorted {
4670 output.push_str(&format!("trap -- '{}' {}\n", cmd, sig));
4671 }
4672 } else {
4673 for sig in &args[1..] {
4674 let sig_upper = sig.to_uppercase();
4675 if let Some(cmd) = self.traps.get(&sig_upper) {
4676 output.push_str(&format!("trap -- '{}' {}\n", cmd, sig_upper));
4677 }
4678 }
4679 }
4680 let result = ExecResult::ok(output);
4681 return self.apply_redirections(result, redirects).await;
4682 }
4683 if args.len() == 1 {
4684 let sig = args[0].to_uppercase();
4685 self.traps.remove(&sig);
4686 } else {
4687 let cmd = args[0].clone();
4688 for sig in &args[1..] {
4689 let sig_upper = sig.to_uppercase();
4690 if cmd == "-" {
4691 self.traps.remove(&sig_upper);
4692 } else {
4693 self.traps.insert(sig_upper, cmd.clone());
4694 }
4695 }
4696 }
4697 let result = ExecResult::ok(String::new());
4698 self.apply_redirections(result, redirects).await
4699 }
4700
4701 async fn execute_let_builtin(
4703 &mut self,
4704 args: &[String],
4705 redirects: &[Redirect],
4706 ) -> Result<ExecResult> {
4707 let mut last_val = 0i64;
4708 for arg in args {
4709 last_val = self.evaluate_arithmetic_with_assign(arg);
4710 }
4711 let exit_code = if last_val == 0 { 1 } else { 0 };
4712 let result = ExecResult {
4713 stdout: String::new(),
4714 stderr: String::new(),
4715 exit_code,
4716 control_flow: ControlFlow::None,
4717 };
4718 self.apply_redirections(result, redirects).await
4719 }
4720
4721 async fn execute_unset_builtin(
4723 &mut self,
4724 args: &[String],
4725 redirects: &[Redirect],
4726 ) -> Result<ExecResult> {
4727 let mut unset_nameref = false;
4728 let mut var_args: Vec<&String> = Vec::new();
4729 for arg in args {
4730 if arg == "-n" {
4731 unset_nameref = true;
4732 } else if arg == "-v" || arg == "-f" {
4733 } else {
4735 var_args.push(arg);
4736 }
4737 }
4738
4739 for arg in &var_args {
4740 if let Some(bracket) = arg.find('[') {
4741 if arg.ends_with(']') {
4742 let arr_name = &arg[..bracket];
4743 let key = &arg[bracket + 1..arg.len() - 1];
4744 let expanded_key = self.expand_variable_or_literal(key);
4745 let resolved_name = self.resolve_nameref(arr_name).to_string();
4746 if let Some(arr) = self.assoc_arrays.get_mut(&resolved_name) {
4747 arr.remove(&expanded_key);
4748 } else if let Some(arr) = self.arrays.get_mut(&resolved_name) {
4749 if let Ok(idx) = key.parse::<usize>() {
4750 arr.remove(&idx);
4751 }
4752 }
4753 continue;
4754 }
4755 }
4756 if unset_nameref {
4757 self.variables.remove(&format!("_NAMEREF_{}", arg));
4758 } else {
4759 let resolved = self.resolve_nameref(arg).to_string();
4760 self.variables.remove(&resolved);
4761 self.arrays.remove(&resolved);
4762 self.assoc_arrays.remove(&resolved);
4763 for frame in self.call_stack.iter_mut().rev() {
4764 frame.locals.remove(&resolved);
4765 }
4766 }
4767 }
4768 let result = ExecResult::ok(String::new());
4769 self.apply_redirections(result, redirects).await
4770 }
4771
4772 async fn execute_caller_builtin(
4774 &mut self,
4775 args: &[String],
4776 redirects: &[Redirect],
4777 ) -> Result<ExecResult> {
4778 let frame_num: usize = args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
4779 if self.call_stack.is_empty() {
4780 let result = ExecResult::err(String::new(), 1);
4781 return self.apply_redirections(result, redirects).await;
4782 }
4783 let source = "main";
4784 let line = 1;
4785 let output = if frame_num == 0 && self.call_stack.len() == 1 {
4786 format!("{} main {}\n", line, source)
4787 } else if frame_num + 1 < self.call_stack.len() {
4788 let idx = self.call_stack.len() - 2 - frame_num;
4789 let frame = &self.call_stack[idx];
4790 format!("{} {} {}\n", line, frame.name, source)
4791 } else if frame_num + 1 == self.call_stack.len() {
4792 format!("{} main {}\n", line, source)
4793 } else {
4794 let result = ExecResult::err(String::new(), 1);
4795 return self.apply_redirections(result, redirects).await;
4796 };
4797 let result = ExecResult::ok(output);
4798 self.apply_redirections(result, redirects).await
4799 }
4800
4801 async fn execute_alias_builtin(
4809 &mut self,
4810 args: &[String],
4811 redirects: &[Redirect],
4812 ) -> Result<ExecResult> {
4813 if args.is_empty() {
4814 let mut output = String::new();
4816 let mut sorted: Vec<_> = self.aliases.iter().collect();
4817 sorted.sort_by_key(|(k, _)| (*k).clone());
4818 for (name, value) in sorted {
4819 output.push_str(&format!("alias {}='{}'\n", name, value));
4820 }
4821 let result = ExecResult::ok(output);
4822 return self.apply_redirections(result, redirects).await;
4823 }
4824
4825 let mut output = String::new();
4826 let mut exit_code = 0;
4827 let mut stderr = String::new();
4828
4829 for arg in args {
4830 if let Some(eq_pos) = arg.find('=') {
4831 let name = &arg[..eq_pos];
4833 let value = &arg[eq_pos + 1..];
4834 self.aliases.insert(name.to_string(), value.to_string());
4835 } else {
4836 if let Some(value) = self.aliases.get(arg.as_str()) {
4838 output.push_str(&format!("alias {}='{}'\n", arg, value));
4839 } else {
4840 stderr.push_str(&format!("bash: alias: {}: not found\n", arg));
4841 exit_code = 1;
4842 }
4843 }
4844 }
4845
4846 let result = ExecResult {
4847 stdout: output,
4848 stderr,
4849 exit_code,
4850 control_flow: ControlFlow::None,
4851 };
4852 self.apply_redirections(result, redirects).await
4853 }
4854
4855 async fn execute_unalias_builtin(
4861 &mut self,
4862 args: &[String],
4863 redirects: &[Redirect],
4864 ) -> Result<ExecResult> {
4865 if args.is_empty() {
4866 let result = ExecResult::err(
4867 "bash: unalias: usage: unalias [-a] name [name ...]\n".to_string(),
4868 2,
4869 );
4870 return self.apply_redirections(result, redirects).await;
4871 }
4872
4873 let mut exit_code = 0;
4874 let mut stderr = String::new();
4875
4876 for arg in args {
4877 if arg == "-a" {
4878 self.aliases.clear();
4879 } else if self.aliases.remove(arg.as_str()).is_none() {
4880 stderr.push_str(&format!("bash: unalias: {}: not found\n", arg));
4881 exit_code = 1;
4882 }
4883 }
4884
4885 let result = ExecResult {
4886 stdout: String::new(),
4887 stderr,
4888 exit_code,
4889 control_flow: ControlFlow::None,
4890 };
4891 self.apply_redirections(result, redirects).await
4892 }
4893
4894 async fn execute_mapfile(
4899 &mut self,
4900 args: &[String],
4901 stdin_data: Option<&str>,
4902 ) -> Result<ExecResult> {
4903 let mut trim_trailing = false; let mut array_name = "MAPFILE".to_string();
4905 let mut positional = Vec::new();
4906
4907 for arg in args {
4908 match arg.as_str() {
4909 "-t" => trim_trailing = true,
4910 a if a.starts_with('-') => {} _ => positional.push(arg.clone()),
4912 }
4913 }
4914
4915 if let Some(name) = positional.first() {
4916 array_name = name.clone();
4917 }
4918
4919 let input = stdin_data.unwrap_or("");
4920
4921 self.arrays.remove(&array_name);
4923
4924 if !input.is_empty() {
4926 let mut arr = HashMap::new();
4927 for (idx, line) in input.lines().enumerate() {
4928 let value = if trim_trailing {
4929 line.to_string()
4930 } else {
4931 format!("{}\n", line)
4932 };
4933 arr.insert(idx, value);
4934 }
4935 if !arr.is_empty() {
4936 self.arrays.insert(array_name, arr);
4937 }
4938 }
4939
4940 Ok(ExecResult::ok(String::new()))
4941 }
4942
4943 async fn execute_getopts(
4951 &mut self,
4952 args: &[String],
4953 redirects: &[Redirect],
4954 ) -> Result<ExecResult> {
4955 if args.len() < 2 {
4956 let result = ExecResult::err("getopts: usage: getopts optstring name [arg ...]\n", 2);
4957 return Ok(result);
4958 }
4959
4960 let optstring = &args[0];
4961 let varname = &args[1];
4962
4963 let parse_args: Vec<String> = if args.len() > 2 {
4965 args[2..].to_vec()
4966 } else {
4967 self.call_stack
4969 .last()
4970 .map(|frame| frame.positional.clone())
4971 .unwrap_or_default()
4972 };
4973
4974 let optind: usize = self
4976 .variables
4977 .get("OPTIND")
4978 .and_then(|v| v.parse().ok())
4979 .unwrap_or(1);
4980
4981 if optind < 1 || optind > parse_args.len() {
4983 self.variables.insert(varname.clone(), "?".to_string());
4984 return Ok(ExecResult {
4985 stdout: String::new(),
4986 stderr: String::new(),
4987 exit_code: 1,
4988 control_flow: crate::interpreter::ControlFlow::None,
4989 });
4990 }
4991
4992 let current_arg = &parse_args[optind - 1];
4993
4994 if !current_arg.starts_with('-') || current_arg == "-" || current_arg == "--" {
4996 self.variables.insert(varname.clone(), "?".to_string());
4997 if current_arg == "--" {
4998 self.variables
4999 .insert("OPTIND".to_string(), (optind + 1).to_string());
5000 }
5001 return Ok(ExecResult {
5002 stdout: String::new(),
5003 stderr: String::new(),
5004 exit_code: 1,
5005 control_flow: crate::interpreter::ControlFlow::None,
5006 });
5007 }
5008
5009 let opt_chars: Vec<char> = current_arg[1..].chars().collect();
5012
5013 let char_idx: usize = self
5015 .variables
5016 .get("_OPTCHAR_IDX")
5017 .and_then(|v| v.parse().ok())
5018 .unwrap_or(0);
5019
5020 if char_idx >= opt_chars.len() {
5021 self.variables
5023 .insert("OPTIND".to_string(), (optind + 1).to_string());
5024 self.variables.remove("_OPTCHAR_IDX");
5025 self.variables.insert(varname.clone(), "?".to_string());
5026 return Ok(ExecResult {
5027 stdout: String::new(),
5028 stderr: String::new(),
5029 exit_code: 1,
5030 control_flow: crate::interpreter::ControlFlow::None,
5031 });
5032 }
5033
5034 let opt_char = opt_chars[char_idx];
5035 let silent = optstring.starts_with(':');
5036 let spec = if silent { &optstring[1..] } else { optstring };
5037
5038 if let Some(pos) = spec.find(opt_char) {
5040 let needs_arg = spec.get(pos + 1..pos + 2) == Some(":");
5041 self.variables.insert(varname.clone(), opt_char.to_string());
5042
5043 if needs_arg {
5044 if char_idx + 1 < opt_chars.len() {
5046 let arg_val: String = opt_chars[char_idx + 1..].iter().collect();
5048 self.variables.insert("OPTARG".to_string(), arg_val);
5049 self.variables
5050 .insert("OPTIND".to_string(), (optind + 1).to_string());
5051 self.variables.remove("_OPTCHAR_IDX");
5052 } else if optind < parse_args.len() {
5053 self.variables
5055 .insert("OPTARG".to_string(), parse_args[optind].clone());
5056 self.variables
5057 .insert("OPTIND".to_string(), (optind + 2).to_string());
5058 self.variables.remove("_OPTCHAR_IDX");
5059 } else {
5060 self.variables.remove("OPTARG");
5062 self.variables
5063 .insert("OPTIND".to_string(), (optind + 1).to_string());
5064 self.variables.remove("_OPTCHAR_IDX");
5065 if silent {
5066 self.variables.insert(varname.clone(), ":".to_string());
5067 self.variables
5068 .insert("OPTARG".to_string(), opt_char.to_string());
5069 } else {
5070 self.variables.insert(varname.clone(), "?".to_string());
5071 let mut result = ExecResult::ok(String::new());
5072 result.stderr = format!(
5073 "bash: getopts: option requires an argument -- '{}'\n",
5074 opt_char
5075 );
5076 result = self.apply_redirections(result, redirects).await?;
5077 return Ok(result);
5078 }
5079 }
5080 } else {
5081 self.variables.remove("OPTARG");
5083 if char_idx + 1 < opt_chars.len() {
5084 self.variables
5086 .insert("_OPTCHAR_IDX".to_string(), (char_idx + 1).to_string());
5087 } else {
5088 self.variables
5090 .insert("OPTIND".to_string(), (optind + 1).to_string());
5091 self.variables.remove("_OPTCHAR_IDX");
5092 }
5093 }
5094 } else {
5095 self.variables.remove("OPTARG");
5097 if char_idx + 1 < opt_chars.len() {
5098 self.variables
5099 .insert("_OPTCHAR_IDX".to_string(), (char_idx + 1).to_string());
5100 } else {
5101 self.variables
5102 .insert("OPTIND".to_string(), (optind + 1).to_string());
5103 self.variables.remove("_OPTCHAR_IDX");
5104 }
5105
5106 if silent {
5107 self.variables.insert(varname.clone(), "?".to_string());
5108 self.variables
5109 .insert("OPTARG".to_string(), opt_char.to_string());
5110 } else {
5111 self.variables.insert(varname.clone(), "?".to_string());
5112 let mut result = ExecResult::ok(String::new());
5113 result.stderr = format!("bash: getopts: illegal option -- '{}'\n", opt_char);
5114 result = self.apply_redirections(result, redirects).await?;
5115 return Ok(result);
5116 }
5117 }
5118
5119 let mut result = ExecResult::ok(String::new());
5120 result = self.apply_redirections(result, redirects).await?;
5121 Ok(result)
5122 }
5123
5124 async fn execute_command_builtin(
5130 &mut self,
5131 args: &[String],
5132 _stdin: Option<String>,
5133 redirects: &[Redirect],
5134 ) -> Result<ExecResult> {
5135 if args.is_empty() {
5136 return Ok(ExecResult::ok(String::new()));
5137 }
5138
5139 let mut mode = ' '; let mut cmd_args_start = 0;
5141
5142 let mut i = 0;
5144 while i < args.len() {
5145 let arg = &args[i];
5146 if arg == "-v" {
5147 mode = 'v';
5148 i += 1;
5149 } else if arg == "-V" {
5150 mode = 'V';
5151 i += 1;
5152 } else if arg == "-p" {
5153 i += 1;
5155 } else {
5156 cmd_args_start = i;
5157 break;
5158 }
5159 }
5160
5161 if cmd_args_start >= args.len() {
5162 return Ok(ExecResult::ok(String::new()));
5163 }
5164
5165 let cmd_name = &args[cmd_args_start];
5166
5167 match mode {
5168 'v' => {
5169 let found = self.builtins.contains_key(cmd_name.as_str())
5171 || self.functions.contains_key(cmd_name.as_str())
5172 || is_keyword(cmd_name);
5173 let mut result = if found {
5174 ExecResult::ok(format!("{}\n", cmd_name))
5175 } else {
5176 ExecResult {
5177 stdout: String::new(),
5178 stderr: String::new(),
5179 exit_code: 1,
5180 control_flow: crate::interpreter::ControlFlow::None,
5181 }
5182 };
5183 result = self.apply_redirections(result, redirects).await?;
5184 Ok(result)
5185 }
5186 'V' => {
5187 let description = if self.functions.contains_key(cmd_name.as_str()) {
5189 format!("{} is a function\n", cmd_name)
5190 } else if self.builtins.contains_key(cmd_name.as_str()) {
5191 format!("{} is a shell builtin\n", cmd_name)
5192 } else if is_keyword(cmd_name) {
5193 format!("{} is a shell keyword\n", cmd_name)
5194 } else {
5195 return Ok(ExecResult::err(
5196 format!("bash: command: {}: not found\n", cmd_name),
5197 1,
5198 ));
5199 };
5200 let mut result = ExecResult::ok(description);
5201 result = self.apply_redirections(result, redirects).await?;
5202 Ok(result)
5203 }
5204 _ => {
5205 let remaining = &args[cmd_args_start..];
5208 if let Some(builtin) = self.builtins.get(remaining[0].as_str()) {
5209 let builtin_args = &remaining[1..];
5210 let ctx = builtins::Context {
5211 args: builtin_args,
5212 env: &self.env,
5213 variables: &mut self.variables,
5214 cwd: &mut self.cwd,
5215 fs: Arc::clone(&self.fs),
5216 stdin: _stdin.as_deref(),
5217 #[cfg(feature = "http_client")]
5218 http_client: self.http_client.as_ref(),
5219 #[cfg(feature = "git")]
5220 git_client: self.git_client.as_ref(),
5221 };
5222 let mut result = builtin.execute(ctx).await?;
5223 result = self.apply_redirections(result, redirects).await?;
5224 Ok(result)
5225 } else {
5226 Ok(ExecResult::err(
5227 format!("bash: {}: command not found\n", remaining[0]),
5228 127,
5229 ))
5230 }
5231 }
5232 }
5233 }
5234
5235 async fn execute_type_builtin(
5242 &mut self,
5243 args: &[String],
5244 redirects: &[Redirect],
5245 ) -> Result<ExecResult> {
5246 if args.is_empty() {
5247 return Ok(ExecResult::err(
5248 "bash: type: usage: type [-afptP] name [name ...]\n".to_string(),
5249 1,
5250 ));
5251 }
5252
5253 let mut type_only = false; let mut path_only = false; let mut show_all = false; let mut names: Vec<&str> = Vec::new();
5257
5258 for arg in args {
5259 if arg.starts_with('-') && arg.len() > 1 {
5260 for c in arg[1..].chars() {
5261 match c {
5262 't' => type_only = true,
5263 'p' => path_only = true,
5264 'a' => show_all = true,
5265 'f' => {} 'P' => path_only = true,
5267 _ => {
5268 return Ok(ExecResult::err(
5269 format!(
5270 "bash: type: -{}: invalid option\ntype: usage: type [-afptP] name [name ...]\n",
5271 c
5272 ),
5273 1,
5274 ));
5275 }
5276 }
5277 }
5278 } else {
5279 names.push(arg);
5280 }
5281 }
5282
5283 let mut output = String::new();
5284 let mut all_found = true;
5285
5286 for name in &names {
5287 let is_func = self.functions.contains_key(*name);
5288 let is_builtin = self.builtins.contains_key(*name);
5289 let is_kw = is_keyword(name);
5290
5291 if type_only {
5292 if is_func {
5293 output.push_str("function\n");
5294 } else if is_kw {
5295 output.push_str("keyword\n");
5296 } else if is_builtin {
5297 output.push_str("builtin\n");
5298 } else {
5299 all_found = false;
5301 }
5302 } else if path_only {
5303 if !is_func && !is_builtin && !is_kw {
5305 all_found = false;
5306 }
5307 } else {
5309 let mut found_any = false;
5311 if is_func {
5312 output.push_str(&format!("{} is a function\n", name));
5313 found_any = true;
5314 if !show_all {
5315 continue;
5316 }
5317 }
5318 if is_kw {
5319 output.push_str(&format!("{} is a shell keyword\n", name));
5320 found_any = true;
5321 if !show_all {
5322 continue;
5323 }
5324 }
5325 if is_builtin {
5326 output.push_str(&format!("{} is a shell builtin\n", name));
5327 found_any = true;
5328 if !show_all {
5329 continue;
5330 }
5331 }
5332 if !found_any {
5333 output.push_str(&format!("bash: type: {}: not found\n", name));
5334 all_found = false;
5335 }
5336 }
5337 }
5338
5339 let exit_code = if all_found { 0 } else { 1 };
5340 let mut result = ExecResult {
5341 stdout: output,
5342 stderr: String::new(),
5343 exit_code,
5344 control_flow: ControlFlow::None,
5345 };
5346 result = self.apply_redirections(result, redirects).await?;
5347 Ok(result)
5348 }
5349
5350 async fn execute_which_builtin(
5355 &mut self,
5356 args: &[String],
5357 redirects: &[Redirect],
5358 ) -> Result<ExecResult> {
5359 let names: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
5360
5361 if names.is_empty() {
5362 return Ok(ExecResult::ok(String::new()));
5363 }
5364
5365 let mut output = String::new();
5366 let mut all_found = true;
5367
5368 for name in &names {
5369 if self.builtins.contains_key(*name)
5370 || self.functions.contains_key(*name)
5371 || is_keyword(name)
5372 {
5373 output.push_str(&format!("{}\n", name));
5374 } else {
5375 all_found = false;
5376 }
5377 }
5378
5379 let exit_code = if all_found { 0 } else { 1 };
5380 let mut result = ExecResult {
5381 stdout: output,
5382 stderr: String::new(),
5383 exit_code,
5384 control_flow: ControlFlow::None,
5385 };
5386 result = self.apply_redirections(result, redirects).await?;
5387 Ok(result)
5388 }
5389
5390 async fn execute_declare_builtin(
5399 &mut self,
5400 args: &[String],
5401 redirects: &[Redirect],
5402 ) -> Result<ExecResult> {
5403 if args.is_empty() {
5404 let mut output = String::new();
5406 let mut entries: Vec<_> = self.variables.iter().collect();
5407 entries.sort_by_key(|(k, _)| (*k).clone());
5408 for (name, value) in entries {
5409 if is_internal_variable(name) {
5410 continue;
5411 }
5412 output.push_str(&format!("declare -- {}=\"{}\"\n", name, value));
5413 }
5414 let mut result = ExecResult::ok(output);
5415 result = self.apply_redirections(result, redirects).await?;
5416 return Ok(result);
5417 }
5418
5419 let mut print_mode = false;
5420 let mut is_readonly = false;
5421 let mut is_export = false;
5422 let mut is_array = false;
5423 let mut is_assoc = false;
5424 let mut is_integer = false;
5425 let mut is_nameref = false;
5426 let mut remove_nameref = false;
5427 let mut is_lowercase = false;
5428 let mut is_uppercase = false;
5429 let mut names: Vec<&str> = Vec::new();
5430
5431 for arg in args {
5432 if arg.starts_with('-') && !arg.contains('=') {
5433 for c in arg[1..].chars() {
5434 match c {
5435 'p' => print_mode = true,
5436 'r' => is_readonly = true,
5437 'x' => is_export = true,
5438 'a' => is_array = true,
5439 'i' => is_integer = true,
5440 'A' => is_assoc = true,
5441 'n' => is_nameref = true,
5442 'l' => is_lowercase = true,
5443 'u' => is_uppercase = true,
5444 'g' | 't' | 'f' | 'F' => {} _ => {}
5446 }
5447 }
5448 } else if arg.starts_with('+') && !arg.contains('=') {
5449 for c in arg[1..].chars() {
5451 if c == 'n' {
5452 remove_nameref = true;
5453 }
5454 }
5455 } else {
5456 names.push(arg);
5457 }
5458 }
5459
5460 if print_mode {
5461 let mut output = String::new();
5462 if names.is_empty() {
5463 let mut entries: Vec<_> = self.variables.iter().collect();
5465 entries.sort_by_key(|(k, _)| (*k).clone());
5466 for (name, value) in entries {
5467 if is_internal_variable(name) {
5468 continue;
5469 }
5470 output.push_str(&format!("declare -- {}=\"{}\"\n", name, value));
5471 }
5472 } else {
5473 for name in &names {
5474 let var_name = name.split('=').next().unwrap_or(name);
5476 if let Some(value) = self.variables.get(var_name) {
5477 let mut attrs = String::from("--");
5478 if self
5479 .variables
5480 .contains_key(&format!("_READONLY_{}", var_name))
5481 {
5482 attrs = String::from("-r");
5483 }
5484 output.push_str(&format!("declare {} {}=\"{}\"\n", attrs, var_name, value));
5485 } else if let Some(arr) = self.assoc_arrays.get(var_name) {
5486 let mut items: Vec<_> = arr.iter().collect();
5487 items.sort_by_key(|(k, _)| (*k).clone());
5488 let inner: String = items
5489 .iter()
5490 .map(|(k, v)| format!("[{}]=\"{}\"", k, v))
5491 .collect::<Vec<_>>()
5492 .join(" ");
5493 output.push_str(&format!("declare -A {}=({})\n", var_name, inner));
5494 } else if let Some(arr) = self.arrays.get(var_name) {
5495 let mut items: Vec<_> = arr.iter().collect();
5496 items.sort_by_key(|(k, _)| *k);
5497 let inner: String = items
5498 .iter()
5499 .map(|(k, v)| format!("[{}]=\"{}\"", k, v))
5500 .collect::<Vec<_>>()
5501 .join(" ");
5502 output.push_str(&format!("declare -a {}=({})\n", var_name, inner));
5503 } else {
5504 return Ok(ExecResult::err(
5505 format!("bash: declare: {}: not found\n", var_name),
5506 1,
5507 ));
5508 }
5509 }
5510 }
5511 let mut result = ExecResult::ok(output);
5512 result = self.apply_redirections(result, redirects).await?;
5513 return Ok(result);
5514 }
5515
5516 let mut merged_names: Vec<String> = Vec::new();
5519 let mut pending: Option<String> = None;
5520 for name in &names {
5521 if let Some(ref mut p) = pending {
5522 p.push(' ');
5523 p.push_str(name);
5524 if name.ends_with(')') {
5525 merged_names.push(p.clone());
5526 pending = None;
5527 }
5528 } else if let Some(eq_pos) = name.find("=(") {
5529 if name.ends_with(')') {
5530 merged_names.push(name.to_string());
5531 } else {
5532 pending = Some(name.to_string());
5533 let _ = eq_pos; }
5535 } else {
5536 merged_names.push(name.to_string());
5537 }
5538 }
5539 if let Some(p) = pending {
5540 merged_names.push(p);
5541 }
5542
5543 for name in &merged_names {
5545 if let Some(eq_pos) = name.find('=') {
5546 let var_name = &name[..eq_pos];
5547 let value = &name[eq_pos + 1..];
5548
5549 if is_internal_variable(var_name) {
5551 continue;
5552 }
5553
5554 if (is_assoc || is_array) && value.starts_with('(') && value.ends_with(')') {
5556 let inner = &value[1..value.len() - 1];
5557 if is_assoc {
5558 let arr = self.assoc_arrays.entry(var_name.to_string()).or_default();
5559 arr.clear();
5560 let mut rest = inner.trim();
5562 while let Some(bracket_start) = rest.find('[') {
5563 if let Some(bracket_end) = rest[bracket_start..].find(']') {
5564 let key = &rest[bracket_start + 1..bracket_start + bracket_end];
5565 let after = &rest[bracket_start + bracket_end + 1..];
5566 if let Some(eq_rest) = after.strip_prefix('=') {
5567 let eq_rest = eq_rest.trim_start();
5568 let (val, remainder) = if let Some(stripped) =
5569 eq_rest.strip_prefix('"')
5570 {
5571 if let Some(end_q) = stripped.find('"') {
5573 (&stripped[..end_q], stripped[end_q + 1..].trim_start())
5574 } else {
5575 (stripped.trim_end_matches('"'), "")
5576 }
5577 } else {
5578 match eq_rest.find(char::is_whitespace) {
5580 Some(sp) => {
5581 (&eq_rest[..sp], eq_rest[sp..].trim_start())
5582 }
5583 None => (eq_rest, ""),
5584 }
5585 };
5586 arr.insert(key.to_string(), val.to_string());
5587 rest = remainder;
5588 } else {
5589 break;
5590 }
5591 } else {
5592 break;
5593 }
5594 }
5595 } else {
5596 let arr = self.arrays.entry(var_name.to_string()).or_default();
5598 arr.clear();
5599 for (idx, val) in inner.split_whitespace().enumerate() {
5600 arr.insert(idx, val.trim_matches('"').to_string());
5601 }
5602 }
5603 } else if is_nameref {
5604 self.variables
5606 .insert(format!("_NAMEREF_{}", var_name), value.to_string());
5607 } else if is_integer {
5608 let int_val = self.evaluate_arithmetic_with_assign(value);
5610 self.variables
5611 .insert(var_name.to_string(), int_val.to_string());
5612 } else {
5613 let final_value = if is_lowercase {
5615 value.to_lowercase()
5616 } else if is_uppercase {
5617 value.to_uppercase()
5618 } else {
5619 value.to_string()
5620 };
5621 self.variables.insert(var_name.to_string(), final_value);
5622 }
5623
5624 if is_lowercase {
5626 self.variables
5627 .insert(format!("_LOWER_{}", var_name), "1".to_string());
5628 self.variables.remove(&format!("_UPPER_{}", var_name));
5629 }
5630 if is_uppercase {
5631 self.variables
5632 .insert(format!("_UPPER_{}", var_name), "1".to_string());
5633 self.variables.remove(&format!("_LOWER_{}", var_name));
5634 }
5635 if is_readonly {
5636 self.variables
5637 .insert(format!("_READONLY_{}", var_name), "1".to_string());
5638 }
5639 if is_export {
5640 self.env.insert(
5641 var_name.to_string(),
5642 self.variables.get(var_name).cloned().unwrap_or_default(),
5643 );
5644 }
5645 } else {
5646 if remove_nameref {
5648 self.variables.remove(&format!("_NAMEREF_{}", name));
5650 } else if is_nameref {
5651 if let Some(existing) = self.variables.get(name.as_str()).cloned() {
5653 if !existing.is_empty() {
5654 self.variables
5655 .insert(format!("_NAMEREF_{}", name), existing);
5656 }
5657 }
5658 } else if is_assoc {
5659 self.assoc_arrays.entry(name.to_string()).or_default();
5661 } else if is_array {
5662 self.arrays.entry(name.to_string()).or_default();
5664 } else if !self.variables.contains_key(name.as_str()) {
5665 self.variables.insert(name.to_string(), String::new());
5666 }
5667 if is_lowercase {
5669 self.variables
5670 .insert(format!("_LOWER_{}", name), "1".to_string());
5671 self.variables.remove(&format!("_UPPER_{}", name));
5672 }
5673 if is_uppercase {
5674 self.variables
5675 .insert(format!("_UPPER_{}", name), "1".to_string());
5676 self.variables.remove(&format!("_LOWER_{}", name));
5677 }
5678 if is_readonly {
5679 self.variables
5680 .insert(format!("_READONLY_{}", name), "1".to_string());
5681 }
5682 if is_export {
5683 self.env.insert(
5684 name.to_string(),
5685 self.variables
5686 .get(name.as_str())
5687 .cloned()
5688 .unwrap_or_default(),
5689 );
5690 }
5691 }
5692 }
5693
5694 let mut result = ExecResult::ok(String::new());
5695 result = self.apply_redirections(result, redirects).await?;
5696 Ok(result)
5697 }
5698
5699 async fn process_input_redirections(
5701 &mut self,
5702 existing_stdin: Option<String>,
5703 redirects: &[Redirect],
5704 ) -> Result<Option<String>> {
5705 let mut stdin = existing_stdin;
5706
5707 for redirect in redirects {
5708 match redirect.kind {
5709 RedirectKind::Input => {
5710 let target_path = self.expand_word(&redirect.target).await?;
5711 let path = self.resolve_path(&target_path);
5712 if is_dev_null(&path) {
5714 stdin = Some(String::new()); } else {
5716 let content = self.fs.read_file(&path).await?;
5717 stdin = Some(String::from_utf8_lossy(&content).to_string());
5718 }
5719 }
5720 RedirectKind::HereString => {
5721 let content = self.expand_word(&redirect.target).await?;
5723 stdin = Some(format!("{}\n", content));
5724 }
5725 RedirectKind::HereDoc | RedirectKind::HereDocStrip => {
5726 let content = self.expand_word(&redirect.target).await?;
5728 stdin = Some(content);
5729 }
5730 _ => {
5731 }
5733 }
5734 }
5735
5736 Ok(stdin)
5737 }
5738
5739 async fn apply_redirections(
5741 &mut self,
5742 mut result: ExecResult,
5743 redirects: &[Redirect],
5744 ) -> Result<ExecResult> {
5745 for redirect in redirects {
5746 match redirect.kind {
5747 RedirectKind::Output => {
5748 let target_path = self.expand_word(&redirect.target).await?;
5749 let path = self.resolve_path(&target_path);
5750 if is_dev_null(&path) {
5752 match redirect.fd {
5754 Some(2) => result.stderr = String::new(),
5755 _ => result.stdout = String::new(),
5756 }
5757 } else {
5758 match redirect.fd {
5760 Some(2) => {
5761 if let Err(e) =
5763 self.fs.write_file(&path, result.stderr.as_bytes()).await
5764 {
5765 result.stderr = format!("bash: {}: {}\n", target_path, e);
5767 result.exit_code = 1;
5768 return Ok(result);
5769 }
5770 result.stderr = String::new();
5771 }
5772 _ => {
5773 if let Err(e) =
5775 self.fs.write_file(&path, result.stdout.as_bytes()).await
5776 {
5777 result.stdout = String::new();
5779 result.stderr = format!("bash: {}: {}\n", target_path, e);
5780 result.exit_code = 1;
5781 return Ok(result);
5782 }
5783 result.stdout = String::new();
5784 }
5785 }
5786 }
5787 }
5788 RedirectKind::Append => {
5789 let target_path = self.expand_word(&redirect.target).await?;
5790 let path = self.resolve_path(&target_path);
5791 if is_dev_null(&path) {
5793 match redirect.fd {
5795 Some(2) => result.stderr = String::new(),
5796 _ => result.stdout = String::new(),
5797 }
5798 } else {
5799 match redirect.fd {
5801 Some(2) => {
5802 if let Err(e) =
5804 self.fs.append_file(&path, result.stderr.as_bytes()).await
5805 {
5806 result.stderr = format!("bash: {}: {}\n", target_path, e);
5807 result.exit_code = 1;
5808 return Ok(result);
5809 }
5810 result.stderr = String::new();
5811 }
5812 _ => {
5813 if let Err(e) =
5815 self.fs.append_file(&path, result.stdout.as_bytes()).await
5816 {
5817 result.stdout = String::new();
5819 result.stderr = format!("bash: {}: {}\n", target_path, e);
5820 result.exit_code = 1;
5821 return Ok(result);
5822 }
5823 result.stdout = String::new();
5824 }
5825 }
5826 }
5827 }
5828 RedirectKind::OutputBoth => {
5829 let target_path = self.expand_word(&redirect.target).await?;
5831 let path = self.resolve_path(&target_path);
5832 if is_dev_null(&path) {
5834 result.stdout = String::new();
5836 result.stderr = String::new();
5837 } else {
5838 let combined = format!("{}{}", result.stdout, result.stderr);
5840 if let Err(e) = self.fs.write_file(&path, combined.as_bytes()).await {
5841 result.stderr = format!("bash: {}: {}\n", target_path, e);
5842 result.exit_code = 1;
5843 return Ok(result);
5844 }
5845 result.stdout = String::new();
5846 result.stderr = String::new();
5847 }
5848 }
5849 RedirectKind::DupOutput => {
5850 let target = self.expand_word(&redirect.target).await?;
5852 let target_fd: i32 = target.parse().unwrap_or(1);
5853 let src_fd = redirect.fd.unwrap_or(1);
5854
5855 match (src_fd, target_fd) {
5856 (2, 1) => {
5857 result.stdout.push_str(&result.stderr);
5859 result.stderr = String::new();
5860 }
5861 (1, 2) => {
5862 result.stderr.push_str(&result.stdout);
5864 result.stdout = String::new();
5865 }
5866 _ => {
5867 }
5869 }
5870 }
5871 RedirectKind::Input
5872 | RedirectKind::HereString
5873 | RedirectKind::HereDoc
5874 | RedirectKind::HereDocStrip => {
5875 }
5877 RedirectKind::DupInput => {
5878 }
5880 }
5881 }
5882
5883 Ok(result)
5884 }
5885
5886 fn resolve_path(&self, path: &str) -> PathBuf {
5888 let p = Path::new(path);
5889 if p.is_absolute() {
5890 p.to_path_buf()
5891 } else {
5892 self.cwd.join(p)
5893 }
5894 }
5895
5896 async fn expand_word(&mut self, word: &Word) -> Result<String> {
5897 let mut result = String::new();
5898 let mut is_first_part = true;
5899
5900 for part in &word.parts {
5901 match part {
5902 WordPart::Literal(s) => {
5903 if is_first_part && s.starts_with('~') {
5905 let home = self
5906 .env
5907 .get("HOME")
5908 .or_else(|| self.variables.get("HOME"))
5909 .cloned()
5910 .unwrap_or_else(|| "/home/user".to_string());
5911
5912 if s == "~" {
5913 result.push_str(&home);
5915 } else if s.starts_with("~/") {
5916 result.push_str(&home);
5918 result.push_str(&s[1..]); } else {
5920 result.push_str(s);
5922 }
5923 } else {
5924 result.push_str(s);
5925 }
5926 }
5927 WordPart::Variable(name) => {
5928 if self.is_nounset() && !self.is_variable_set(name) {
5930 self.nounset_error = Some(format!("bash: {}: unbound variable\n", name));
5931 }
5932 if name == "*" && word.quoted {
5934 let positional = self
5935 .call_stack
5936 .last()
5937 .map(|f| f.positional.clone())
5938 .unwrap_or_default();
5939 let sep = match self.variables.get("IFS") {
5940 Some(ifs) => ifs
5941 .chars()
5942 .next()
5943 .map(|c| c.to_string())
5944 .unwrap_or_default(),
5945 None => " ".to_string(),
5946 };
5947 result.push_str(&positional.join(&sep));
5948 } else {
5949 result.push_str(&self.expand_variable(name));
5950 }
5951 }
5952 WordPart::CommandSubstitution(commands) => {
5953 let mut stdout = String::new();
5955 for cmd in commands {
5956 let cmd_result = self.execute_command(cmd).await?;
5957 stdout.push_str(&cmd_result.stdout);
5958 self.last_exit_code = cmd_result.exit_code;
5960 }
5961 let trimmed = stdout.trim_end_matches('\n');
5963 result.push_str(trimmed);
5964 }
5965 WordPart::ArithmeticExpansion(expr) => {
5966 let value = self.evaluate_arithmetic_with_assign(expr);
5969 result.push_str(&value.to_string());
5970 }
5971 WordPart::Length(name) => {
5972 let value = if let Some(bracket_pos) = name.find('[') {
5975 let arr_name = &name[..bracket_pos];
5977 let index_end = name.find(']').unwrap_or(name.len());
5978 let index_str = &name[bracket_pos + 1..index_end];
5979 let idx: usize =
5980 self.evaluate_arithmetic(index_str).try_into().unwrap_or(0);
5981 if let Some(arr) = self.arrays.get(arr_name) {
5982 arr.get(&idx).cloned().unwrap_or_default()
5983 } else {
5984 String::new()
5985 }
5986 } else {
5987 self.expand_variable(name)
5988 };
5989 result.push_str(&value.chars().count().to_string());
5990 }
5991 WordPart::ParameterExpansion {
5992 name,
5993 operator,
5994 operand,
5995 colon_variant,
5996 } => {
5997 if name.is_empty()
6000 && !matches!(
6001 operator,
6002 ParameterOp::UseDefault
6003 | ParameterOp::AssignDefault
6004 | ParameterOp::UseReplacement
6005 | ParameterOp::Error
6006 )
6007 {
6008 self.nounset_error = Some("bash: ${}: bad substitution\n".to_string());
6009 continue;
6010 }
6011
6012 let suppress_nounset = matches!(
6015 operator,
6016 ParameterOp::UseDefault
6017 | ParameterOp::AssignDefault
6018 | ParameterOp::UseReplacement
6019 | ParameterOp::Error
6020 );
6021
6022 let (is_set, value) = self.resolve_param_expansion_name(name);
6024
6025 if self.is_nounset() && !suppress_nounset && !is_set {
6026 self.nounset_error = Some(format!("bash: {}: unbound variable\n", name));
6027 }
6028 let expanded = self.apply_parameter_op(
6029 &value,
6030 name,
6031 operator,
6032 operand,
6033 *colon_variant,
6034 is_set,
6035 );
6036 result.push_str(&expanded);
6037 }
6038 WordPart::ArrayAccess { name, index } => {
6039 let resolved_name = self.resolve_nameref(name);
6041 let (arr_name, extra_index) = if let Some(bracket) = resolved_name.find('[') {
6043 let idx_part = &resolved_name[bracket + 1..resolved_name.len() - 1];
6044 (&resolved_name[..bracket], Some(idx_part.to_string()))
6045 } else {
6046 (resolved_name, None)
6047 };
6048 if index == "@" || index == "*" {
6049 if let Some(arr) = self.assoc_arrays.get(arr_name) {
6051 let mut keys: Vec<_> = arr.keys().collect();
6052 keys.sort();
6053 let values: Vec<String> =
6054 keys.iter().filter_map(|k| arr.get(*k).cloned()).collect();
6055 result.push_str(&values.join(" "));
6056 } else if let Some(arr) = self.arrays.get(arr_name) {
6057 let mut indices: Vec<_> = arr.keys().collect();
6058 indices.sort();
6059 let values: Vec<_> =
6060 indices.iter().filter_map(|i| arr.get(i)).collect();
6061 result.push_str(
6062 &values.into_iter().cloned().collect::<Vec<_>>().join(" "),
6063 );
6064 }
6065 } else if let Some(extra_idx) = extra_index {
6066 if let Some(arr) = self.assoc_arrays.get(arr_name) {
6068 if let Some(value) = arr.get(&extra_idx) {
6069 result.push_str(value);
6070 }
6071 } else {
6072 let idx: usize =
6073 self.evaluate_arithmetic(&extra_idx).try_into().unwrap_or(0);
6074 if let Some(arr) = self.arrays.get(arr_name) {
6075 if let Some(value) = arr.get(&idx) {
6076 result.push_str(value);
6077 }
6078 }
6079 }
6080 } else if let Some(arr) = self.assoc_arrays.get(arr_name) {
6081 let key = self.expand_variable_or_literal(index);
6083 if let Some(value) = arr.get(&key) {
6084 result.push_str(value);
6085 }
6086 } else {
6087 let raw_idx = self.evaluate_arithmetic(index);
6089 let idx = if raw_idx < 0 {
6090 let len = self
6092 .arrays
6093 .get(arr_name)
6094 .map(|a| a.keys().max().map(|m| m + 1).unwrap_or(0))
6095 .unwrap_or(0) as i64;
6096 (len + raw_idx).max(0) as usize
6097 } else {
6098 raw_idx as usize
6099 };
6100 if let Some(arr) = self.arrays.get(arr_name) {
6101 if let Some(value) = arr.get(&idx) {
6102 result.push_str(value);
6103 }
6104 }
6105 }
6106 }
6107 WordPart::ArrayIndices(name) => {
6108 if let Some(arr) = self.assoc_arrays.get(name) {
6110 let mut keys: Vec<_> = arr.keys().cloned().collect();
6111 keys.sort();
6112 result.push_str(&keys.join(" "));
6113 } else if let Some(arr) = self.arrays.get(name) {
6114 let mut indices: Vec<_> = arr.keys().collect();
6115 indices.sort();
6116 let index_strs: Vec<String> =
6117 indices.iter().map(|i| i.to_string()).collect();
6118 result.push_str(&index_strs.join(" "));
6119 }
6120 }
6121 WordPart::Substring {
6122 name,
6123 offset,
6124 length,
6125 } => {
6126 let value = self.expand_variable(name);
6128 let char_count = value.chars().count();
6129 let offset_val: isize = self.evaluate_arithmetic(offset) as isize;
6130 let start = if offset_val < 0 {
6131 (char_count as isize + offset_val).max(0) as usize
6132 } else {
6133 (offset_val as usize).min(char_count)
6134 };
6135 let substr: String = if let Some(len_expr) = length {
6136 let len_val = self.evaluate_arithmetic(len_expr) as usize;
6137 value.chars().skip(start).take(len_val).collect()
6138 } else {
6139 value.chars().skip(start).collect()
6140 };
6141 result.push_str(&substr);
6142 }
6143 WordPart::ArraySlice {
6144 name,
6145 offset,
6146 length,
6147 } => {
6148 if let Some(arr) = self.arrays.get(name) {
6150 let mut indices: Vec<_> = arr.keys().cloned().collect();
6151 indices.sort();
6152 let values: Vec<_> =
6153 indices.iter().filter_map(|i| arr.get(i).cloned()).collect();
6154
6155 let offset_val: isize = self.evaluate_arithmetic(offset) as isize;
6156 let start = if offset_val < 0 {
6157 (values.len() as isize + offset_val).max(0) as usize
6158 } else {
6159 (offset_val as usize).min(values.len())
6160 };
6161
6162 let sliced = if let Some(len_expr) = length {
6163 let len_val = self.evaluate_arithmetic(len_expr) as usize;
6164 let end = (start + len_val).min(values.len());
6165 &values[start..end]
6166 } else {
6167 &values[start..]
6168 };
6169 result.push_str(&sliced.join(" "));
6170 }
6171 }
6172 WordPart::IndirectExpansion(name) => {
6173 let nameref_key = format!("_NAMEREF_{}", name);
6176 if let Some(target) = self.variables.get(&nameref_key).cloned() {
6177 result.push_str(&target);
6179 } else {
6180 let var_name = self.expand_variable(name);
6182 let value = self.expand_variable(&var_name);
6183 result.push_str(&value);
6184 }
6185 }
6186 WordPart::PrefixMatch(prefix) => {
6187 let mut names: Vec<String> = self
6189 .variables
6190 .keys()
6191 .filter(|k| k.starts_with(prefix.as_str()))
6192 .filter(|k| !Self::is_internal_variable(k))
6194 .cloned()
6195 .collect();
6196 for k in self.env.keys() {
6198 if k.starts_with(prefix.as_str())
6199 && !names.contains(k)
6200 && !Self::is_internal_variable(k)
6202 {
6203 names.push(k.clone());
6204 }
6205 }
6206 names.sort();
6207 result.push_str(&names.join(" "));
6208 }
6209 WordPart::ArrayLength(name) => {
6210 if let Some(arr) = self.assoc_arrays.get(name) {
6212 result.push_str(&arr.len().to_string());
6213 } else if let Some(arr) = self.arrays.get(name) {
6214 result.push_str(&arr.len().to_string());
6215 } else {
6216 result.push('0');
6217 }
6218 }
6219 WordPart::ProcessSubstitution { commands, is_input } => {
6220 let mut stdout = String::new();
6222 for cmd in commands {
6223 let cmd_result = self.execute_command(cmd).await?;
6224 stdout.push_str(&cmd_result.stdout);
6225 }
6226
6227 let path_str = format!(
6229 "/dev/fd/proc_sub_{}",
6230 PROC_SUB_COUNTER.fetch_add(1, Ordering::Relaxed)
6231 );
6232 let path = Path::new(&path_str);
6233
6234 if self.fs.write_file(path, stdout.as_bytes()).await.is_err() {
6236 if *is_input {
6239 result.push_str(&stdout);
6240 }
6241 } else {
6242 result.push_str(&path_str);
6243 }
6244 }
6245 WordPart::Transformation { name, operator } => {
6246 let value = self.expand_variable(name);
6247 let transformed = match operator {
6248 'Q' => {
6249 format!("'{}'", value.replace('\'', "'\\''"))
6251 }
6252 'E' => {
6253 value
6255 .replace("\\n", "\n")
6256 .replace("\\t", "\t")
6257 .replace("\\\\", "\\")
6258 }
6259 'P' => {
6260 value.clone()
6262 }
6263 'A' => {
6264 format!("{}='{}'", name, value.replace('\'', "'\\''"))
6266 }
6267 'K' => {
6268 value.clone()
6270 }
6271 'a' => {
6272 let mut attrs = String::new();
6274 if self.variables.contains_key(&format!("_READONLY_{}", name)) {
6275 attrs.push('r');
6276 }
6277 if self.env.contains_key(name.as_str()) {
6278 attrs.push('x');
6279 }
6280 attrs
6281 }
6282 'u' | 'U' => {
6283 if *operator == 'U' {
6285 value.to_uppercase()
6286 } else {
6287 let mut chars = value.chars();
6288 match chars.next() {
6289 Some(first) => {
6290 first.to_uppercase().collect::<String>() + chars.as_str()
6291 }
6292 None => String::new(),
6293 }
6294 }
6295 }
6296 'L' => {
6297 value.to_lowercase()
6299 }
6300 _ => value.clone(),
6301 };
6302 result.push_str(&transformed);
6303 }
6304 }
6305 is_first_part = false;
6306 }
6307
6308 Ok(result)
6309 }
6310
6311 async fn expand_word_to_fields(&mut self, word: &Word) -> Result<Vec<String>> {
6315 if word.parts.len() == 1 {
6317 if let WordPart::Variable(name) = &word.parts[0] {
6319 if name == "@" {
6320 let positional = self
6321 .call_stack
6322 .last()
6323 .map(|f| f.positional.clone())
6324 .unwrap_or_default();
6325 if word.quoted {
6326 return Ok(positional);
6328 }
6329 let mut fields = Vec::new();
6331 for p in &positional {
6332 fields.extend(self.ifs_split(p));
6333 }
6334 return Ok(fields);
6335 }
6336 if name == "*" {
6337 let positional = self
6338 .call_stack
6339 .last()
6340 .map(|f| f.positional.clone())
6341 .unwrap_or_default();
6342 if word.quoted {
6343 let sep = match self.variables.get("IFS") {
6346 Some(ifs) => ifs
6347 .chars()
6348 .next()
6349 .map(|c| c.to_string())
6350 .unwrap_or_default(),
6351 None => " ".to_string(),
6352 };
6353 return Ok(vec![positional.join(&sep)]);
6354 }
6355 let mut fields = Vec::new();
6357 for p in &positional {
6358 fields.extend(self.ifs_split(p));
6359 }
6360 return Ok(fields);
6361 }
6362 }
6363 if let WordPart::ArrayAccess { name, index } = &word.parts[0] {
6364 if index == "@" || index == "*" {
6365 if let Some(arr) = self.assoc_arrays.get(name) {
6367 let mut keys: Vec<_> = arr.keys().cloned().collect();
6368 keys.sort();
6369 let values: Vec<String> =
6370 keys.iter().filter_map(|k| arr.get(k).cloned()).collect();
6371 if word.quoted && index == "*" {
6372 return Ok(vec![values.join(" ")]);
6373 }
6374 return Ok(values);
6375 }
6376 if let Some(arr) = self.arrays.get(name) {
6377 let mut indices: Vec<_> = arr.keys().collect();
6378 indices.sort();
6379 let values: Vec<String> =
6380 indices.iter().filter_map(|i| arr.get(i).cloned()).collect();
6381 if word.quoted && index == "*" {
6383 return Ok(vec![values.join(" ")]);
6384 }
6385 return Ok(values);
6386 }
6387 return Ok(Vec::new());
6388 }
6389 }
6390 if let WordPart::ArrayIndices(name) = &word.parts[0] {
6392 if let Some(arr) = self.assoc_arrays.get(name) {
6393 let mut keys: Vec<_> = arr.keys().cloned().collect();
6394 keys.sort();
6395 return Ok(keys);
6396 }
6397 if let Some(arr) = self.arrays.get(name) {
6398 let mut indices: Vec<_> = arr.keys().collect();
6399 indices.sort();
6400 return Ok(indices.iter().map(|i| i.to_string()).collect());
6401 }
6402 return Ok(Vec::new());
6403 }
6404 }
6405
6406 let expanded = self.expand_word(word).await?;
6411
6412 let is_assignment_word =
6417 matches!(word.parts.first(), Some(WordPart::Literal(s)) if s.contains('='));
6418 let has_expansion = !word.quoted
6419 && !is_assignment_word
6420 && word.parts.iter().any(|p| {
6421 matches!(
6422 p,
6423 WordPart::Variable(_)
6424 | WordPart::CommandSubstitution(_)
6425 | WordPart::ArithmeticExpansion(_)
6426 | WordPart::ParameterExpansion { .. }
6427 | WordPart::ArrayAccess { .. }
6428 )
6429 });
6430
6431 if has_expansion {
6432 Ok(self.ifs_split(&expanded))
6433 } else {
6434 Ok(vec![expanded])
6435 }
6436 }
6437
6438 fn resolve_param_expansion_name(&self, name: &str) -> (bool, String) {
6441 if let Some(arr_name) = name
6443 .strip_suffix("[@]")
6444 .or_else(|| name.strip_suffix("[*]"))
6445 {
6446 if let Some(arr) = self.assoc_arrays.get(arr_name) {
6447 let is_set = !arr.is_empty();
6448 let mut keys: Vec<_> = arr.keys().collect();
6449 keys.sort();
6450 let values: Vec<String> =
6451 keys.iter().filter_map(|k| arr.get(*k).cloned()).collect();
6452 return (is_set, values.join(" "));
6453 }
6454 if let Some(arr) = self.arrays.get(arr_name) {
6455 let is_set = !arr.is_empty();
6456 let mut indices: Vec<_> = arr.keys().collect();
6457 indices.sort();
6458 let values: Vec<_> = indices.iter().filter_map(|i| arr.get(i)).collect();
6459 return (
6460 is_set,
6461 values.into_iter().cloned().collect::<Vec<_>>().join(" "),
6462 );
6463 }
6464 return (false, String::new());
6465 }
6466
6467 if name == "@" || name == "*" {
6469 if let Some(frame) = self.call_stack.last() {
6470 let is_set = !frame.positional.is_empty();
6471 return (is_set, frame.positional.join(" "));
6472 }
6473 return (false, String::new());
6474 }
6475
6476 let is_set = self.is_variable_set(name);
6478 let value = self.expand_variable(name);
6479 (is_set, value)
6480 }
6481
6482 fn ifs_split(&self, s: &str) -> Vec<String> {
6490 let ifs = self
6491 .variables
6492 .get("IFS")
6493 .cloned()
6494 .unwrap_or_else(|| " \t\n".to_string());
6495
6496 if ifs.is_empty() {
6497 return vec![s.to_string()];
6498 }
6499
6500 let is_ifs = |c: char| ifs.contains(c);
6501 let is_ifs_ws = |c: char| ifs.contains(c) && " \t\n".contains(c);
6502 let is_ifs_nws = |c: char| ifs.contains(c) && !" \t\n".contains(c);
6503 let all_whitespace_ifs = ifs.chars().all(|c| " \t\n".contains(c));
6504
6505 if all_whitespace_ifs {
6506 return s
6508 .split(|c: char| is_ifs(c))
6509 .filter(|f| !f.is_empty())
6510 .map(|f| f.to_string())
6511 .collect();
6512 }
6513
6514 let mut fields: Vec<String> = Vec::new();
6516 let mut current = String::new();
6517 let chars: Vec<char> = s.chars().collect();
6518 let mut i = 0;
6519
6520 while i < chars.len() && is_ifs_ws(chars[i]) {
6522 i += 1;
6523 }
6524 if i < chars.len() && is_ifs_nws(chars[i]) {
6526 fields.push(String::new());
6527 i += 1;
6528 while i < chars.len() && is_ifs_ws(chars[i]) {
6529 i += 1;
6530 }
6531 }
6532
6533 while i < chars.len() {
6534 let c = chars[i];
6535 if is_ifs_nws(c) {
6536 fields.push(std::mem::take(&mut current));
6538 i += 1;
6539 while i < chars.len() && is_ifs_ws(chars[i]) {
6541 i += 1;
6542 }
6543 } else if is_ifs_ws(c) {
6544 while i < chars.len() && is_ifs_ws(chars[i]) {
6546 i += 1;
6547 }
6548 if i < chars.len() && is_ifs_nws(chars[i]) {
6549 fields.push(std::mem::take(&mut current));
6551 i += 1; while i < chars.len() && is_ifs_ws(chars[i]) {
6553 i += 1;
6554 }
6555 } else if i < chars.len() {
6556 fields.push(std::mem::take(&mut current));
6558 }
6559 } else {
6561 current.push(c);
6562 i += 1;
6563 }
6564 }
6565
6566 if !current.is_empty() {
6567 fields.push(current);
6568 }
6569
6570 fields
6571 }
6572
6573 fn expand_operand(&mut self, operand: &str) -> String {
6576 if operand.is_empty() {
6577 return String::new();
6578 }
6579 let word = Parser::parse_word_string_with_limits(
6581 operand,
6582 self.limits.max_ast_depth,
6583 self.limits.max_parser_operations,
6584 );
6585 let mut result = String::new();
6586 for part in &word.parts {
6587 match part {
6588 WordPart::Literal(s) => result.push_str(s),
6589 WordPart::Variable(name) => {
6590 result.push_str(&self.expand_variable(name));
6591 }
6592 WordPart::ArithmeticExpansion(expr) => {
6593 let val = self.evaluate_arithmetic_with_assign(expr);
6594 result.push_str(&val.to_string());
6595 }
6596 WordPart::ParameterExpansion {
6597 name,
6598 operator,
6599 operand: inner_operand,
6600 colon_variant,
6601 } => {
6602 let (is_set, value) = self.resolve_param_expansion_name(name);
6603 let expanded = self.apply_parameter_op(
6604 &value,
6605 name,
6606 operator,
6607 inner_operand,
6608 *colon_variant,
6609 is_set,
6610 );
6611 result.push_str(&expanded);
6612 }
6613 WordPart::Length(name) => {
6614 let value = self.expand_variable(name);
6615 result.push_str(&value.len().to_string());
6616 }
6617 _ => {}
6619 }
6620 }
6621 result
6622 }
6623
6624 fn apply_parameter_op(
6628 &mut self,
6629 value: &str,
6630 name: &str,
6631 operator: &ParameterOp,
6632 operand: &str,
6633 colon_variant: bool,
6634 is_set: bool,
6635 ) -> String {
6636 let use_default = if colon_variant {
6639 !is_set || value.is_empty()
6640 } else {
6641 !is_set
6642 };
6643 let use_replacement = if colon_variant {
6644 is_set && !value.is_empty()
6645 } else {
6646 is_set
6647 };
6648
6649 match operator {
6650 ParameterOp::UseDefault => {
6651 if use_default {
6652 self.expand_operand(operand)
6653 } else {
6654 value.to_string()
6655 }
6656 }
6657 ParameterOp::AssignDefault => {
6658 if use_default {
6659 let expanded = self.expand_operand(operand);
6660 self.set_variable(name.to_string(), expanded.clone());
6661 expanded
6662 } else {
6663 value.to_string()
6664 }
6665 }
6666 ParameterOp::UseReplacement => {
6667 if use_replacement {
6668 self.expand_operand(operand)
6669 } else {
6670 String::new()
6671 }
6672 }
6673 ParameterOp::Error => {
6674 if use_default {
6675 let expanded = self.expand_operand(operand);
6676 let msg = if expanded.is_empty() {
6677 format!("bash: {}: parameter null or not set\n", name)
6678 } else {
6679 format!("bash: {}: {}\n", name, expanded)
6680 };
6681 self.nounset_error = Some(msg);
6682 String::new()
6683 } else {
6684 value.to_string()
6685 }
6686 }
6687 ParameterOp::RemovePrefixShort => {
6688 self.remove_pattern(value, operand, true, false)
6690 }
6691 ParameterOp::RemovePrefixLong => {
6692 self.remove_pattern(value, operand, true, true)
6694 }
6695 ParameterOp::RemoveSuffixShort => {
6696 self.remove_pattern(value, operand, false, false)
6698 }
6699 ParameterOp::RemoveSuffixLong => {
6700 self.remove_pattern(value, operand, false, true)
6702 }
6703 ParameterOp::ReplaceFirst {
6704 pattern,
6705 replacement,
6706 } => {
6707 self.replace_pattern(value, pattern, replacement, false)
6709 }
6710 ParameterOp::ReplaceAll {
6711 pattern,
6712 replacement,
6713 } => {
6714 self.replace_pattern(value, pattern, replacement, true)
6716 }
6717 ParameterOp::UpperFirst => {
6718 let mut chars = value.chars();
6720 match chars.next() {
6721 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
6722 None => String::new(),
6723 }
6724 }
6725 ParameterOp::UpperAll => {
6726 value.to_uppercase()
6728 }
6729 ParameterOp::LowerFirst => {
6730 let mut chars = value.chars();
6732 match chars.next() {
6733 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
6734 None => String::new(),
6735 }
6736 }
6737 ParameterOp::LowerAll => {
6738 value.to_lowercase()
6740 }
6741 }
6742 }
6743
6744 fn replace_pattern(
6746 &self,
6747 value: &str,
6748 pattern: &str,
6749 replacement: &str,
6750 global: bool,
6751 ) -> String {
6752 if pattern.is_empty() {
6753 return value.to_string();
6754 }
6755
6756 if let Some(rest) = pattern.strip_prefix('#') {
6758 if rest.is_empty() {
6759 return value.to_string();
6760 }
6761 if let Some(stripped) = value.strip_prefix(rest) {
6762 return format!("{}{}", replacement, stripped);
6763 }
6764 if rest.contains('*') {
6766 let matched = self.remove_pattern(value, rest, true, false);
6767 if matched != value {
6768 let prefix_len = value.len() - matched.len();
6769 return format!("{}{}", replacement, &value[prefix_len..]);
6770 }
6771 }
6772 return value.to_string();
6773 }
6774
6775 if let Some(rest) = pattern.strip_prefix('%') {
6777 if rest.is_empty() {
6778 return value.to_string();
6779 }
6780 if let Some(stripped) = value.strip_suffix(rest) {
6781 return format!("{}{}", stripped, replacement);
6782 }
6783 if rest.contains('*') {
6785 let matched = self.remove_pattern(value, rest, false, false);
6786 if matched != value {
6787 return format!("{}{}", matched, replacement);
6788 }
6789 }
6790 return value.to_string();
6791 }
6792
6793 if pattern.contains('*') {
6795 if pattern == "*" {
6798 return replacement.to_string();
6800 }
6801
6802 if let Some(star_pos) = pattern.find('*') {
6803 let prefix = &pattern[..star_pos];
6804 let suffix = &pattern[star_pos + 1..];
6805
6806 if prefix.is_empty() && !suffix.is_empty() {
6807 if let Some(pos) = value.find(suffix) {
6809 let after = &value[pos + suffix.len()..];
6810 if global {
6811 return replacement.to_string()
6812 + &self.replace_pattern(after, pattern, replacement, true);
6813 } else {
6814 return replacement.to_string() + after;
6815 }
6816 }
6817 } else if !prefix.is_empty() && suffix.is_empty() {
6818 if value.starts_with(prefix) {
6820 return replacement.to_string();
6821 }
6822 }
6823 }
6824 return value.to_string();
6826 }
6827
6828 if global {
6830 value.replace(pattern, replacement)
6831 } else {
6832 value.replacen(pattern, replacement, 1)
6833 }
6834 }
6835
6836 fn remove_pattern(&self, value: &str, pattern: &str, prefix: bool, longest: bool) -> String {
6838 if pattern.is_empty() {
6840 return value.to_string();
6841 }
6842
6843 if prefix {
6844 if pattern == "*" {
6846 if longest {
6847 return String::new();
6848 } else if !value.is_empty() {
6849 return value.chars().skip(1).collect();
6850 } else {
6851 return value.to_string();
6852 }
6853 }
6854
6855 if let Some(star_pos) = pattern.find('*') {
6857 let prefix_part = &pattern[..star_pos];
6858 let suffix_part = &pattern[star_pos + 1..];
6859
6860 if prefix_part.is_empty() {
6861 if longest {
6863 if let Some(pos) = value.rfind(suffix_part) {
6865 return value[pos + suffix_part.len()..].to_string();
6866 }
6867 } else {
6868 if let Some(pos) = value.find(suffix_part) {
6870 return value[pos + suffix_part.len()..].to_string();
6871 }
6872 }
6873 } else if suffix_part.is_empty() {
6874 if let Some(rest) = value.strip_prefix(prefix_part) {
6876 if longest {
6877 return String::new();
6878 } else {
6879 return rest.to_string();
6880 }
6881 }
6882 } else {
6883 if let Some(rest) = value.strip_prefix(prefix_part) {
6885 if longest {
6886 if let Some(pos) = rest.rfind(suffix_part) {
6887 return rest[pos + suffix_part.len()..].to_string();
6888 }
6889 } else if let Some(pos) = rest.find(suffix_part) {
6890 return rest[pos + suffix_part.len()..].to_string();
6891 }
6892 }
6893 }
6894 } else if let Some(rest) = value.strip_prefix(pattern) {
6895 return rest.to_string();
6896 }
6897 } else {
6898 if pattern == "*" {
6900 if longest {
6901 return String::new();
6902 } else if !value.is_empty() {
6903 let mut s = value.to_string();
6904 s.pop();
6905 return s;
6906 } else {
6907 return value.to_string();
6908 }
6909 }
6910
6911 if let Some(star_pos) = pattern.find('*') {
6913 let prefix_part = &pattern[..star_pos];
6914 let suffix_part = &pattern[star_pos + 1..];
6915
6916 if suffix_part.is_empty() {
6917 if longest {
6919 if let Some(pos) = value.find(prefix_part) {
6921 return value[..pos].to_string();
6922 }
6923 } else {
6924 if let Some(pos) = value.rfind(prefix_part) {
6926 return value[..pos].to_string();
6927 }
6928 }
6929 } else if prefix_part.is_empty() {
6930 if let Some(before) = value.strip_suffix(suffix_part) {
6932 if longest {
6933 return String::new();
6934 } else {
6935 return before.to_string();
6936 }
6937 }
6938 } else {
6939 if let Some(before_suffix) = value.strip_suffix(suffix_part) {
6941 if longest {
6942 if let Some(pos) = before_suffix.find(prefix_part) {
6943 return value[..pos].to_string();
6944 }
6945 } else if let Some(pos) = before_suffix.rfind(prefix_part) {
6946 return value[..pos].to_string();
6947 }
6948 }
6949 }
6950 } else if let Some(before) = value.strip_suffix(pattern) {
6951 return before.to_string();
6952 }
6953 }
6954
6955 value.to_string()
6956 }
6957
6958 const MAX_ARITHMETIC_DEPTH: usize = 50;
6962
6963 fn is_valid_var_name(s: &str) -> bool {
6968 !s.is_empty()
6969 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
6970 && !s.chars().next().unwrap_or('0').is_ascii_digit()
6971 }
6972
6973 fn evaluate_arithmetic_with_assign(&mut self, expr: &str) -> i64 {
6974 let expr = expr.trim();
6975
6976 {
6979 let mut depth = 0i32;
6980 let chars: Vec<char> = expr.chars().collect();
6981 for i in (0..chars.len()).rev() {
6982 match chars[i] {
6983 '(' => depth += 1,
6984 ')' => depth -= 1,
6985 ',' if depth == 0 => {
6986 let left = &expr[..i];
6987 let right = &expr[i + 1..];
6988 self.evaluate_arithmetic_with_assign(left);
6989 return self.evaluate_arithmetic_with_assign(right);
6990 }
6991 _ => {}
6992 }
6993 }
6994 }
6995
6996 if let Some(var_name) = expr.strip_prefix("++") {
6998 let var_name = var_name.trim();
6999 if Self::is_valid_var_name(var_name) {
7000 let val = self.expand_variable(var_name).parse::<i64>().unwrap_or(0) + 1;
7001 self.set_variable(var_name.to_string(), val.to_string());
7002 return val;
7003 }
7004 }
7005 if let Some(var_name) = expr.strip_prefix("--") {
7006 let var_name = var_name.trim();
7007 if Self::is_valid_var_name(var_name) {
7008 let val = self.expand_variable(var_name).parse::<i64>().unwrap_or(0) - 1;
7009 self.set_variable(var_name.to_string(), val.to_string());
7010 return val;
7011 }
7012 }
7013
7014 if let Some(var_name) = expr.strip_suffix("++") {
7016 let var_name = var_name.trim();
7017 if Self::is_valid_var_name(var_name) {
7018 let old_val = self.expand_variable(var_name).parse::<i64>().unwrap_or(0);
7019 self.set_variable(var_name.to_string(), (old_val + 1).to_string());
7020 return old_val;
7021 }
7022 }
7023 if let Some(var_name) = expr.strip_suffix("--") {
7024 let var_name = var_name.trim();
7025 if Self::is_valid_var_name(var_name) {
7026 let old_val = self.expand_variable(var_name).parse::<i64>().unwrap_or(0);
7027 self.set_variable(var_name.to_string(), (old_val - 1).to_string());
7028 return old_val;
7029 }
7030 }
7031
7032 if let Some(eq_pos) = expr.find('=') {
7035 let before = &expr[..eq_pos];
7036 let after_char = expr.as_bytes().get(eq_pos + 1);
7037 if !before.ends_with('!') && after_char != Some(&b'=') {
7039 let (var_name, op) = if let Some(s) = before.strip_suffix("<<") {
7041 (s.trim(), "<<")
7042 } else if let Some(s) = before.strip_suffix(">>") {
7043 (s.trim(), ">>")
7044 } else if let Some(s) = before.strip_suffix('+') {
7045 (s.trim(), "+")
7046 } else if let Some(s) = before.strip_suffix('-') {
7047 (s.trim(), "-")
7048 } else if let Some(s) = before.strip_suffix('*') {
7049 (s.trim(), "*")
7050 } else if let Some(s) = before.strip_suffix('/') {
7051 (s.trim(), "/")
7052 } else if let Some(s) = before.strip_suffix('%') {
7053 (s.trim(), "%")
7054 } else if let Some(s) = before.strip_suffix('&') {
7055 (s.trim(), "&")
7056 } else if let Some(s) = before.strip_suffix('|') {
7057 (s.trim(), "|")
7058 } else if let Some(s) = before.strip_suffix('^') {
7059 (s.trim(), "^")
7060 } else if !before.ends_with('<') && !before.ends_with('>') {
7061 (before.trim(), "")
7062 } else {
7063 ("", "")
7064 };
7065
7066 if Self::is_valid_var_name(var_name) {
7067 let rhs = &expr[eq_pos + 1..];
7068 let rhs_val = self.evaluate_arithmetic(rhs);
7069 let value = if op.is_empty() {
7070 rhs_val
7071 } else {
7072 let lhs_val = self.expand_variable(var_name).parse::<i64>().unwrap_or(0);
7073 match op {
7075 "+" => lhs_val.wrapping_add(rhs_val),
7076 "-" => lhs_val.wrapping_sub(rhs_val),
7077 "*" => lhs_val.wrapping_mul(rhs_val),
7078 "/" => {
7079 if rhs_val != 0 && !(lhs_val == i64::MIN && rhs_val == -1) {
7080 lhs_val / rhs_val
7081 } else {
7082 0
7083 }
7084 }
7085 "%" => {
7086 if rhs_val != 0 && !(lhs_val == i64::MIN && rhs_val == -1) {
7087 lhs_val % rhs_val
7088 } else {
7089 0
7090 }
7091 }
7092 "&" => lhs_val & rhs_val,
7093 "|" => lhs_val | rhs_val,
7094 "^" => lhs_val ^ rhs_val,
7095 "<<" => lhs_val.wrapping_shl((rhs_val & 63) as u32),
7096 ">>" => lhs_val.wrapping_shr((rhs_val & 63) as u32),
7097 _ => rhs_val,
7098 }
7099 };
7100 self.set_variable(var_name.to_string(), value.to_string());
7101 return value;
7102 }
7103 }
7104 }
7105
7106 self.evaluate_arithmetic(expr)
7107 }
7108
7109 fn evaluate_arithmetic(&self, expr: &str) -> i64 {
7111 let expr = expr.trim();
7113
7114 let expanded = self.expand_arithmetic_vars(expr);
7116
7117 self.parse_arithmetic_impl(&expanded, 0)
7119 }
7120
7121 fn resolve_arith_var(&self, value: &str, depth: usize) -> String {
7127 if depth >= Self::MAX_ARITHMETIC_DEPTH {
7128 return "0".to_string();
7129 }
7130 let trimmed = value.trim();
7131 if trimmed.is_empty() {
7132 return "0".to_string();
7133 }
7134 if trimmed.parse::<i64>().is_ok() {
7136 return trimmed.to_string();
7137 }
7138 if Self::is_valid_var_name(trimmed) {
7140 let inner = self.expand_variable(trimmed);
7141 return self.resolve_arith_var(&inner, depth + 1);
7142 }
7143 let expanded = self.expand_arithmetic_vars_depth(trimmed, depth + 1);
7146 format!("({})", expanded)
7147 }
7148
7149 fn expand_arithmetic_vars(&self, expr: &str) -> String {
7151 self.expand_arithmetic_vars_depth(expr, 0)
7152 }
7153
7154 fn expand_arithmetic_vars_depth(&self, expr: &str, depth: usize) -> String {
7157 if depth >= Self::MAX_ARITHMETIC_DEPTH {
7158 return "0".to_string();
7159 }
7160
7161 let expr = expr.replace('"', "");
7163
7164 let mut result = String::new();
7165 let mut chars = expr.chars().peekable();
7166 let mut in_numeric_literal = false;
7168
7169 while let Some(ch) = chars.next() {
7170 if ch == '$' {
7171 in_numeric_literal = false;
7172 let mut name = String::new();
7174 while let Some(&c) = chars.peek() {
7175 if c.is_ascii_alphanumeric() || c == '_' {
7176 name.push(chars.next().unwrap());
7177 } else {
7178 break;
7179 }
7180 }
7181 if !name.is_empty() {
7182 let value = self.expand_variable(&name);
7185 if value.is_empty() {
7186 result.push('0');
7187 } else {
7188 result.push_str(&value);
7189 }
7190 } else {
7191 result.push(ch);
7192 }
7193 } else if ch == '#' {
7194 result.push(ch);
7196 in_numeric_literal = true;
7197 } else if in_numeric_literal && (ch.is_ascii_alphanumeric() || ch == '_') {
7198 result.push(ch);
7200 } else if ch.is_ascii_digit() {
7201 result.push(ch);
7202 if ch == '0' {
7204 if let Some(&next) = chars.peek() {
7205 if next == 'x' || next == 'X' {
7206 result.push(chars.next().unwrap());
7207 in_numeric_literal = true;
7208 }
7209 }
7210 }
7211 } else if ch.is_ascii_alphabetic() || ch == '_' {
7212 in_numeric_literal = false;
7213 let mut name = String::new();
7215 name.push(ch);
7216 while let Some(&c) = chars.peek() {
7217 if c.is_ascii_alphanumeric() || c == '_' {
7218 name.push(chars.next().unwrap());
7219 } else {
7220 break;
7221 }
7222 }
7223 if chars.peek() == Some(&'[') {
7225 chars.next(); let mut index_expr = String::new();
7227 let mut bracket_depth = 1;
7228 while let Some(&c) = chars.peek() {
7229 chars.next();
7230 if c == '[' {
7231 bracket_depth += 1;
7232 index_expr.push(c);
7233 } else if c == ']' {
7234 bracket_depth -= 1;
7235 if bracket_depth == 0 {
7236 break;
7237 }
7238 index_expr.push(c);
7239 } else {
7240 index_expr.push(c);
7241 }
7242 }
7243 let idx = self.evaluate_arithmetic(&index_expr);
7245 if let Some(arr) = self.arrays.get(&name) {
7247 let idx_usize: usize = idx.try_into().unwrap_or(0);
7248 let value = arr.get(&idx_usize).cloned().unwrap_or_default();
7249 result.push_str(&self.resolve_arith_var(&value, depth));
7250 } else {
7251 let value = self.expand_variable(&name);
7253 if idx == 0 {
7254 result.push_str(&self.resolve_arith_var(&value, depth));
7255 } else {
7256 result.push('0');
7257 }
7258 }
7259 } else {
7260 let value = self.expand_variable(&name);
7262 result.push_str(&self.resolve_arith_var(&value, depth));
7263 }
7264 } else {
7265 in_numeric_literal = false;
7266 result.push(ch);
7267 }
7268 }
7269
7270 result
7271 }
7272
7273 fn parse_arithmetic_impl(&self, expr: &str, arith_depth: usize) -> i64 {
7276 let expr = expr.trim();
7277
7278 if expr.is_empty() {
7279 return 0;
7280 }
7281
7282 if !expr.is_ascii() {
7284 return 0;
7285 }
7286
7287 if arith_depth >= Self::MAX_ARITHMETIC_DEPTH {
7289 return 0;
7290 }
7291
7292 if expr.starts_with('(') && expr.ends_with(')') {
7294 let mut depth = 0;
7296 let mut balanced = true;
7297 for (i, ch) in expr.chars().enumerate() {
7298 match ch {
7299 '(' => depth += 1,
7300 ')' => {
7301 depth -= 1;
7302 if depth == 0 && i < expr.len() - 1 {
7303 balanced = false;
7304 break;
7305 }
7306 }
7307 _ => {}
7308 }
7309 }
7310 if balanced && depth == 0 {
7311 return self.parse_arithmetic_impl(&expr[1..expr.len() - 1], arith_depth + 1);
7312 }
7313 }
7314
7315 let chars: Vec<char> = expr.chars().collect();
7316
7317 let mut depth = 0;
7319 for i in 0..chars.len() {
7320 match chars[i] {
7321 '(' => depth += 1,
7322 ')' => depth -= 1,
7323 '?' if depth == 0 => {
7324 let mut colon_depth = 0;
7326 for j in (i + 1)..chars.len() {
7327 match chars[j] {
7328 '(' => colon_depth += 1,
7329 ')' => colon_depth -= 1,
7330 '?' => colon_depth += 1,
7331 ':' if colon_depth == 0 => {
7332 let cond = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7333 let then_val =
7334 self.parse_arithmetic_impl(&expr[i + 1..j], arith_depth + 1);
7335 let else_val =
7336 self.parse_arithmetic_impl(&expr[j + 1..], arith_depth + 1);
7337 return if cond != 0 { then_val } else { else_val };
7338 }
7339 ':' => colon_depth -= 1,
7340 _ => {}
7341 }
7342 }
7343 }
7344 _ => {}
7345 }
7346 }
7347
7348 depth = 0;
7350 for i in (0..chars.len()).rev() {
7351 match chars[i] {
7352 '(' => depth += 1,
7353 ')' => depth -= 1,
7354 '|' if depth == 0 && i > 0 && chars[i - 1] == '|' => {
7355 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7356 if left != 0 {
7358 return 1;
7359 }
7360 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7361 return if right != 0 { 1 } else { 0 };
7362 }
7363 _ => {}
7364 }
7365 }
7366
7367 depth = 0;
7369 for i in (0..chars.len()).rev() {
7370 match chars[i] {
7371 '(' => depth += 1,
7372 ')' => depth -= 1,
7373 '&' if depth == 0 && i > 0 && chars[i - 1] == '&' => {
7374 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7375 if left == 0 {
7377 return 0;
7378 }
7379 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7380 return if right != 0 { 1 } else { 0 };
7381 }
7382 _ => {}
7383 }
7384 }
7385
7386 depth = 0;
7388 for i in (0..chars.len()).rev() {
7389 match chars[i] {
7390 '(' => depth += 1,
7391 ')' => depth -= 1,
7392 '|' if depth == 0
7393 && (i == 0 || chars[i - 1] != '|')
7394 && (i + 1 >= chars.len() || chars[i + 1] != '|') =>
7395 {
7396 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7397 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7398 return left | right;
7399 }
7400 _ => {}
7401 }
7402 }
7403
7404 depth = 0;
7406 for i in (0..chars.len()).rev() {
7407 match chars[i] {
7408 '(' => depth += 1,
7409 ')' => depth -= 1,
7410 '^' if depth == 0 => {
7411 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7412 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7413 return left ^ right;
7414 }
7415 _ => {}
7416 }
7417 }
7418
7419 depth = 0;
7421 for i in (0..chars.len()).rev() {
7422 match chars[i] {
7423 '(' => depth += 1,
7424 ')' => depth -= 1,
7425 '&' if depth == 0
7426 && (i == 0 || chars[i - 1] != '&')
7427 && (i + 1 >= chars.len() || chars[i + 1] != '&') =>
7428 {
7429 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7430 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7431 return left & right;
7432 }
7433 _ => {}
7434 }
7435 }
7436
7437 depth = 0;
7439 for i in (0..chars.len()).rev() {
7440 match chars[i] {
7441 '(' => depth += 1,
7442 ')' => depth -= 1,
7443 '=' if depth == 0 && i > 0 && chars[i - 1] == '=' => {
7444 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7445 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7446 return if left == right { 1 } else { 0 };
7447 }
7448 '=' if depth == 0 && i > 0 && chars[i - 1] == '!' => {
7449 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7450 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7451 return if left != right { 1 } else { 0 };
7452 }
7453 _ => {}
7454 }
7455 }
7456
7457 depth = 0;
7459 for i in (0..chars.len()).rev() {
7460 match chars[i] {
7461 '(' => depth += 1,
7462 ')' => depth -= 1,
7463 '=' if depth == 0 && i > 0 && chars[i - 1] == '<' => {
7464 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7465 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7466 return if left <= right { 1 } else { 0 };
7467 }
7468 '=' if depth == 0 && i > 0 && chars[i - 1] == '>' => {
7469 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7470 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7471 return if left >= right { 1 } else { 0 };
7472 }
7473 '<' if depth == 0
7474 && (i + 1 >= chars.len() || (chars[i + 1] != '=' && chars[i + 1] != '<'))
7475 && (i == 0 || chars[i - 1] != '<') =>
7476 {
7477 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7478 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7479 return if left < right { 1 } else { 0 };
7480 }
7481 '>' if depth == 0
7482 && (i + 1 >= chars.len() || (chars[i + 1] != '=' && chars[i + 1] != '>'))
7483 && (i == 0 || chars[i - 1] != '>') =>
7484 {
7485 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7486 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7487 return if left > right { 1 } else { 0 };
7488 }
7489 _ => {}
7490 }
7491 }
7492
7493 depth = 0;
7495 for i in (0..chars.len()).rev() {
7496 match chars[i] {
7497 '(' => depth += 1,
7498 ')' => depth -= 1,
7499 '<' if depth == 0
7500 && i > 0
7501 && chars[i - 1] == '<'
7502 && (i < 2 || chars[i - 2] != '<')
7503 && (i + 1 >= chars.len() || chars[i + 1] != '=') =>
7504 {
7505 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7506 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7507 let shift = right.clamp(0, 63) as u32;
7509 return left.wrapping_shl(shift);
7510 }
7511 '>' if depth == 0
7512 && i > 0
7513 && chars[i - 1] == '>'
7514 && (i < 2 || chars[i - 2] != '>')
7515 && (i + 1 >= chars.len() || chars[i + 1] != '=') =>
7516 {
7517 let left = self.parse_arithmetic_impl(&expr[..i - 1], arith_depth + 1);
7518 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7519 let shift = right.clamp(0, 63) as u32;
7521 return left.wrapping_shr(shift);
7522 }
7523 _ => {}
7524 }
7525 }
7526
7527 depth = 0;
7529 for i in (0..chars.len()).rev() {
7530 match chars[i] {
7531 '(' => depth += 1,
7532 ')' => depth -= 1,
7533 '+' | '-' if depth == 0 && i > 0 => {
7534 if chars[i] == '+' && i + 1 < chars.len() && chars[i + 1] == '+' {
7536 continue;
7537 }
7538 if chars[i] == '+' && i > 0 && chars[i - 1] == '+' {
7539 continue;
7540 }
7541 if chars[i] == '-' && i + 1 < chars.len() && chars[i + 1] == '-' {
7542 continue;
7543 }
7544 if chars[i] == '-' && i > 0 && chars[i - 1] == '-' {
7545 continue;
7546 }
7547 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7548 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7549 return if chars[i] == '+' {
7551 left.wrapping_add(right)
7552 } else {
7553 left.wrapping_sub(right)
7554 };
7555 }
7556 _ => {}
7557 }
7558 }
7559
7560 depth = 0;
7562 for i in (0..chars.len()).rev() {
7563 match chars[i] {
7564 '(' => depth += 1,
7565 ')' => depth -= 1,
7566 '*' if depth == 0 => {
7567 if i + 1 < chars.len() && chars[i + 1] == '*' {
7569 continue;
7570 }
7571 if i > 0 && chars[i - 1] == '*' {
7572 continue;
7573 }
7574 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7575 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7576 return left.wrapping_mul(right);
7578 }
7579 '/' | '%' if depth == 0 => {
7580 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7581 let right = self.parse_arithmetic_impl(&expr[i + 1..], arith_depth + 1);
7582 return match chars[i] {
7584 '/' => {
7585 if right != 0 {
7586 left.wrapping_div(right)
7587 } else {
7588 0
7589 }
7590 }
7591 '%' => {
7592 if right != 0 {
7593 left.wrapping_rem(right)
7594 } else {
7595 0
7596 }
7597 }
7598 _ => 0,
7599 };
7600 }
7601 _ => {}
7602 }
7603 }
7604
7605 depth = 0;
7607 for i in 0..chars.len() {
7608 match chars[i] {
7609 '(' => depth += 1,
7610 ')' => depth -= 1,
7611 '*' if depth == 0 && i + 1 < chars.len() && chars[i + 1] == '*' => {
7612 let left = self.parse_arithmetic_impl(&expr[..i], arith_depth + 1);
7613 let right = self.parse_arithmetic_impl(&expr[i + 2..], arith_depth + 1);
7615 let exp = right.clamp(0, 63) as u32;
7617 return left.wrapping_pow(exp);
7618 }
7619 _ => {}
7620 }
7621 }
7622
7623 if let Some(rest) = expr.strip_prefix('-') {
7625 let rest = rest.trim();
7626 if !rest.is_empty() {
7627 return self
7629 .parse_arithmetic_impl(rest, arith_depth + 1)
7630 .wrapping_neg();
7631 }
7632 }
7633 if let Some(rest) = expr.strip_prefix('~') {
7634 let rest = rest.trim();
7635 if !rest.is_empty() {
7636 return !self.parse_arithmetic_impl(rest, arith_depth + 1);
7637 }
7638 }
7639 if let Some(rest) = expr.strip_prefix('!') {
7640 let rest = rest.trim();
7641 if !rest.is_empty() {
7642 let val = self.parse_arithmetic_impl(rest, arith_depth + 1);
7643 return if val == 0 { 1 } else { 0 };
7644 }
7645 }
7646
7647 if let Some(hash_pos) = expr.find('#') {
7649 let base_str = &expr[..hash_pos];
7650 let value_str = &expr[hash_pos + 1..];
7651 if let Ok(base) = base_str.parse::<u32>() {
7652 if (2..=36).contains(&base) {
7653 return i64::from_str_radix(value_str, base).unwrap_or(0);
7654 } else if (37..=64).contains(&base) {
7655 return Self::parse_base_n(value_str, base);
7657 }
7658 }
7659 }
7660
7661 if expr.starts_with("0x") || expr.starts_with("0X") {
7663 return i64::from_str_radix(&expr[2..], 16).unwrap_or(0);
7664 }
7665 if expr.starts_with('0') && expr.len() > 1 && expr.chars().all(|c| c.is_ascii_digit()) {
7666 return i64::from_str_radix(&expr[1..], 8).unwrap_or(0);
7667 }
7668
7669 expr.trim().parse().unwrap_or(0)
7671 }
7672
7673 fn parse_base_n(value_str: &str, base: u32) -> i64 {
7675 let mut result: i64 = 0;
7676 for ch in value_str.chars() {
7677 let digit = match ch {
7678 '0'..='9' => ch as u32 - '0' as u32,
7679 'a'..='z' => 10 + ch as u32 - 'a' as u32,
7680 'A'..='Z' => 36 + ch as u32 - 'A' as u32,
7681 '@' => 62,
7682 '_' => 63,
7683 _ => return 0,
7684 };
7685 if digit >= base {
7686 return 0;
7687 }
7688 result = result.wrapping_mul(base as i64).wrapping_add(digit as i64);
7689 }
7690 result
7691 }
7692
7693 fn expand_variable_or_literal(&self, s: &str) -> String {
7697 let trimmed = s.trim();
7699 if let Some(var_name) = trimmed.strip_prefix('$') {
7700 let var_name = var_name.trim_start_matches('{').trim_end_matches('}');
7701 return self.expand_variable(var_name);
7702 }
7703 if let Some(val) = self.variables.get(s) {
7704 return val.clone();
7705 }
7706 s.to_string()
7707 }
7708
7709 fn is_internal_variable(name: &str) -> bool {
7711 is_internal_variable(name)
7712 }
7713
7714 fn set_variable(&mut self, name: String, value: String) {
7718 if Self::is_internal_variable(&name) {
7720 return;
7721 }
7722 let resolved = self.resolve_nameref(&name).to_string();
7724 let value = if self
7726 .variables
7727 .get(&format!("_LOWER_{}", resolved))
7728 .map(|v| v == "1")
7729 .unwrap_or(false)
7730 {
7731 value.to_lowercase()
7732 } else if self
7733 .variables
7734 .get(&format!("_UPPER_{}", resolved))
7735 .map(|v| v == "1")
7736 .unwrap_or(false)
7737 {
7738 value.to_uppercase()
7739 } else {
7740 value
7741 };
7742 for frame in self.call_stack.iter_mut().rev() {
7743 if let std::collections::hash_map::Entry::Occupied(mut e) =
7744 frame.locals.entry(resolved.clone())
7745 {
7746 e.insert(value);
7747 return;
7748 }
7749 }
7750 self.variables.insert(resolved, value);
7751 }
7752
7753 fn resolve_nameref<'a>(&'a self, name: &'a str) -> &'a str {
7756 let mut current = name;
7757 let mut visited = std::collections::HashSet::new();
7758 visited.insert(name);
7759 for _ in 0..10 {
7760 let key = format!("_NAMEREF_{}", current);
7761 if let Some(target) = self.variables.get(&key) {
7762 if !visited.insert(target.as_str()) {
7764 return name;
7766 }
7767 current = target.as_str();
7768 } else {
7769 break;
7770 }
7771 }
7772 current
7773 }
7774
7775 fn expand_variable(&self, name: &str) -> String {
7776 let name = self.resolve_nameref(name);
7778
7779 if let Some(bracket) = name.find('[') {
7781 if name.ends_with(']') {
7782 let arr_name = &name[..bracket];
7783 let idx_str = &name[bracket + 1..name.len() - 1];
7784 if let Some(arr) = self.assoc_arrays.get(arr_name) {
7785 return arr.get(idx_str).cloned().unwrap_or_default();
7786 } else if let Some(arr) = self.arrays.get(arr_name) {
7787 let idx: usize = self.evaluate_arithmetic(idx_str).try_into().unwrap_or(0);
7788 return arr.get(&idx).cloned().unwrap_or_default();
7789 }
7790 return String::new();
7791 }
7792 }
7793
7794 match name {
7796 "?" => return self.last_exit_code.to_string(),
7797 "#" => {
7798 if let Some(frame) = self.call_stack.last() {
7800 return frame.positional.len().to_string();
7801 }
7802 return "0".to_string();
7803 }
7804 "@" => {
7805 if let Some(frame) = self.call_stack.last() {
7807 return frame.positional.join(" ");
7808 }
7809 return String::new();
7810 }
7811 "*" => {
7812 if let Some(frame) = self.call_stack.last() {
7814 let sep = match self.variables.get("IFS") {
7815 Some(ifs) => ifs
7816 .chars()
7817 .next()
7818 .map(|c| c.to_string())
7819 .unwrap_or_default(),
7820 None => " ".to_string(),
7821 };
7822 return frame.positional.join(&sep);
7823 }
7824 return String::new();
7825 }
7826 "$" => {
7827 return "1".to_string();
7829 }
7830 "!" => {
7831 if let Some(last_bg_pid) = self.variables.get("_LAST_BG_PID") {
7835 return last_bg_pid.clone();
7836 }
7837 return String::new();
7838 }
7839 "-" => {
7840 let mut flags = String::new();
7843 for opt in ['e', 'x', 'u', 'f', 'n', 'v', 'a', 'b', 'h', 'm'] {
7844 let opt_name = format!("SHOPT_{}", opt);
7845 if self
7846 .variables
7847 .get(&opt_name)
7848 .map(|v| v == "1")
7849 .unwrap_or(false)
7850 {
7851 flags.push(opt);
7852 }
7853 }
7854 if self.options.errexit && !flags.contains('e') {
7856 flags.push('e');
7857 }
7858 if self.options.xtrace && !flags.contains('x') {
7859 flags.push('x');
7860 }
7861 return flags;
7862 }
7863 "RANDOM" => {
7864 use std::collections::hash_map::RandomState;
7866 use std::hash::{BuildHasher, Hasher};
7867 let random = RandomState::new().build_hasher().finish() as u32;
7868 return (random % 32768).to_string();
7869 }
7870 "LINENO" => {
7871 return self.current_line.to_string();
7873 }
7874 "PWD" => {
7875 return self.cwd.to_string_lossy().to_string();
7876 }
7877 "OLDPWD" => {
7878 if let Some(v) = self.variables.get("OLDPWD") {
7879 return v.clone();
7880 }
7881 return self.cwd.to_string_lossy().to_string();
7882 }
7883 "HOSTNAME" => {
7884 if let Some(v) = self.variables.get("HOSTNAME") {
7885 return v.clone();
7886 }
7887 return "localhost".to_string();
7888 }
7889 "BASH_VERSION" => {
7890 return format!("{}-bashkit", env!("CARGO_PKG_VERSION"));
7891 }
7892 "SECONDS" => {
7893 if let Some(v) = self.variables.get("SECONDS") {
7895 return v.clone();
7896 }
7897 return "0".to_string();
7898 }
7899 _ => {}
7900 }
7901
7902 if let Ok(n) = name.parse::<usize>() {
7904 if n == 0 {
7905 if let Some(frame) = self.call_stack.last() {
7907 return frame.name.clone();
7908 }
7909 return "bash".to_string();
7910 }
7911 if let Some(frame) = self.call_stack.last() {
7913 if n > 0 && n <= frame.positional.len() {
7914 return frame.positional[n - 1].clone();
7915 }
7916 }
7917 return String::new();
7918 }
7919
7920 for frame in self.call_stack.iter().rev() {
7922 if let Some(value) = frame.locals.get(name) {
7923 return value.clone();
7924 }
7925 }
7926
7927 if let Some(value) = self.variables.get(name) {
7929 return value.clone();
7930 }
7931
7932 if let Some(value) = self.env.get(name) {
7934 return value.clone();
7935 }
7936
7937 String::new()
7939 }
7940
7941 fn is_variable_set(&self, name: &str) -> bool {
7943 if matches!(
7945 name,
7946 "?" | "#"
7947 | "@"
7948 | "*"
7949 | "$"
7950 | "!"
7951 | "-"
7952 | "RANDOM"
7953 | "LINENO"
7954 | "PWD"
7955 | "OLDPWD"
7956 | "HOSTNAME"
7957 | "BASH_VERSION"
7958 | "SECONDS"
7959 ) {
7960 return true;
7961 }
7962 if let Ok(n) = name.parse::<usize>() {
7964 if n == 0 {
7965 return true;
7966 }
7967 return self
7968 .call_stack
7969 .last()
7970 .map(|f| n <= f.positional.len())
7971 .unwrap_or(false);
7972 }
7973 for frame in self.call_stack.iter().rev() {
7975 if frame.locals.contains_key(name) {
7976 return true;
7977 }
7978 }
7979 if self.variables.contains_key(name) {
7981 return true;
7982 }
7983 self.env.contains_key(name)
7985 }
7986
7987 fn is_nounset(&self) -> bool {
7989 self.variables
7990 .get("SHOPT_u")
7991 .map(|v| v == "1")
7992 .unwrap_or(false)
7993 }
7994
7995 fn is_pipefail(&self) -> bool {
7997 self.options.pipefail
7998 || self
7999 .variables
8000 .get("SHOPT_pipefail")
8001 .map(|v| v == "1")
8002 .unwrap_or(false)
8003 }
8004
8005 async fn run_err_trap(&mut self, stdout: &mut String, stderr: &mut String) {
8007 if let Some(trap_cmd) = self.traps.get("ERR").cloned() {
8008 if let Ok(trap_script) = Parser::with_limits(
8010 &trap_cmd,
8011 self.limits.max_ast_depth,
8012 self.limits.max_parser_operations,
8013 )
8014 .parse()
8015 {
8016 let emit_before = self.output_emit_count;
8017 if let Ok(trap_result) = self.execute_command_sequence(&trap_script.commands).await
8018 {
8019 self.maybe_emit_output(&trap_result.stdout, &trap_result.stderr, emit_before);
8020 stdout.push_str(&trap_result.stdout);
8021 stderr.push_str(&trap_result.stderr);
8022 }
8023 }
8024 }
8025 }
8026
8027 #[allow(dead_code)]
8029 fn set_local(&mut self, name: &str, value: &str) {
8030 if let Some(frame) = self.call_stack.last_mut() {
8031 frame.locals.insert(name.to_string(), value.to_string());
8032 }
8033 }
8034
8035 fn expand_braces(&self, s: &str) -> Vec<String> {
8040 const MAX_BRACE_EXPANSION_TOTAL: usize = 100_000;
8041 let mut count = 0;
8042 self.expand_braces_capped(s, &mut count, MAX_BRACE_EXPANSION_TOTAL)
8043 }
8044
8045 fn expand_braces_capped(&self, s: &str, count: &mut usize, max: usize) -> Vec<String> {
8046 if *count >= max {
8047 return vec![s.to_string()];
8048 }
8049
8050 let mut depth = 0;
8052 let mut brace_start = None;
8053 let mut brace_end = None;
8054 let chars: Vec<char> = s.chars().collect();
8055
8056 for (i, &ch) in chars.iter().enumerate() {
8057 match ch {
8058 '{' => {
8059 if depth == 0 {
8060 brace_start = Some(i);
8061 }
8062 depth += 1;
8063 }
8064 '}' => {
8065 depth -= 1;
8066 if depth == 0 && brace_start.is_some() {
8067 brace_end = Some(i);
8068 break;
8069 }
8070 }
8071 _ => {}
8072 }
8073 }
8074
8075 let (start, end) = match (brace_start, brace_end) {
8077 (Some(s), Some(e)) => (s, e),
8078 _ => return vec![s.to_string()],
8079 };
8080
8081 let prefix: String = chars[..start].iter().collect();
8082 let suffix: String = chars[end + 1..].iter().collect();
8083 let brace_content: String = chars[start + 1..end].iter().collect();
8084
8085 if brace_content.starts_with(' ') || brace_content.ends_with(' ') {
8087 return vec![s.to_string()];
8088 }
8089
8090 if let Some(range_result) = self.try_expand_range(&brace_content) {
8092 let mut results = Vec::new();
8093 for item in range_result {
8094 if *count >= max {
8095 break;
8096 }
8097 let expanded = format!("{}{}{}", prefix, item, suffix);
8098 let sub = self.expand_braces_capped(&expanded, count, max);
8099 *count += sub.len();
8100 results.extend(sub);
8101 }
8102 return results;
8103 }
8104
8105 let items = self.split_brace_items(&brace_content);
8108 if items.len() <= 1 && !brace_content.contains(',') {
8109 return vec![s.to_string()];
8111 }
8112
8113 let mut results = Vec::new();
8114 for item in items {
8115 if *count >= max {
8116 break;
8117 }
8118 let expanded = format!("{}{}{}", prefix, item, suffix);
8119 let sub = self.expand_braces_capped(&expanded, count, max);
8120 *count += sub.len();
8121 results.extend(sub);
8122 }
8123
8124 results
8125 }
8126
8127 fn try_expand_range(&self, content: &str) -> Option<Vec<String>> {
8130 const MAX_BRACE_RANGE: u64 = 10_000;
8132
8133 let parts: Vec<&str> = content.split("..").collect();
8135 if parts.len() != 2 {
8136 return None;
8137 }
8138
8139 let start = parts[0];
8140 let end = parts[1];
8141
8142 if let (Ok(start_num), Ok(end_num)) = (start.parse::<i64>(), end.parse::<i64>()) {
8144 let range_size = (end_num as i128 - start_num as i128).unsigned_abs() + 1;
8145 if range_size > MAX_BRACE_RANGE as u128 {
8146 return None; }
8148 let mut results = Vec::new();
8149 if start_num <= end_num {
8150 for i in start_num..=end_num {
8151 results.push(i.to_string());
8152 }
8153 } else {
8154 for i in (end_num..=start_num).rev() {
8155 results.push(i.to_string());
8156 }
8157 }
8158 return Some(results);
8159 }
8160
8161 if start.len() == 1 && end.len() == 1 {
8163 let start_char = start.chars().next().unwrap();
8164 let end_char = end.chars().next().unwrap();
8165
8166 if start_char.is_ascii_alphabetic() && end_char.is_ascii_alphabetic() {
8167 let mut results = Vec::new();
8168 let start_byte = start_char as u8;
8169 let end_byte = end_char as u8;
8170
8171 if start_byte <= end_byte {
8172 for b in start_byte..=end_byte {
8173 results.push((b as char).to_string());
8174 }
8175 } else {
8176 for b in (end_byte..=start_byte).rev() {
8177 results.push((b as char).to_string());
8178 }
8179 }
8180 return Some(results);
8181 }
8182 }
8183
8184 None
8185 }
8186
8187 fn split_brace_items(&self, content: &str) -> Vec<String> {
8189 let mut items = Vec::new();
8190 let mut current = String::new();
8191 let mut depth = 0;
8192
8193 for ch in content.chars() {
8194 match ch {
8195 '{' => {
8196 depth += 1;
8197 current.push(ch);
8198 }
8199 '}' => {
8200 depth -= 1;
8201 current.push(ch);
8202 }
8203 ',' if depth == 0 => {
8204 items.push(current);
8205 current = String::new();
8206 }
8207 _ => {
8208 current.push(ch);
8209 }
8210 }
8211 }
8212 items.push(current);
8213
8214 items
8215 }
8216
8217 fn contains_glob_chars(&self, s: &str) -> bool {
8218 s.contains('*') || s.contains('?') || s.contains('[')
8219 }
8220
8221 fn is_dotglob(&self) -> bool {
8223 self.variables
8224 .get("SHOPT_dotglob")
8225 .map(|v| v == "1")
8226 .unwrap_or(false)
8227 }
8228
8229 fn is_nocaseglob(&self) -> bool {
8231 self.variables
8232 .get("SHOPT_nocaseglob")
8233 .map(|v| v == "1")
8234 .unwrap_or(false)
8235 }
8236
8237 fn is_noglob(&self) -> bool {
8239 self.variables
8240 .get("SHOPT_f")
8241 .map(|v| v == "1")
8242 .unwrap_or(false)
8243 }
8244
8245 fn is_failglob(&self) -> bool {
8247 self.variables
8248 .get("SHOPT_failglob")
8249 .map(|v| v == "1")
8250 .unwrap_or(false)
8251 }
8252
8253 fn is_globstar(&self) -> bool {
8255 self.variables
8256 .get("SHOPT_globstar")
8257 .map(|v| v == "1")
8258 .unwrap_or(false)
8259 }
8260
8261 fn is_extglob(&self) -> bool {
8263 self.variables
8264 .get("SHOPT_extglob")
8265 .map(|v| v == "1")
8266 .unwrap_or(false)
8267 }
8268
8269 async fn expand_glob_item(&self, item: &str) -> std::result::Result<Vec<String>, String> {
8272 if !self.contains_glob_chars(item) || self.is_noglob() {
8273 return Ok(vec![item.to_string()]);
8274 }
8275 let glob_matches = self.expand_glob(item).await.unwrap_or_default();
8276 if glob_matches.is_empty() {
8277 if self.is_failglob() {
8278 return Err(item.to_string());
8279 }
8280 let nullglob = self
8281 .variables
8282 .get("SHOPT_nullglob")
8283 .map(|v| v == "1")
8284 .unwrap_or(false);
8285 if nullglob {
8286 Ok(vec![])
8287 } else {
8288 Ok(vec![item.to_string()])
8289 }
8290 } else {
8291 Ok(glob_matches)
8292 }
8293 }
8294
8295 async fn expand_glob(&self, pattern: &str) -> Result<Vec<String>> {
8297 if pattern.contains("**") && self.is_globstar() {
8299 return self.expand_glob_recursive(pattern).await;
8300 }
8301
8302 let mut matches = Vec::new();
8303 let dotglob = self.is_dotglob();
8304 let nocase = self.is_nocaseglob();
8305
8306 let path = Path::new(pattern);
8308 let (dir, file_pattern) = if path.is_absolute() {
8309 let parent = path.parent().unwrap_or(Path::new("/"));
8310 let name = path.file_name().map(|s| s.to_string_lossy().to_string());
8311 (parent.to_path_buf(), name)
8312 } else {
8313 let parent = path.parent();
8315 let name = path.file_name().map(|s| s.to_string_lossy().to_string());
8316 if let Some(p) = parent {
8317 if p.as_os_str().is_empty() {
8318 (self.cwd.clone(), name)
8319 } else {
8320 (self.cwd.join(p), name)
8321 }
8322 } else {
8323 (self.cwd.clone(), name)
8324 }
8325 };
8326
8327 let file_pattern = match file_pattern {
8328 Some(p) => p,
8329 None => return Ok(matches),
8330 };
8331
8332 if !self.fs.exists(&dir).await.unwrap_or(false) {
8334 return Ok(matches);
8335 }
8336
8337 let entries = match self.fs.read_dir(&dir).await {
8339 Ok(e) => e,
8340 Err(_) => return Ok(matches),
8341 };
8342
8343 let pattern_starts_with_dot = file_pattern.starts_with('.');
8345
8346 for entry in entries {
8348 if entry.name.starts_with('.') && !dotglob && !pattern_starts_with_dot {
8350 continue;
8351 }
8352
8353 if self.glob_match_impl(&entry.name, &file_pattern, nocase, 0) {
8354 let full_path = if path.is_absolute() {
8356 dir.join(&entry.name).to_string_lossy().to_string()
8357 } else {
8358 if let Some(parent) = path.parent() {
8360 if parent.as_os_str().is_empty() {
8361 entry.name.clone()
8362 } else {
8363 format!("{}/{}", parent.to_string_lossy(), entry.name)
8364 }
8365 } else {
8366 entry.name.clone()
8367 }
8368 };
8369 matches.push(full_path);
8370 }
8371 }
8372
8373 matches.sort();
8375 Ok(matches)
8376 }
8377
8378 async fn expand_glob_recursive(&self, pattern: &str) -> Result<Vec<String>> {
8380 let is_absolute = pattern.starts_with('/');
8381 let components: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
8382 let dotglob = self.is_dotglob();
8383 let nocase = self.is_nocaseglob();
8384
8385 let star_star_idx = match components.iter().position(|&c| c == "**") {
8387 Some(i) => i,
8388 None => return Ok(Vec::new()),
8389 };
8390
8391 let base_dir = if is_absolute {
8393 let mut p = PathBuf::from("/");
8394 for c in &components[..star_star_idx] {
8395 p.push(c);
8396 }
8397 p
8398 } else {
8399 let mut p = self.cwd.clone();
8400 for c in &components[..star_star_idx] {
8401 p.push(c);
8402 }
8403 p
8404 };
8405
8406 let after_pattern: Vec<&str> = components[star_star_idx + 1..].to_vec();
8408
8409 let mut all_dirs = vec![base_dir.clone()];
8411 let max_depth = self.fs.limits().max_path_depth;
8413 self.collect_dirs_recursive(&base_dir, &mut all_dirs, max_depth)
8414 .await;
8415
8416 let mut matches = Vec::new();
8417
8418 for dir in &all_dirs {
8419 if after_pattern.is_empty() {
8420 if let Ok(entries) = self.fs.read_dir(dir).await {
8422 for entry in entries {
8423 if entry.name.starts_with('.') && !dotglob {
8424 continue;
8425 }
8426 if !entry.metadata.file_type.is_dir() {
8427 matches.push(dir.join(&entry.name).to_string_lossy().to_string());
8428 }
8429 }
8430 }
8431 } else if after_pattern.len() == 1 {
8432 let pat = after_pattern[0];
8434 let pattern_starts_with_dot = pat.starts_with('.');
8435 if let Ok(entries) = self.fs.read_dir(dir).await {
8436 for entry in entries {
8437 if entry.name.starts_with('.') && !dotglob && !pattern_starts_with_dot {
8438 continue;
8439 }
8440 if self.glob_match_impl(&entry.name, pat, nocase, 0) {
8441 matches.push(dir.join(&entry.name).to_string_lossy().to_string());
8442 }
8443 }
8444 }
8445 }
8446 }
8447
8448 matches.sort();
8449 Ok(matches)
8450 }
8451
8452 fn collect_dirs_recursive<'a>(
8455 &'a self,
8456 dir: &'a Path,
8457 result: &'a mut Vec<PathBuf>,
8458 max_depth: usize,
8459 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'a>> {
8460 Box::pin(async move {
8461 if max_depth == 0 {
8462 return;
8463 }
8464 if let Ok(entries) = self.fs.read_dir(dir).await {
8465 for entry in entries {
8466 if entry.metadata.file_type.is_dir() {
8467 let subdir = dir.join(&entry.name);
8468 result.push(subdir.clone());
8469 self.collect_dirs_recursive(&subdir, result, max_depth - 1)
8470 .await;
8471 }
8472 }
8473 }
8474 })
8475 }
8476}
8477
8478#[cfg(test)]
8479mod tests {
8480 use super::*;
8481 use crate::fs::InMemoryFs;
8482 use crate::parser::Parser;
8483
8484 #[tokio::test(start_paused = true)]
8486 async fn test_timeout_expires_deterministically() {
8487 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
8488 let mut interp = Interpreter::new(Arc::clone(&fs));
8489
8490 let parser = Parser::new("timeout 0.001 sleep 10; echo $?");
8492 let ast = parser.parse().unwrap();
8493 let result = interp.execute(&ast).await.unwrap();
8494 assert_eq!(
8495 result.stdout.trim(),
8496 "124",
8497 "Expected exit code 124 for timeout"
8498 );
8499 }
8500
8501 #[tokio::test(start_paused = true)]
8503 async fn test_timeout_zero_deterministically() {
8504 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
8505 let mut interp = Interpreter::new(Arc::clone(&fs));
8506
8507 let parser = Parser::new("timeout 0 sleep 1; echo $?");
8509 let ast = parser.parse().unwrap();
8510 let result = interp.execute(&ast).await.unwrap();
8511 assert_eq!(
8512 result.stdout.trim(),
8513 "124",
8514 "Expected exit code 124 for zero timeout"
8515 );
8516 }
8517
8518 #[test]
8520 fn test_parse_timeout_duration_subsecond() {
8521 use std::time::Duration;
8522
8523 let d = Interpreter::parse_timeout_duration("0.001").unwrap();
8525 assert_eq!(d, Duration::from_secs_f64(0.001));
8526
8527 let d = Interpreter::parse_timeout_duration("0.5").unwrap();
8528 assert_eq!(d, Duration::from_millis(500));
8529
8530 let d = Interpreter::parse_timeout_duration("1.5s").unwrap();
8531 assert_eq!(d, Duration::from_millis(1500));
8532
8533 let d = Interpreter::parse_timeout_duration("0").unwrap();
8535 assert_eq!(d, Duration::ZERO);
8536 }
8537
8538 async fn run_script(script: &str) -> ExecResult {
8542 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
8543 let mut interp = Interpreter::new(Arc::clone(&fs));
8544 let parser = Parser::new(script);
8545 let ast = parser.parse().unwrap();
8546 interp.execute(&ast).await.unwrap()
8547 }
8548
8549 #[tokio::test]
8550 async fn test_colon_null_utility() {
8551 let result = run_script(":").await;
8553 assert_eq!(result.exit_code, 0);
8554 assert_eq!(result.stdout, "");
8555 }
8556
8557 #[tokio::test]
8558 async fn test_colon_with_args() {
8559 let result = run_script(": arg1 arg2 arg3").await;
8561 assert_eq!(result.exit_code, 0);
8562 assert_eq!(result.stdout, "");
8563 }
8564
8565 #[tokio::test]
8566 async fn test_colon_in_while_loop() {
8567 let result = run_script(
8569 "x=0; while :; do x=$((x+1)); if [ $x -ge 3 ]; then break; fi; done; echo $x",
8570 )
8571 .await;
8572 assert_eq!(result.stdout.trim(), "3");
8573 }
8574
8575 #[tokio::test]
8576 async fn test_times_builtin() {
8577 let result = run_script("times").await;
8579 assert_eq!(result.exit_code, 0);
8580 assert!(result.stdout.contains("0m0.000s"));
8581 }
8582
8583 #[tokio::test]
8584 async fn test_readonly_basic() {
8585 let result = run_script("readonly X=value; echo $X").await;
8587 assert_eq!(result.stdout.trim(), "value");
8588 }
8589
8590 #[tokio::test]
8591 async fn test_special_param_dash() {
8592 let result = run_script("set -e; echo \"$-\"").await;
8594 assert!(result.stdout.contains('e'));
8595 }
8596
8597 #[tokio::test]
8598 async fn test_special_param_bang() {
8599 let result = run_script("echo \"$!\"").await;
8601 assert_eq!(result.exit_code, 0);
8603 }
8604
8605 #[tokio::test]
8610 async fn test_colon_variable_side_effect() {
8611 let result = run_script(": ${X:=default}; echo $X").await;
8613 assert_eq!(result.stdout.trim(), "default");
8614 assert_eq!(result.exit_code, 0);
8615 }
8616
8617 #[tokio::test]
8618 async fn test_colon_in_if_then() {
8619 let result = run_script("if true; then :; fi; echo done").await;
8621 assert_eq!(result.stdout.trim(), "done");
8622 assert_eq!(result.exit_code, 0);
8623 }
8624
8625 #[tokio::test]
8626 async fn test_readonly_set_and_read() {
8627 let result = run_script("readonly FOO=bar; readonly BAR=baz; echo $FOO $BAR").await;
8629 assert_eq!(result.stdout.trim(), "bar baz");
8630 }
8631
8632 #[tokio::test]
8633 async fn test_readonly_mark_existing() {
8634 let result = run_script("X=hello; readonly X; echo $X").await;
8636 assert_eq!(result.stdout.trim(), "hello");
8637 }
8638
8639 #[tokio::test]
8640 async fn test_times_two_lines() {
8641 let result = run_script("times").await;
8643 let lines: Vec<&str> = result.stdout.lines().collect();
8644 assert_eq!(lines.len(), 2);
8645 }
8646
8647 #[tokio::test]
8648 async fn test_eval_simple_command() {
8649 let result = run_script("cmd='echo hello'; eval $cmd").await;
8651 assert_eq!(result.exit_code, 0);
8653 }
8654
8655 #[tokio::test]
8656 async fn test_special_param_dash_multiple_options() {
8657 let result = run_script("set -e; set -x; echo \"$-\"").await;
8659 assert!(result.stdout.contains('e'));
8660 }
8662
8663 #[tokio::test]
8664 async fn test_special_param_dash_no_options() {
8665 let result = run_script("echo \"flags:$-:end\"").await;
8667 assert!(result.stdout.contains("flags:"));
8668 assert!(result.stdout.contains(":end"));
8669 assert_eq!(result.exit_code, 0);
8670 }
8671
8672 #[tokio::test]
8677 async fn test_colon_does_not_produce_output() {
8678 let result = run_script(": 'this should not appear'").await;
8680 assert_eq!(result.stdout, "");
8681 assert_eq!(result.stderr, "");
8682 }
8683
8684 #[tokio::test]
8685 async fn test_eval_empty_args() {
8686 let result = run_script("eval; echo $?").await;
8688 assert!(result.stdout.contains('0'));
8689 assert_eq!(result.exit_code, 0);
8690 }
8691
8692 #[tokio::test]
8693 async fn test_readonly_empty_value() {
8694 let result = run_script("readonly EMPTY=; echo \"[$EMPTY]\"").await;
8696 assert_eq!(result.stdout.trim(), "[]");
8697 }
8698
8699 #[tokio::test]
8700 async fn test_times_no_args_accepted() {
8701 let result = run_script("times ignored args here").await;
8703 assert_eq!(result.exit_code, 0);
8704 assert!(result.stdout.contains("0m0.000s"));
8705 }
8706
8707 #[tokio::test]
8708 async fn test_special_param_bang_empty_without_bg() {
8709 let result = run_script("x=\"$!\"; [ -z \"$x\" ] && echo empty || echo not_empty").await;
8711 assert_eq!(result.stdout.trim(), "empty");
8712 }
8713
8714 #[tokio::test]
8715 async fn test_colon_exit_code_zero() {
8716 let result = run_script("false; :; echo $?").await;
8718 assert_eq!(result.stdout.trim(), "0");
8719 }
8720
8721 #[tokio::test]
8722 async fn test_readonly_without_value_preserves_existing() {
8723 let result = run_script("VAR=existing; readonly VAR; echo $VAR").await;
8725 assert_eq!(result.stdout.trim(), "existing");
8726 }
8727
8728 #[tokio::test]
8731 async fn test_bash_c_simple_command() {
8732 let result = run_script("bash -c 'echo hello'").await;
8734 assert_eq!(result.exit_code, 0);
8735 assert_eq!(result.stdout.trim(), "hello");
8736 }
8737
8738 #[tokio::test]
8739 async fn test_sh_c_simple_command() {
8740 let result = run_script("sh -c 'echo world'").await;
8742 assert_eq!(result.exit_code, 0);
8743 assert_eq!(result.stdout.trim(), "world");
8744 }
8745
8746 #[tokio::test]
8747 async fn test_bash_c_multiple_commands() {
8748 let result = run_script("bash -c 'echo one; echo two'").await;
8750 assert_eq!(result.exit_code, 0);
8751 assert_eq!(result.stdout, "one\ntwo\n");
8752 }
8753
8754 #[tokio::test]
8755 async fn test_bash_c_with_positional_args() {
8756 let result = run_script("bash -c 'echo $0 $1' zero one").await;
8758 assert_eq!(result.exit_code, 0);
8759 assert_eq!(result.stdout.trim(), "zero one");
8760 }
8761
8762 #[tokio::test]
8763 async fn test_bash_script_file() {
8764 let fs = Arc::new(InMemoryFs::new());
8766 fs.write_file(std::path::Path::new("/tmp/test.sh"), b"echo 'from script'")
8767 .await
8768 .unwrap();
8769
8770 let mut interpreter = Interpreter::new(fs.clone());
8771 let parser = Parser::new("bash /tmp/test.sh");
8772 let script = parser.parse().unwrap();
8773 let result = interpreter.execute(&script).await.unwrap();
8774
8775 assert_eq!(result.exit_code, 0);
8776 assert_eq!(result.stdout.trim(), "from script");
8777 }
8778
8779 #[tokio::test]
8780 async fn test_bash_script_file_with_args() {
8781 let fs = Arc::new(InMemoryFs::new());
8783 fs.write_file(std::path::Path::new("/tmp/args.sh"), b"echo $1 $2")
8784 .await
8785 .unwrap();
8786
8787 let mut interpreter = Interpreter::new(fs.clone());
8788 let parser = Parser::new("bash /tmp/args.sh first second");
8789 let script = parser.parse().unwrap();
8790 let result = interpreter.execute(&script).await.unwrap();
8791
8792 assert_eq!(result.exit_code, 0);
8793 assert_eq!(result.stdout.trim(), "first second");
8794 }
8795
8796 #[tokio::test]
8797 async fn test_bash_piped_script() {
8798 let result = run_script("echo 'echo piped' | bash").await;
8800 assert_eq!(result.exit_code, 0);
8801 assert_eq!(result.stdout.trim(), "piped");
8802 }
8803
8804 #[tokio::test]
8805 async fn test_bash_nonexistent_file() {
8806 let result = run_script("bash /nonexistent/missing.sh").await;
8808 assert_eq!(result.exit_code, 127);
8809 assert!(result.stderr.contains("No such file"));
8810 }
8811
8812 #[tokio::test]
8813 async fn test_bash_c_missing_argument() {
8814 let result = run_script("bash -c").await;
8816 assert_eq!(result.exit_code, 2);
8817 assert!(result.stderr.contains("option requires an argument"));
8818 }
8819
8820 #[tokio::test]
8821 async fn test_bash_c_syntax_error() {
8822 let result = run_script("bash -c 'if then'").await;
8824 assert_eq!(result.exit_code, 2);
8825 assert!(result.stderr.contains("syntax error"));
8826 }
8827
8828 #[tokio::test]
8829 async fn test_bash_preserves_variables() {
8830 let result = run_script("bash -c 'X=inner'; echo $X").await;
8833 assert_eq!(result.exit_code, 0);
8834 assert_eq!(result.stdout.trim(), "inner");
8835 }
8836
8837 #[tokio::test]
8838 async fn test_bash_c_exit_code_propagates() {
8839 let result = run_script("bash -c 'exit 42'; echo $?").await;
8841 assert_eq!(result.stdout.trim(), "42");
8842 }
8843
8844 #[tokio::test]
8845 async fn test_bash_nested() {
8846 let result = run_script("bash -c \"bash -c 'echo nested'\"").await;
8848 assert_eq!(result.exit_code, 0);
8849 assert_eq!(result.stdout.trim(), "nested");
8850 }
8851
8852 #[tokio::test]
8853 async fn test_sh_script_file() {
8854 let fs = Arc::new(InMemoryFs::new());
8856 fs.write_file(std::path::Path::new("/tmp/sh_test.sh"), b"echo 'sh works'")
8857 .await
8858 .unwrap();
8859
8860 let mut interpreter = Interpreter::new(fs.clone());
8861 let parser = Parser::new("sh /tmp/sh_test.sh");
8862 let script = parser.parse().unwrap();
8863 let result = interpreter.execute(&script).await.unwrap();
8864
8865 assert_eq!(result.exit_code, 0);
8866 assert_eq!(result.stdout.trim(), "sh works");
8867 }
8868
8869 #[tokio::test]
8870 async fn test_bash_with_option_e() {
8871 let result = run_script("bash -e -c 'echo works'").await;
8873 assert_eq!(result.exit_code, 0);
8874 assert_eq!(result.stdout.trim(), "works");
8875 }
8876
8877 #[tokio::test]
8878 async fn test_bash_empty_input() {
8879 let result = run_script("bash; echo done").await;
8881 assert_eq!(result.exit_code, 0);
8882 assert_eq!(result.stdout.trim(), "done");
8883 }
8884
8885 #[tokio::test]
8888 async fn test_bash_n_syntax_check_success() {
8889 let result = run_script("bash -n -c 'echo should not print'").await;
8891 assert_eq!(result.exit_code, 0);
8892 assert_eq!(result.stdout, ""); }
8894
8895 #[tokio::test]
8896 async fn test_bash_n_syntax_error_detected() {
8897 let result = run_script("bash -n -c 'if then'").await;
8899 assert_eq!(result.exit_code, 2);
8900 assert!(result.stderr.contains("syntax error"));
8901 }
8902
8903 #[tokio::test]
8904 async fn test_bash_n_combined_flags() {
8905 let result = run_script("bash -ne -c 'echo test'; echo done").await;
8907 assert_eq!(result.exit_code, 0);
8908 assert_eq!(result.stdout.trim(), "done"); }
8910
8911 #[tokio::test]
8912 async fn test_bash_version() {
8913 let result = run_script("bash --version").await;
8915 assert_eq!(result.exit_code, 0);
8916 assert!(result.stdout.contains("Bashkit"));
8917 assert!(result.stdout.contains("virtual"));
8918 }
8919
8920 #[tokio::test]
8921 async fn test_sh_version() {
8922 let result = run_script("sh --version").await;
8924 assert_eq!(result.exit_code, 0);
8925 assert!(result.stdout.contains("virtual sh"));
8926 }
8927
8928 #[tokio::test]
8929 async fn test_bash_help() {
8930 let result = run_script("bash --help").await;
8932 assert_eq!(result.exit_code, 0);
8933 assert!(result.stdout.contains("Usage:"));
8934 assert!(result.stdout.contains("-c string"));
8935 assert!(result.stdout.contains("-n"));
8936 }
8937
8938 #[tokio::test]
8939 async fn test_bash_double_dash() {
8940 let result = run_script("bash -- --help").await;
8942 assert_eq!(result.exit_code, 127);
8944 }
8945
8946 #[tokio::test]
8949 async fn test_bash_invalid_syntax_in_file() {
8950 let fs = Arc::new(InMemoryFs::new());
8952 fs.write_file(std::path::Path::new("/tmp/bad.sh"), b"if true; then echo x")
8953 .await
8954 .unwrap();
8955
8956 let mut interpreter = Interpreter::new(fs.clone());
8957 let parser = Parser::new("bash /tmp/bad.sh");
8958 let script = parser.parse().unwrap();
8959 let result = interpreter.execute(&script).await.unwrap();
8960
8961 assert_eq!(result.exit_code, 2);
8962 assert!(result.stderr.contains("syntax error"));
8963 }
8964
8965 #[tokio::test]
8966 async fn test_bash_permission_in_sandbox() {
8967 let result = run_script("bash -c 'echo test > /tmp/out.txt && cat /tmp/out.txt'").await;
8969 assert_eq!(result.exit_code, 0);
8970 assert_eq!(result.stdout.trim(), "test");
8971 }
8972
8973 #[tokio::test]
8974 async fn test_bash_all_positional() {
8975 let result = run_script("bash -c 'echo $@' _ a b c").await;
8977 assert_eq!(result.exit_code, 0);
8978 assert_eq!(result.stdout.trim(), "a b c");
8979 }
8980
8981 #[tokio::test]
8982 async fn test_bash_arg_count() {
8983 let result = run_script("bash -c 'echo $#' _ 1 2 3 4").await;
8985 assert_eq!(result.exit_code, 0);
8986 assert_eq!(result.stdout.trim(), "4");
8987 }
8988
8989 #[tokio::test]
8992 async fn test_bash_no_real_bash_escape() {
8993 let result = run_script("bash -c 'which bash 2>/dev/null || echo not found'").await;
8996 assert!(result.stdout.contains("not found") || result.exit_code == 127);
8998 }
8999
9000 #[tokio::test]
9001 async fn test_bash_nested_limits_respected() {
9002 let result = run_script("bash -c 'for i in 1 2 3; do echo $i; done'").await;
9005 assert_eq!(result.exit_code, 0);
9006 }
9008
9009 #[tokio::test]
9010 async fn test_bash_c_injection_safe() {
9011 let result = run_script("INJECT='; rm -rf /'; bash -c 'echo safe'").await;
9013 assert_eq!(result.exit_code, 0);
9014 assert_eq!(result.stdout.trim(), "safe");
9015 }
9016
9017 #[tokio::test]
9018 async fn test_bash_version_no_host_info() {
9019 let result = run_script("bash --version").await;
9021 assert!(!result.stdout.contains("/usr"));
9022 assert!(!result.stdout.contains("GNU"));
9023 }
9025
9026 #[tokio::test]
9029 async fn test_bash_c_with_quotes() {
9030 let result = run_script(r#"bash -c 'echo "hello world"'"#).await;
9032 assert_eq!(result.exit_code, 0);
9033 assert_eq!(result.stdout.trim(), "hello world");
9034 }
9035
9036 #[tokio::test]
9037 async fn test_bash_c_with_variables() {
9038 let result = run_script("X=test; bash -c 'echo $X'").await;
9040 assert_eq!(result.exit_code, 0);
9041 assert_eq!(result.stdout.trim(), "test");
9042 }
9043
9044 #[tokio::test]
9045 async fn test_bash_c_pipe_in_command() {
9046 let result = run_script("bash -c 'echo hello | cat'").await;
9048 assert_eq!(result.exit_code, 0);
9049 assert_eq!(result.stdout.trim(), "hello");
9050 }
9051
9052 #[tokio::test]
9053 async fn test_bash_c_subshell() {
9054 let result = run_script("bash -c 'echo $(echo inner)'").await;
9056 assert_eq!(result.exit_code, 0);
9057 assert_eq!(result.stdout.trim(), "inner");
9058 }
9059
9060 #[tokio::test]
9061 async fn test_bash_c_conditional() {
9062 let result = run_script("bash -c 'if true; then echo yes; fi'").await;
9064 assert_eq!(result.exit_code, 0);
9065 assert_eq!(result.stdout.trim(), "yes");
9066 }
9067
9068 #[tokio::test]
9069 async fn test_bash_script_with_shebang() {
9070 let fs = Arc::new(InMemoryFs::new());
9072 fs.write_file(
9073 std::path::Path::new("/tmp/shebang.sh"),
9074 b"#!/bin/bash\necho works",
9075 )
9076 .await
9077 .unwrap();
9078
9079 let mut interpreter = Interpreter::new(fs.clone());
9080 let parser = Parser::new("bash /tmp/shebang.sh");
9081 let script = parser.parse().unwrap();
9082 let result = interpreter.execute(&script).await.unwrap();
9083
9084 assert_eq!(result.exit_code, 0);
9085 assert_eq!(result.stdout.trim(), "works");
9086 }
9087
9088 #[tokio::test]
9089 async fn test_bash_n_with_valid_multiline() {
9090 let result = run_script("bash -n -c 'echo one\necho two\necho three'").await;
9092 assert_eq!(result.exit_code, 0);
9093 }
9094
9095 #[tokio::test]
9096 async fn test_sh_behaves_like_bash() {
9097 let bash_result = run_script("bash -c 'echo $((1+2))'").await;
9099 let sh_result = run_script("sh -c 'echo $((1+2))'").await;
9100 assert_eq!(bash_result.stdout, sh_result.stdout);
9101 assert_eq!(bash_result.exit_code, sh_result.exit_code);
9102 }
9103
9104 #[tokio::test]
9107 async fn test_bash_n_unclosed_if() {
9108 let result = run_script("bash -n -c 'if true; then echo x'").await;
9110 assert_eq!(result.exit_code, 2);
9111 assert!(result.stderr.contains("syntax error"));
9112 }
9113
9114 #[tokio::test]
9115 async fn test_bash_n_unclosed_while() {
9116 let result = run_script("bash -n -c 'while true; do echo x'").await;
9118 assert_eq!(result.exit_code, 2);
9119 }
9120
9121 #[tokio::test]
9122 async fn test_bash_empty_c_string() {
9123 let result = run_script("bash -c ''").await;
9125 assert_eq!(result.exit_code, 0);
9126 assert_eq!(result.stdout, "");
9127 }
9128
9129 #[tokio::test]
9130 async fn test_bash_whitespace_only_c_string() {
9131 let result = run_script("bash -c ' '").await;
9133 assert_eq!(result.exit_code, 0);
9134 }
9135
9136 #[tokio::test]
9137 async fn test_bash_directory_not_file() {
9138 let result = run_script("bash /tmp").await;
9140 assert_ne!(result.exit_code, 0);
9142 }
9143
9144 #[tokio::test]
9145 async fn test_bash_c_exit_propagates() {
9146 let result = run_script("bash -c 'exit 42'; echo \"code: $?\"").await;
9148 assert_eq!(result.exit_code, 0);
9149 assert!(result.stdout.contains("code: 42"));
9150 }
9151
9152 #[tokio::test]
9153 async fn test_bash_multiple_scripts_sequential() {
9154 let result = run_script("bash -c 'echo 1'; bash -c 'echo 2'; bash -c 'echo 3'").await;
9156 assert_eq!(result.exit_code, 0);
9157 assert_eq!(result.stdout, "1\n2\n3\n");
9158 }
9159
9160 #[tokio::test]
9163 async fn test_bash_c_path_traversal_blocked() {
9164 let result =
9166 run_script("bash -c 'cat /../../etc/passwd 2>/dev/null || echo blocked'").await;
9167 assert!(result.stdout.contains("blocked") || result.exit_code != 0);
9168 }
9169
9170 #[tokio::test]
9171 async fn test_bash_nested_deeply() {
9172 let result = run_script("bash -c \"bash -c 'bash -c \\\"echo deep\\\"'\"").await;
9174 assert_eq!(result.exit_code, 0);
9175 assert_eq!(result.stdout.trim(), "deep");
9176 }
9177
9178 #[tokio::test]
9179 async fn test_bash_c_special_chars() {
9180 let result = run_script("bash -c 'echo \"$HOME\"'").await;
9182 assert!(!result.stdout.contains("/root"));
9184 assert!(result.stdout.contains("/home/sandbox"));
9185 }
9186
9187 #[tokio::test]
9188 async fn test_bash_c_dollar_substitution() {
9189 let result = run_script("bash -c 'echo $(echo subst)'").await;
9191 assert_eq!(result.exit_code, 0);
9192 assert_eq!(result.stdout.trim(), "subst");
9193 }
9194
9195 #[tokio::test]
9196 async fn test_bash_help_contains_expected_options() {
9197 let result = run_script("bash --help").await;
9199 assert!(result.stdout.contains("-c"));
9200 assert!(result.stdout.contains("-n"));
9201 assert!(result.stdout.contains("--version"));
9202 }
9203
9204 #[tokio::test]
9205 async fn test_bash_c_array_operations() {
9206 let result = run_script("bash -c 'arr=(a b c); echo ${arr[1]}'").await;
9208 assert_eq!(result.exit_code, 0);
9209 assert_eq!(result.stdout.trim(), "b");
9210 }
9211
9212 #[tokio::test]
9213 async fn test_bash_positional_special_vars() {
9214 let result = run_script("bash -c 'echo \"args: $#, first: $1, all: $*\"' prog a b c").await;
9216 assert_eq!(result.exit_code, 0);
9217 assert!(result.stdout.contains("args: 3"));
9218 assert!(result.stdout.contains("first: a"));
9219 assert!(result.stdout.contains("all: a b c"));
9220 }
9221
9222 #[tokio::test]
9223 async fn test_xtrace_basic() {
9224 let result = run_script("set -x; echo hello").await;
9226 assert_eq!(result.exit_code, 0);
9227 assert_eq!(result.stdout, "hello\n");
9228 assert!(
9229 result.stderr.contains("+ echo hello"),
9230 "stderr should contain xtrace: {:?}",
9231 result.stderr
9232 );
9233 }
9234
9235 #[tokio::test]
9236 async fn test_xtrace_multiple_commands() {
9237 let result = run_script("set -x; echo one; echo two").await;
9238 assert_eq!(result.stdout, "one\ntwo\n");
9239 assert!(result.stderr.contains("+ echo one"));
9240 assert!(result.stderr.contains("+ echo two"));
9241 }
9242
9243 #[tokio::test]
9244 async fn test_xtrace_expanded_variables() {
9245 let result = run_script("x=hello; set -x; echo $x").await;
9247 assert_eq!(result.stdout, "hello\n");
9248 assert!(
9249 result.stderr.contains("+ echo hello"),
9250 "xtrace should show expanded value: {:?}",
9251 result.stderr
9252 );
9253 }
9254
9255 #[tokio::test]
9256 async fn test_xtrace_disable() {
9257 let result = run_script("set -x; echo traced; set +x; echo not_traced").await;
9259 assert_eq!(result.stdout, "traced\nnot_traced\n");
9260 assert!(result.stderr.contains("+ echo traced"));
9261 assert!(
9262 result.stderr.contains("+ set +x"),
9263 "set +x should be traced: {:?}",
9264 result.stderr
9265 );
9266 assert!(
9267 !result.stderr.contains("+ echo not_traced"),
9268 "echo after set +x should NOT be traced: {:?}",
9269 result.stderr
9270 );
9271 }
9272
9273 #[tokio::test]
9274 async fn test_xtrace_no_trace_without_flag() {
9275 let result = run_script("echo hello").await;
9276 assert_eq!(result.stdout, "hello\n");
9277 assert!(
9278 result.stderr.is_empty(),
9279 "no xtrace without set -x: {:?}",
9280 result.stderr
9281 );
9282 }
9283
9284 #[tokio::test]
9285 async fn test_xtrace_not_captured_by_redirect() {
9286 let result = run_script("set -x; echo hello 2>&1").await;
9288 assert_eq!(result.stdout, "hello\n");
9289 assert!(
9290 result.stderr.contains("+ echo hello"),
9291 "xtrace should stay in stderr even with 2>&1: {:?}",
9292 result.stderr
9293 );
9294 }
9295
9296 #[tokio::test]
9299 async fn test_xargs_executes_command() {
9300 let fs = Arc::new(InMemoryFs::new());
9302 fs.mkdir(std::path::Path::new("/workspace"), true)
9303 .await
9304 .unwrap();
9305 fs.write_file(std::path::Path::new("/workspace/file.txt"), b"hello world")
9306 .await
9307 .unwrap();
9308
9309 let mut interp = Interpreter::new(fs.clone());
9310 let parser = Parser::new("echo /workspace/file.txt | xargs cat");
9311 let ast = parser.parse().unwrap();
9312 let result = interp.execute(&ast).await.unwrap();
9313
9314 assert_eq!(result.exit_code, 0);
9315 assert_eq!(
9316 result.stdout.trim(),
9317 "hello world",
9318 "xargs should execute cat, not echo it. Got: {:?}",
9319 result.stdout
9320 );
9321 }
9322
9323 #[tokio::test]
9324 async fn test_xargs_default_echo() {
9325 let result = run_script("echo 'a b c' | xargs").await;
9327 assert_eq!(result.exit_code, 0);
9328 assert_eq!(result.stdout.trim(), "a b c");
9329 }
9330
9331 #[tokio::test]
9332 async fn test_xargs_splits_newlines() {
9333 let fs = Arc::new(InMemoryFs::new());
9335 fs.mkdir(std::path::Path::new("/workspace"), true)
9336 .await
9337 .unwrap();
9338 fs.write_file(std::path::Path::new("/workspace/a.txt"), b"AAA")
9339 .await
9340 .unwrap();
9341 fs.write_file(std::path::Path::new("/workspace/b.txt"), b"BBB")
9342 .await
9343 .unwrap();
9344
9345 let mut interp = Interpreter::new(fs.clone());
9346 let script = "printf '/workspace/a.txt\\n/workspace/b.txt' | xargs cat";
9347 let parser = Parser::new(script);
9348 let ast = parser.parse().unwrap();
9349 let result = interp.execute(&ast).await.unwrap();
9350
9351 assert_eq!(result.exit_code, 0);
9352 assert!(
9353 result.stdout.contains("AAA"),
9354 "should contain contents of a.txt"
9355 );
9356 assert!(
9357 result.stdout.contains("BBB"),
9358 "should contain contents of b.txt"
9359 );
9360 }
9361
9362 #[tokio::test]
9363 async fn test_xargs_n1_executes_per_item() {
9364 let result = run_script("echo 'a b c' | xargs -n 1 echo item:").await;
9366 assert_eq!(result.exit_code, 0);
9367 let lines: Vec<&str> = result.stdout.trim().lines().collect();
9368 assert_eq!(lines.len(), 3);
9369 assert_eq!(lines[0], "item: a");
9370 assert_eq!(lines[1], "item: b");
9371 assert_eq!(lines[2], "item: c");
9372 }
9373
9374 #[tokio::test]
9375 async fn test_xargs_replace_str() {
9376 let fs = Arc::new(InMemoryFs::new());
9378 fs.mkdir(std::path::Path::new("/workspace"), true)
9379 .await
9380 .unwrap();
9381 fs.write_file(std::path::Path::new("/workspace/hello.txt"), b"Hello!")
9382 .await
9383 .unwrap();
9384
9385 let mut interp = Interpreter::new(fs.clone());
9386 let script = "echo /workspace/hello.txt | xargs -I {} cat {}";
9387 let parser = Parser::new(script);
9388 let ast = parser.parse().unwrap();
9389 let result = interp.execute(&ast).await.unwrap();
9390
9391 assert_eq!(result.exit_code, 0);
9392 assert_eq!(result.stdout.trim(), "Hello!");
9393 }
9394
9395 #[tokio::test]
9398 async fn test_find_exec_per_file() {
9399 let fs = Arc::new(InMemoryFs::new());
9401 fs.mkdir(std::path::Path::new("/project"), true)
9402 .await
9403 .unwrap();
9404 fs.write_file(std::path::Path::new("/project/a.txt"), b"content-a")
9405 .await
9406 .unwrap();
9407 fs.write_file(std::path::Path::new("/project/b.txt"), b"content-b")
9408 .await
9409 .unwrap();
9410
9411 let mut interp = Interpreter::new(fs.clone());
9412 interp.set_cwd(std::path::PathBuf::from("/"));
9413
9414 let script = r#"find /project -name "*.txt" -exec echo {} \;"#;
9415 let parser = Parser::new(script);
9416 let ast = parser.parse().unwrap();
9417 let result = interp.execute(&ast).await.unwrap();
9418
9419 assert_eq!(result.exit_code, 0);
9420 let lines: Vec<&str> = result.stdout.trim().lines().collect();
9421 assert_eq!(lines.len(), 2);
9422 assert!(result.stdout.contains("/project/a.txt"));
9423 assert!(result.stdout.contains("/project/b.txt"));
9424 }
9425
9426 #[tokio::test]
9427 async fn test_find_exec_batch_mode() {
9428 let fs = Arc::new(InMemoryFs::new());
9430 fs.mkdir(std::path::Path::new("/project"), true)
9431 .await
9432 .unwrap();
9433 fs.write_file(std::path::Path::new("/project/a.txt"), b"aaa")
9434 .await
9435 .unwrap();
9436 fs.write_file(std::path::Path::new("/project/b.txt"), b"bbb")
9437 .await
9438 .unwrap();
9439
9440 let mut interp = Interpreter::new(fs.clone());
9441 interp.set_cwd(std::path::PathBuf::from("/"));
9442
9443 let script = r#"find /project -name "*.txt" -exec echo {} +"#;
9444 let parser = Parser::new(script);
9445 let ast = parser.parse().unwrap();
9446 let result = interp.execute(&ast).await.unwrap();
9447
9448 assert_eq!(result.exit_code, 0);
9449 let lines: Vec<&str> = result.stdout.trim().lines().collect();
9451 assert_eq!(lines.len(), 1);
9452 assert!(result.stdout.contains("/project/a.txt"));
9453 assert!(result.stdout.contains("/project/b.txt"));
9454 }
9455
9456 #[tokio::test]
9457 async fn test_find_exec_cat_reads_files() {
9458 let fs = Arc::new(InMemoryFs::new());
9460 fs.mkdir(std::path::Path::new("/data"), true).await.unwrap();
9461 fs.write_file(std::path::Path::new("/data/hello.txt"), b"Hello World")
9462 .await
9463 .unwrap();
9464
9465 let mut interp = Interpreter::new(fs.clone());
9466 interp.set_cwd(std::path::PathBuf::from("/"));
9467
9468 let script = r#"find /data -name "hello.txt" -exec cat {} \;"#;
9469 let parser = Parser::new(script);
9470 let ast = parser.parse().unwrap();
9471 let result = interp.execute(&ast).await.unwrap();
9472
9473 assert_eq!(result.exit_code, 0);
9474 assert_eq!(result.stdout, "Hello World");
9475 }
9476
9477 #[tokio::test]
9478 async fn test_find_exec_with_type_filter() {
9479 let fs = Arc::new(InMemoryFs::new());
9481 fs.mkdir(std::path::Path::new("/root/subdir"), true)
9482 .await
9483 .unwrap();
9484 fs.write_file(std::path::Path::new("/root/file.txt"), b"data")
9485 .await
9486 .unwrap();
9487
9488 let mut interp = Interpreter::new(fs.clone());
9489 interp.set_cwd(std::path::PathBuf::from("/"));
9490
9491 let script = r#"find /root -type f -exec echo found {} \;"#;
9492 let parser = Parser::new(script);
9493 let ast = parser.parse().unwrap();
9494 let result = interp.execute(&ast).await.unwrap();
9495
9496 assert_eq!(result.exit_code, 0);
9497 assert!(result.stdout.contains("found /root/file.txt"));
9498 assert!(!result.stdout.contains("found /root/subdir"));
9499 }
9500
9501 #[tokio::test]
9502 async fn test_find_exec_nonexistent_path() {
9503 let fs = Arc::new(InMemoryFs::new());
9504 let mut interp = Interpreter::new(fs.clone());
9505 interp.set_cwd(std::path::PathBuf::from("/"));
9506
9507 let script = r#"find /nonexistent -exec echo {} \;"#;
9508 let parser = Parser::new(script);
9509 let ast = parser.parse().unwrap();
9510 let result = interp.execute(&ast).await.unwrap();
9511
9512 assert_eq!(result.exit_code, 1);
9513 assert!(result.stderr.contains("No such file or directory"));
9514 }
9515
9516 #[tokio::test]
9517 async fn test_find_exec_no_matches() {
9518 let fs = Arc::new(InMemoryFs::new());
9519 fs.mkdir(std::path::Path::new("/empty"), true)
9520 .await
9521 .unwrap();
9522
9523 let mut interp = Interpreter::new(fs.clone());
9524 interp.set_cwd(std::path::PathBuf::from("/"));
9525
9526 let script = r#"find /empty -name "*.xyz" -exec echo {} \;"#;
9527 let parser = Parser::new(script);
9528 let ast = parser.parse().unwrap();
9529 let result = interp.execute(&ast).await.unwrap();
9530
9531 assert_eq!(result.exit_code, 0);
9532 assert_eq!(result.stdout, "");
9533 }
9534
9535 #[tokio::test]
9536 async fn test_find_exec_multiple_placeholder() {
9537 let fs = Arc::new(InMemoryFs::new());
9539 fs.mkdir(std::path::Path::new("/src"), true).await.unwrap();
9540 fs.write_file(std::path::Path::new("/src/test.txt"), b"hi")
9541 .await
9542 .unwrap();
9543
9544 let mut interp = Interpreter::new(fs.clone());
9545 interp.set_cwd(std::path::PathBuf::from("/"));
9546
9547 let script = r#"find /src -name "test.txt" -exec echo {} {} \;"#;
9548 let parser = Parser::new(script);
9549 let ast = parser.parse().unwrap();
9550 let result = interp.execute(&ast).await.unwrap();
9551
9552 assert_eq!(result.exit_code, 0);
9553 assert_eq!(result.stdout.trim(), "/src/test.txt /src/test.txt");
9554 }
9555
9556 #[tokio::test]
9557 async fn test_star_join_with_ifs() {
9558 let result = run_script("set -- x y z\nIFS=:\necho \"$*\"").await;
9560 assert_eq!(result.stdout, "x:y:z\n");
9561 let result = run_script("set -- x y z\nIFS=\necho \"$*\"").await;
9562 assert_eq!(result.stdout, "xyz\n");
9563 let result = run_script("set -- x y z\necho [\"$*\"]").await;
9565 assert_eq!(result.stdout, "[x y z]\n");
9566 let result = run_script("IFS=:\nset -- x 'y z'\ns=\"$*\"\necho \"star=$s\"").await;
9568 assert_eq!(result.stdout, "star=x:y z\n");
9569 let result = run_script("set a b c\necho $#\necho $1 $2 $3").await;
9571 assert_eq!(result.stdout, "3\na b c\n");
9572 }
9573
9574 #[tokio::test]
9575 async fn test_arithmetic_exponent_negative_no_panic() {
9576 let result = run_script("echo $(( 2 ** -1 ))").await;
9577 assert_eq!(result.exit_code, 0);
9578 }
9579
9580 #[tokio::test]
9581 async fn test_arithmetic_exponent_large_no_panic() {
9582 let result = run_script("echo $(( 2 ** 100 ))").await;
9583 assert_eq!(result.exit_code, 0);
9584 }
9585
9586 #[tokio::test]
9587 async fn test_arithmetic_shift_large_no_panic() {
9588 let result = run_script("echo $(( 1 << 64 ))").await;
9589 assert_eq!(result.exit_code, 0);
9590 }
9591
9592 #[tokio::test]
9593 async fn test_arithmetic_shift_negative_no_panic() {
9594 let result = run_script("echo $(( 1 << -1 ))").await;
9595 assert_eq!(result.exit_code, 0);
9596 }
9597
9598 #[tokio::test]
9599 async fn test_arithmetic_div_min_neg1_no_panic() {
9600 let result = run_script("echo $(( -9223372036854775808 / -1 ))").await;
9601 assert_eq!(result.exit_code, 0);
9602 }
9603
9604 #[tokio::test]
9605 async fn test_arithmetic_mod_min_neg1_no_panic() {
9606 let result = run_script("echo $(( -9223372036854775808 % -1 ))").await;
9607 assert_eq!(result.exit_code, 0);
9608 }
9609
9610 #[tokio::test]
9611 async fn test_arithmetic_overflow_add_no_panic() {
9612 let result = run_script("echo $(( 9223372036854775807 + 1 ))").await;
9613 assert_eq!(result.exit_code, 0);
9614 }
9615
9616 #[tokio::test]
9617 async fn test_arithmetic_overflow_mul_no_panic() {
9618 let result = run_script("echo $(( 9223372036854775807 * 2 ))").await;
9619 assert_eq!(result.exit_code, 0);
9620 }
9621
9622 #[tokio::test]
9625 async fn test_arithmetic_base_gt_36_no_panic() {
9626 let result = run_script("echo $(( 64#A ))").await;
9627 assert_eq!(result.exit_code, 0);
9628 assert_eq!(result.stdout.trim(), "36");
9630 }
9631
9632 #[tokio::test]
9633 async fn test_arithmetic_base_gt_36_special_chars() {
9634 let result = run_script("echo $(( 64#@ ))").await;
9636 assert_eq!(result.stdout.trim(), "62");
9637 let result = run_script("echo $(( 64#_ ))").await;
9638 assert_eq!(result.stdout.trim(), "63");
9639 }
9640
9641 #[tokio::test]
9642 async fn test_arithmetic_base_gt_36_invalid_digit() {
9643 let result = run_script("echo $(( 37#! ))").await;
9645 assert_eq!(result.exit_code, 0);
9646 }
9647
9648 #[tokio::test]
9649 async fn test_eval_respects_parser_limits() {
9650 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
9651 let mut interp = Interpreter::new(Arc::clone(&fs));
9652 interp.limits.max_ast_depth = 5;
9653 let parser = Parser::new("eval 'echo hello'");
9654 let ast = parser.parse().unwrap();
9655 let result = interp.execute(&ast).await.unwrap();
9656 assert_eq!(result.exit_code, 0);
9657 }
9658
9659 #[tokio::test]
9660 async fn test_source_respects_parser_limits() {
9661 let fs: Arc<dyn FileSystem> = Arc::new(InMemoryFs::new());
9662 fs.write_file(std::path::Path::new("/tmp/test.sh"), b"echo sourced")
9663 .await
9664 .unwrap();
9665 let mut interp = Interpreter::new(Arc::clone(&fs));
9666 interp.limits.max_ast_depth = 5;
9667 let parser = Parser::new("source /tmp/test.sh");
9668 let ast = parser.parse().unwrap();
9669 let result = interp.execute(&ast).await.unwrap();
9670 assert_eq!(result.exit_code, 0);
9671 assert_eq!(result.stdout.trim(), "sourced");
9672 }
9673
9674 #[tokio::test]
9675 async fn test_internal_var_prefix_not_exposed() {
9676 let result = run_script("echo \"${!_NAMEREF*}\"").await;
9678 assert_eq!(result.stdout.trim(), "");
9679 }
9680
9681 #[tokio::test]
9682 async fn test_internal_var_readonly_not_exposed() {
9683 let result = run_script("echo \"${!_READONLY*}\"").await;
9684 assert_eq!(result.stdout.trim(), "");
9685 }
9686
9687 #[tokio::test]
9688 async fn test_internal_var_assignment_blocked() {
9689 let result = run_script("_NAMEREF_x=PATH; echo ${!x}").await;
9691 assert!(!result.stdout.contains("/usr"));
9692 }
9693
9694 #[tokio::test]
9695 async fn test_internal_var_readonly_injection_blocked() {
9696 let result = run_script("_READONLY_myvar=1; myvar=hello; echo $myvar").await;
9698 assert_eq!(result.stdout.trim(), "hello");
9699 }
9700
9701 #[tokio::test]
9702 async fn test_extglob_no_hang() {
9703 use std::time::{Duration, Instant};
9704 let start = Instant::now();
9705 let result = run_script(
9706 r#"shopt -s extglob; [[ "aaaaaaaaaaaa" == +(a|aa) ]] && echo yes || echo no"#,
9707 )
9708 .await;
9709 let elapsed = start.elapsed();
9710 assert!(
9711 elapsed < Duration::from_secs(5),
9712 "extglob took too long: {:?}",
9713 elapsed
9714 );
9715 assert_eq!(result.exit_code, 0);
9716 }
9717
9718 #[tokio::test]
9720 async fn test_dollar_dollar_no_host_pid_leak() {
9721 let mut bash = crate::Bash::new();
9722 let result = bash.exec("echo $$").await.unwrap();
9723 let pid: u32 = result.stdout.trim().parse().unwrap();
9724 assert_eq!(pid, 1, "$$ should return sandboxed PID, not real host PID");
9726 }
9727
9728 #[tokio::test]
9730 async fn test_cyclic_nameref_detected() {
9731 let mut bash = crate::Bash::new();
9732 let result = bash
9734 .exec("declare -n a=b; declare -n b=a; a=hello; echo $a")
9735 .await
9736 .unwrap();
9737 assert_eq!(result.exit_code, 0);
9740 }
9741
9742 #[tokio::test]
9744 async fn test_arithmetic_compound_assign_ascii() {
9745 let mut bash = crate::Bash::new();
9746 let result = bash.exec("x=10; (( x += 5 )); echo $x").await.unwrap();
9747 assert_eq!(result.stdout.trim(), "15");
9748 }
9749
9750 #[tokio::test]
9751 async fn test_getopts_while_loop() {
9752 let mut bash = crate::Bash::new();
9754 let result = bash
9755 .exec(
9756 r#"
9757set -- -f json -v
9758while getopts "f:vh" opt; do
9759 case "$opt" in
9760 f) FORMAT="$OPTARG" ;;
9761 v) VERBOSE=1 ;;
9762 esac
9763done
9764echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9765"#,
9766 )
9767 .await
9768 .unwrap();
9769 assert_eq!(result.exit_code, 0);
9770 assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9771 }
9772
9773 #[tokio::test]
9774 async fn test_getopts_script_with_args() {
9775 let mut bash = crate::Bash::new();
9777 let result = bash
9779 .exec(
9780 r#"
9781cat > /tmp/test_getopts.sh << 'SCRIPT'
9782while getopts "f:vh" opt; do
9783 case "$opt" in
9784 f) FORMAT="$OPTARG" ;;
9785 v) VERBOSE=1 ;;
9786 esac
9787done
9788echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9789SCRIPT
9790bash /tmp/test_getopts.sh -f json -v
9791"#,
9792 )
9793 .await
9794 .unwrap();
9795 assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9796 }
9797
9798 #[tokio::test]
9799 async fn test_getopts_bash_c_with_args() {
9800 let mut bash = crate::Bash::new();
9802 let result = bash
9803 .exec(
9804 r#"bash -c '
9805FORMAT="csv"
9806VERBOSE=0
9807while getopts "f:vh" opt; do
9808 case "$opt" in
9809 f) FORMAT="$OPTARG" ;;
9810 v) VERBOSE=1 ;;
9811 esac
9812done
9813echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9814' -- -f json -v"#,
9815 )
9816 .await
9817 .unwrap();
9818 assert_eq!(result.stdout.trim(), "FORMAT=json VERBOSE=1");
9819 }
9820
9821 #[tokio::test]
9822 async fn test_getopts_optind_reset_between_scripts() {
9823 let mut bash = crate::Bash::new();
9826 let result = bash
9827 .exec(
9828 r#"
9829cat > /tmp/opts.sh << 'SCRIPT'
9830FORMAT="csv"
9831VERBOSE=0
9832while getopts "f:vh" opt; do
9833 case "$opt" in
9834 f) FORMAT="$OPTARG" ;;
9835 v) VERBOSE=1 ;;
9836 esac
9837done
9838echo "FORMAT=$FORMAT VERBOSE=$VERBOSE"
9839SCRIPT
9840bash /tmp/opts.sh -f json -v
9841bash /tmp/opts.sh -f xml -v
9842"#,
9843 )
9844 .await
9845 .unwrap();
9846 let lines: Vec<&str> = result.stdout.trim().lines().collect();
9847 assert_eq!(lines.len(), 2, "expected 2 lines: {}", result.stdout);
9848 assert_eq!(lines[0], "FORMAT=json VERBOSE=1");
9849 assert_eq!(lines[1], "FORMAT=xml VERBOSE=1");
9850 }
9851
9852 #[tokio::test]
9853 async fn test_wc_l_in_pipe() {
9854 let mut bash = crate::Bash::new();
9855 let result = bash.exec(r#"echo -e "a\nb\nc" | wc -l"#).await.unwrap();
9856 assert_eq!(result.exit_code, 0);
9857 assert_eq!(result.stdout.trim(), "3");
9858 }
9859
9860 #[tokio::test]
9861 async fn test_wc_l_in_pipe_subst() {
9862 let mut bash = crate::Bash::new();
9863 let result = bash
9864 .exec(
9865 r#"
9866cat > /tmp/data.csv << 'EOF'
9867name,score
9868alice,95
9869bob,87
9870carol,92
9871EOF
9872COUNT=$(tail -n +2 /tmp/data.csv | wc -l)
9873echo "count=$COUNT"
9874"#,
9875 )
9876 .await
9877 .unwrap();
9878 assert_eq!(result.exit_code, 0);
9879 assert_eq!(result.stdout.trim(), "count=3");
9880 }
9881
9882 #[tokio::test]
9883 async fn test_wc_l_counts_newlines() {
9884 let mut bash = crate::Bash::new();
9885 let result = bash.exec(r#"printf "a\nb\nc" | wc -l"#).await.unwrap();
9886 assert_eq!(result.stdout.trim(), "2");
9887 }
9888
9889 #[tokio::test]
9890 async fn test_regex_match_from_variable() {
9891 let mut bash = crate::Bash::new();
9892 let result = bash
9893 .exec(r#"re="200"; line="hello 200 world"; [[ $line =~ $re ]] && echo "match" || echo "no""#)
9894 .await
9895 .unwrap();
9896 assert_eq!(result.stdout.trim(), "match");
9897 }
9898
9899 #[tokio::test]
9900 async fn test_regex_match_literal() {
9901 let mut bash = crate::Bash::new();
9902 let result = bash
9903 .exec(r#"line="hello 200 world"; [[ $line =~ 200 ]] && echo "match" || echo "no""#)
9904 .await
9905 .unwrap();
9906 assert_eq!(result.stdout.trim(), "match");
9907 }
9908
9909 #[tokio::test]
9910 async fn test_assoc_array_in_double_quotes() {
9911 let mut bash = crate::Bash::new();
9912 let result = bash
9913 .exec(r#"declare -A arr; arr["foo"]="bar"; echo "value: ${arr["foo"]}""#)
9914 .await
9915 .unwrap();
9916 assert_eq!(result.stdout.trim(), "value: bar");
9917 }
9918
9919 #[tokio::test]
9920 async fn test_assoc_array_keys_in_quotes() {
9921 let mut bash = crate::Bash::new();
9922 let result = bash
9923 .exec(r#"declare -A arr; arr["a"]=1; arr["b"]=2; echo "keys: ${!arr[@]}""#)
9924 .await
9925 .unwrap();
9926 let output = result.stdout.trim();
9927 assert!(output.starts_with("keys: "), "got: {}", output);
9928 assert!(output.contains("a"), "got: {}", output);
9929 assert!(output.contains("b"), "got: {}", output);
9930 }
9931
9932 #[tokio::test]
9933 async fn test_glob_with_quoted_prefix() {
9934 let mut bash = crate::Bash::new();
9935 bash.fs()
9936 .mkdir(std::path::Path::new("/testdir"), true)
9937 .await
9938 .unwrap();
9939 bash.fs()
9940 .write_file(std::path::Path::new("/testdir/a.txt"), b"a")
9941 .await
9942 .unwrap();
9943 bash.fs()
9944 .write_file(std::path::Path::new("/testdir/b.txt"), b"b")
9945 .await
9946 .unwrap();
9947 let result = bash
9948 .exec(r#"DIR="/testdir"; for f in "$DIR"/*; do echo "$f"; done"#)
9949 .await
9950 .unwrap();
9951 let mut lines: Vec<&str> = result.stdout.trim().lines().collect();
9952 lines.sort();
9953 assert_eq!(lines, vec!["/testdir/a.txt", "/testdir/b.txt"]);
9954 }
9955}