1use crate::commands::CommandMeta;
4use crate::error::RustBashError;
5use crate::interpreter::walker::execute_program;
6use crate::interpreter::{
7 ControlFlow, ExecResult, InterpreterState, Variable, VariableAttrs, VariableValue, parse,
8 set_array_element, set_variable,
9};
10use crate::vfs::NodeType;
11use std::path::Path;
12
13pub(crate) fn execute_builtin(
17 name: &str,
18 args: &[String],
19 state: &mut InterpreterState,
20 stdin: &str,
21) -> Result<Option<ExecResult>, RustBashError> {
22 match name {
23 "exit" => builtin_exit(args, state).map(Some),
24 "cd" => builtin_cd(args, state).map(Some),
25 "export" => builtin_export(args, state).map(Some),
26 "unset" => builtin_unset(args, state).map(Some),
27 "set" => builtin_set(args, state).map(Some),
28 "shift" => builtin_shift(args, state).map(Some),
29 "readonly" => builtin_readonly(args, state).map(Some),
30 "declare" | "typeset" => builtin_declare(args, state).map(Some),
31 "read" => builtin_read(args, state, stdin).map(Some),
32 "eval" => builtin_eval(args, state).map(Some),
33 "source" | "." => builtin_source(args, state).map(Some),
34 "break" => builtin_break(args, state).map(Some),
35 "continue" => builtin_continue(args, state).map(Some),
36 ":" | "colon" => Ok(Some(ExecResult::default())),
37 "let" => builtin_let(args, state).map(Some),
38 "local" => builtin_local(args, state).map(Some),
39 "return" => builtin_return(args, state).map(Some),
40 "trap" => builtin_trap(args, state).map(Some),
41 "shopt" => builtin_shopt(args, state).map(Some),
42 "type" => builtin_type(args, state).map(Some),
43 "command" => builtin_command(args, state, stdin).map(Some),
44 "builtin" => builtin_builtin(args, state, stdin).map(Some),
45 "getopts" => builtin_getopts(args, state).map(Some),
46 "mapfile" | "readarray" => builtin_mapfile(args, state, stdin).map(Some),
47 "pushd" => builtin_pushd(args, state).map(Some),
48 "popd" => builtin_popd(args, state).map(Some),
49 "dirs" => builtin_dirs(args, state).map(Some),
50 "hash" => builtin_hash(args, state).map(Some),
51 "wait" => Ok(Some(ExecResult::default())),
52 "alias" => builtin_alias(args, state).map(Some),
53 "unalias" => builtin_unalias(args, state).map(Some),
54 "printf" => builtin_printf(args, state).map(Some),
55 "sh" | "bash" => builtin_sh(args, state, stdin).map(Some),
56 "help" => builtin_help(args, state).map(Some),
57 "history" => Ok(Some(ExecResult::default())),
58 _ => Ok(None),
59 }
60}
61
62pub(crate) fn is_builtin(name: &str) -> bool {
65 builtin_names().contains(&name)
66}
67
68fn is_valid_var_name(name: &str) -> bool {
70 !name.is_empty()
71 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
72 && !name.starts_with(|c: char| c.is_ascii_digit())
73}
74
75fn validate_var_arg(arg: &str, builtin: &str) -> Result<(), String> {
79 let var_name = if let Some((n, _)) = arg.split_once("+=") {
80 n
81 } else if let Some((n, _)) = arg.split_once('=') {
82 n
83 } else {
84 arg
85 };
86 let base_name = var_name.split('[').next().unwrap_or(var_name);
88 if is_valid_var_name(base_name) {
89 Ok(())
90 } else {
91 Err(format!(
92 "rust-bash: {builtin}: `{arg}': not a valid identifier\n"
93 ))
94 }
95}
96
97fn check_help(name: &str, state: &InterpreterState) -> Option<ExecResult> {
99 if let Some(meta) = builtin_meta(name)
100 && meta.supports_help_flag
101 {
102 return Some(ExecResult {
103 stdout: crate::commands::format_help(meta),
104 ..ExecResult::default()
105 });
106 }
107 if let Some(cmd) = state.commands.get(name)
108 && let Some(meta) = cmd.meta()
109 && meta.supports_help_flag
110 {
111 return Some(ExecResult {
112 stdout: crate::commands::format_help(meta),
113 ..ExecResult::default()
114 });
115 }
116 None
117}
118
119pub fn builtin_names() -> &'static [&'static str] {
121 &[
122 "exit",
123 "cd",
124 "export",
125 "unset",
126 "set",
127 "shift",
128 "readonly",
129 "declare",
130 "typeset",
131 "read",
132 "eval",
133 "source",
134 ".",
135 "break",
136 "continue",
137 ":",
138 "colon",
139 "let",
140 "local",
141 "return",
142 "trap",
143 "shopt",
144 "type",
145 "command",
146 "builtin",
147 "getopts",
148 "mapfile",
149 "readarray",
150 "pushd",
151 "popd",
152 "dirs",
153 "hash",
154 "wait",
155 "alias",
156 "unalias",
157 "printf",
158 "exec",
159 "sh",
160 "bash",
161 "help",
162 "history",
163 ]
164}
165
166static CD_META: CommandMeta = CommandMeta {
169 name: "cd",
170 synopsis: "cd [dir]",
171 description: "Change the shell working directory.",
172 options: &[],
173 supports_help_flag: true,
174 flags: &[],
175};
176
177static EXIT_META: CommandMeta = CommandMeta {
178 name: "exit",
179 synopsis: "exit [n]",
180 description: "Exit the shell.",
181 options: &[],
182 supports_help_flag: true,
183 flags: &[],
184};
185
186static EXPORT_META: CommandMeta = CommandMeta {
187 name: "export",
188 synopsis: "export [-n] [name[=value] ...]",
189 description: "Set export attribute for shell variables.",
190 options: &[("-n", "remove the export property from each name")],
191 supports_help_flag: true,
192 flags: &[],
193};
194
195static UNSET_META: CommandMeta = CommandMeta {
196 name: "unset",
197 synopsis: "unset [-fv] [name ...]",
198 description: "Unset values and attributes of shell variables and functions.",
199 options: &[
200 ("-f", "treat each name as a shell function"),
201 ("-v", "treat each name as a shell variable"),
202 ],
203 supports_help_flag: true,
204 flags: &[],
205};
206
207static SET_META: CommandMeta = CommandMeta {
208 name: "set",
209 synopsis: "set [-euxvnCaf] [-o option-name] [--] [arg ...]",
210 description: "Set or unset values of shell options and positional parameters.",
211 options: &[
212 (
213 "-e",
214 "exit immediately if a command exits with non-zero status",
215 ),
216 ("-u", "treat unset variables as an error"),
217 (
218 "-x",
219 "print commands and their arguments as they are executed",
220 ),
221 ("-v", "print shell input lines as they are read"),
222 ("-n", "read commands but do not execute them"),
223 ("-C", "do not allow output redirection to overwrite files"),
224 ("-a", "mark variables for export"),
225 ("-f", "disable file name generation (globbing)"),
226 (
227 "-o OPTION",
228 "set option by name (errexit, nounset, pipefail, ...)",
229 ),
230 ],
231 supports_help_flag: true,
232 flags: &[],
233};
234
235static SHIFT_META: CommandMeta = CommandMeta {
236 name: "shift",
237 synopsis: "shift [n]",
238 description: "Shift positional parameters.",
239 options: &[],
240 supports_help_flag: true,
241 flags: &[],
242};
243
244static READONLY_META: CommandMeta = CommandMeta {
245 name: "readonly",
246 synopsis: "readonly [name[=value] ...]",
247 description: "Mark shell variables as unchangeable.",
248 options: &[],
249 supports_help_flag: true,
250 flags: &[],
251};
252
253static DECLARE_META: CommandMeta = CommandMeta {
254 name: "declare",
255 synopsis: "declare [-aAilnprux] [name[=value] ...]",
256 description: "Set variable values and attributes.",
257 options: &[
258 ("-a", "indexed array"),
259 ("-A", "associative array"),
260 ("-i", "integer attribute"),
261 ("-l", "convert to lower case on assignment"),
262 ("-u", "convert to upper case on assignment"),
263 ("-n", "nameref attribute"),
264 ("-r", "readonly attribute"),
265 ("-x", "export attribute"),
266 ("-p", "display attributes and values"),
267 ],
268 supports_help_flag: true,
269 flags: &[],
270};
271
272static READ_META: CommandMeta = CommandMeta {
273 name: "read",
274 synopsis: "read [-r] [-a array] [-d delim] [-n count] [-N count] [-p prompt] [name ...]",
275 description: "Read a line from standard input and split it into fields.",
276 options: &[
277 ("-r", "do not allow backslashes to escape characters"),
278 ("-a ARRAY", "assign words to indices of ARRAY"),
279 ("-d DELIM", "read until DELIM instead of newline"),
280 ("-n COUNT", "read at most COUNT characters"),
281 ("-N COUNT", "read exactly COUNT characters"),
282 ("-p PROMPT", "output PROMPT before reading"),
283 ],
284 supports_help_flag: true,
285 flags: &[],
286};
287
288static EVAL_META: CommandMeta = CommandMeta {
289 name: "eval",
290 synopsis: "eval [arg ...]",
291 description: "Execute arguments as a shell command.",
292 options: &[],
293 supports_help_flag: true,
294 flags: &[],
295};
296
297static SOURCE_META: CommandMeta = CommandMeta {
298 name: "source",
299 synopsis: "source filename [arguments]",
300 description: "Execute commands from a file in the current shell.",
301 options: &[],
302 supports_help_flag: true,
303 flags: &[],
304};
305
306static BREAK_META: CommandMeta = CommandMeta {
307 name: "break",
308 synopsis: "break [n]",
309 description: "Exit for, while, or until loops.",
310 options: &[],
311 supports_help_flag: true,
312 flags: &[],
313};
314
315static CONTINUE_META: CommandMeta = CommandMeta {
316 name: "continue",
317 synopsis: "continue [n]",
318 description: "Resume the next iteration of the enclosing loop.",
319 options: &[],
320 supports_help_flag: true,
321 flags: &[],
322};
323
324static COLON_META: CommandMeta = CommandMeta {
325 name: ":",
326 synopsis: ": [arguments]",
327 description: "No effect; the command does nothing.",
328 options: &[],
329 supports_help_flag: true,
330 flags: &[],
331};
332
333static LET_META: CommandMeta = CommandMeta {
334 name: "let",
335 synopsis: "let arg [arg ...]",
336 description: "Evaluate arithmetic expressions.",
337 options: &[],
338 supports_help_flag: true,
339 flags: &[],
340};
341
342static LOCAL_META: CommandMeta = CommandMeta {
343 name: "local",
344 synopsis: "local [name[=value] ...]",
345 description: "Define local variables.",
346 options: &[],
347 supports_help_flag: true,
348 flags: &[],
349};
350
351static RETURN_META: CommandMeta = CommandMeta {
352 name: "return",
353 synopsis: "return [n]",
354 description: "Return from a shell function.",
355 options: &[],
356 supports_help_flag: true,
357 flags: &[],
358};
359
360static TRAP_META: CommandMeta = CommandMeta {
361 name: "trap",
362 synopsis: "trap [-lp] [action signal ...]",
363 description: "Trap signals and other events.",
364 options: &[
365 ("-l", "list signal names"),
366 ("-p", "display trap commands for each signal"),
367 ],
368 supports_help_flag: true,
369 flags: &[],
370};
371
372static SHOPT_META: CommandMeta = CommandMeta {
373 name: "shopt",
374 synopsis: "shopt [-pqsu] [optname ...]",
375 description: "Set and unset shell options.",
376 options: &[
377 ("-s", "enable (set) each optname"),
378 ("-u", "disable (unset) each optname"),
379 (
380 "-q",
381 "suppresses normal output; exit status indicates match",
382 ),
383 ("-p", "display in a form that may be reused as input"),
384 ],
385 supports_help_flag: true,
386 flags: &[],
387};
388
389static TYPE_META: CommandMeta = CommandMeta {
390 name: "type",
391 synopsis: "type [-tap] name [name ...]",
392 description: "Display information about command type.",
393 options: &[
394 ("-t", "print a single word describing the type"),
395 ("-a", "display all locations containing an executable"),
396 ("-p", "print the file name of the disk file"),
397 ],
398 supports_help_flag: true,
399 flags: &[],
400};
401
402static COMMAND_META: CommandMeta = CommandMeta {
403 name: "command",
404 synopsis: "command [-vVp] command [arg ...]",
405 description: "Execute a simple command or display information about commands.",
406 options: &[
407 ("-v", "display a description of COMMAND similar to type"),
408 ("-V", "display a more verbose description"),
409 ("-p", "use a default value for PATH"),
410 ],
411 supports_help_flag: true,
412 flags: &[],
413};
414
415static BUILTIN_CMD_META: CommandMeta = CommandMeta {
416 name: "builtin",
417 synopsis: "builtin shell-builtin [arguments]",
418 description: "Execute shell builtins.",
419 options: &[],
420 supports_help_flag: true,
421 flags: &[],
422};
423
424static GETOPTS_META: CommandMeta = CommandMeta {
425 name: "getopts",
426 synopsis: "getopts optstring name [arg ...]",
427 description: "Parse option arguments.",
428 options: &[],
429 supports_help_flag: true,
430 flags: &[],
431};
432
433static MAPFILE_META: CommandMeta = CommandMeta {
434 name: "mapfile",
435 synopsis: "mapfile [-t] [-d delim] [-n count] [-s count] [array]",
436 description: "Read lines from standard input into an indexed array variable.",
437 options: &[
438 ("-t", "remove a trailing delimiter from each line"),
439 (
440 "-d DELIM",
441 "use DELIM to terminate lines instead of newline",
442 ),
443 ("-n COUNT", "copy at most COUNT lines"),
444 ("-s COUNT", "discard the first COUNT lines"),
445 ],
446 supports_help_flag: true,
447 flags: &[],
448};
449
450static PUSHD_META: CommandMeta = CommandMeta {
451 name: "pushd",
452 synopsis: "pushd [+N | -N | dir]",
453 description: "Add directories to stack.",
454 options: &[],
455 supports_help_flag: true,
456 flags: &[],
457};
458
459static POPD_META: CommandMeta = CommandMeta {
460 name: "popd",
461 synopsis: "popd [+N | -N]",
462 description: "Remove directories from stack.",
463 options: &[],
464 supports_help_flag: true,
465 flags: &[],
466};
467
468static DIRS_META: CommandMeta = CommandMeta {
469 name: "dirs",
470 synopsis: "dirs [-cpvl]",
471 description: "Display directory stack.",
472 options: &[
473 ("-c", "clear the directory stack"),
474 ("-p", "print one entry per line"),
475 ("-v", "print one entry per line, with index"),
476 ("-l", "use full pathnames"),
477 ],
478 supports_help_flag: true,
479 flags: &[],
480};
481
482static HASH_META: CommandMeta = CommandMeta {
483 name: "hash",
484 synopsis: "hash [-r] [name ...]",
485 description: "Remember or display program locations.",
486 options: &[("-r", "forget all remembered locations")],
487 supports_help_flag: true,
488 flags: &[],
489};
490
491static WAIT_META: CommandMeta = CommandMeta {
492 name: "wait",
493 synopsis: "wait [pid ...]",
494 description: "Wait for job completion and return exit status.",
495 options: &[],
496 supports_help_flag: true,
497 flags: &[],
498};
499
500static ALIAS_META: CommandMeta = CommandMeta {
501 name: "alias",
502 synopsis: "alias [-p] [name[=value] ...]",
503 description: "Define or display aliases.",
504 options: &[("-p", "print all defined aliases in a reusable format")],
505 supports_help_flag: true,
506 flags: &[],
507};
508
509static UNALIAS_META: CommandMeta = CommandMeta {
510 name: "unalias",
511 synopsis: "unalias [-a] name [name ...]",
512 description: "Remove alias definitions.",
513 options: &[("-a", "remove all alias definitions")],
514 supports_help_flag: true,
515 flags: &[],
516};
517
518static PRINTF_META: CommandMeta = CommandMeta {
519 name: "printf",
520 synopsis: "printf [-v var] format [arguments]",
521 description: "Format and print data.",
522 options: &[("-v VAR", "assign the output to shell variable VAR")],
523 supports_help_flag: true,
524 flags: &[],
525};
526
527static EXEC_META: CommandMeta = CommandMeta {
528 name: "exec",
529 synopsis: "exec [-a name] [command [arguments]]",
530 description: "Replace the shell with the given command.",
531 options: &[],
532 supports_help_flag: true,
533 flags: &[],
534};
535
536static SH_META: CommandMeta = CommandMeta {
537 name: "sh",
538 synopsis: "sh [-c command_string] [file]",
539 description: "Execute commands from a string, file, or standard input.",
540 options: &[("-c", "read commands from the command_string operand")],
541 supports_help_flag: true,
542 flags: &[],
543};
544
545static HELP_META: CommandMeta = CommandMeta {
546 name: "help",
547 synopsis: "help [pattern]",
548 description: "Display information about builtin commands.",
549 options: &[],
550 supports_help_flag: true,
551 flags: &[],
552};
553
554static HISTORY_META: CommandMeta = CommandMeta {
555 name: "history",
556 synopsis: "history [n]",
557 description: "Display the command history list.",
558 options: &[],
559 supports_help_flag: true,
560 flags: &[],
561};
562
563pub(crate) fn builtin_meta(name: &str) -> Option<&'static CommandMeta> {
565 match name {
566 "cd" => Some(&CD_META),
567 "exit" => Some(&EXIT_META),
568 "export" => Some(&EXPORT_META),
569 "unset" => Some(&UNSET_META),
570 "set" => Some(&SET_META),
571 "shift" => Some(&SHIFT_META),
572 "readonly" => Some(&READONLY_META),
573 "declare" | "typeset" => Some(&DECLARE_META),
574 "read" => Some(&READ_META),
575 "eval" => Some(&EVAL_META),
576 "source" | "." => Some(&SOURCE_META),
577 "break" => Some(&BREAK_META),
578 "continue" => Some(&CONTINUE_META),
579 ":" | "colon" => Some(&COLON_META),
580 "let" => Some(&LET_META),
581 "local" => Some(&LOCAL_META),
582 "return" => Some(&RETURN_META),
583 "trap" => Some(&TRAP_META),
584 "shopt" => Some(&SHOPT_META),
585 "type" => Some(&TYPE_META),
586 "command" => Some(&COMMAND_META),
587 "builtin" => Some(&BUILTIN_CMD_META),
588 "getopts" => Some(&GETOPTS_META),
589 "mapfile" | "readarray" => Some(&MAPFILE_META),
590 "pushd" => Some(&PUSHD_META),
591 "popd" => Some(&POPD_META),
592 "dirs" => Some(&DIRS_META),
593 "hash" => Some(&HASH_META),
594 "wait" => Some(&WAIT_META),
595 "alias" => Some(&ALIAS_META),
596 "unalias" => Some(&UNALIAS_META),
597 "printf" => Some(&PRINTF_META),
598 "exec" => Some(&EXEC_META),
599 "sh" | "bash" => Some(&SH_META),
600 "help" => Some(&HELP_META),
601 "history" => Some(&HISTORY_META),
602 _ => None,
603 }
604}
605
606fn builtin_exit(
607 args: &[String],
608 state: &mut InterpreterState,
609) -> Result<ExecResult, RustBashError> {
610 state.should_exit = true;
611 let code = if let Some(arg) = args.first() {
612 match arg.parse::<i32>() {
613 Ok(n) => n,
614 Err(_) => {
615 return Ok(ExecResult {
616 stderr: format!("exit: {arg}: numeric argument required\n"),
617 exit_code: 2,
618 ..ExecResult::default()
619 });
620 }
621 }
622 } else {
623 state.last_exit_code
624 };
625 Ok(ExecResult {
626 exit_code: code & 0xFF,
627 ..ExecResult::default()
628 })
629}
630
631fn builtin_break(
634 args: &[String],
635 state: &mut InterpreterState,
636) -> Result<ExecResult, RustBashError> {
637 let n = parse_loop_level("break", args)?;
638 let n = match n {
639 Ok(level) => level,
640 Err(result) => return Ok(result),
641 };
642 if state.loop_depth == 0 {
643 return Ok(ExecResult {
644 stderr: "break: only meaningful in a `for', `while', or `until' loop\n".to_string(),
645 exit_code: 1,
646 ..ExecResult::default()
647 });
648 }
649 state.control_flow = Some(ControlFlow::Break(n.min(state.loop_depth)));
650 Ok(ExecResult::default())
651}
652
653fn builtin_continue(
656 args: &[String],
657 state: &mut InterpreterState,
658) -> Result<ExecResult, RustBashError> {
659 let n = parse_loop_level("continue", args)?;
660 let n = match n {
661 Ok(level) => level,
662 Err(result) => return Ok(result),
663 };
664 if state.loop_depth == 0 {
665 return Ok(ExecResult {
666 stderr: "continue: only meaningful in a `for', `while', or `until' loop\n".to_string(),
667 exit_code: 1,
668 ..ExecResult::default()
669 });
670 }
671 state.control_flow = Some(ControlFlow::Continue(n.min(state.loop_depth)));
672 Ok(ExecResult::default())
673}
674
675fn parse_loop_level(
678 name: &str,
679 args: &[String],
680) -> Result<Result<usize, ExecResult>, RustBashError> {
681 if let Some(arg) = args.first() {
682 match arg.parse::<isize>() {
683 Ok(n) if n <= 0 => Ok(Err(ExecResult {
684 stderr: format!("{name}: {arg}: loop count out of range\n"),
685 exit_code: 1,
686 ..ExecResult::default()
687 })),
688 Ok(n) => Ok(Ok(n as usize)),
689 Err(_) => Ok(Err(ExecResult {
690 stderr: format!("{name}: {arg}: numeric argument required\n"),
691 exit_code: 128,
692 ..ExecResult::default()
693 })),
694 }
695 } else {
696 Ok(Ok(1))
697 }
698}
699
700fn builtin_cd(args: &[String], state: &mut InterpreterState) -> Result<ExecResult, RustBashError> {
703 let effective_args: &[String] = if args.first().is_some_and(|a| a == "--") {
705 &args[1..]
706 } else {
707 args
708 };
709
710 if effective_args.len() > 1 {
712 return Ok(ExecResult {
713 stderr: "cd: too many arguments\n".to_string(),
714 exit_code: 1,
715 ..ExecResult::default()
716 });
717 }
718
719 let target = if effective_args.is_empty() {
720 match state.env.get("HOME") {
722 Some(v) if !v.value.as_scalar().is_empty() => v.value.as_scalar().to_string(),
723 _ => {
724 return Ok(ExecResult {
725 stderr: "cd: HOME not set\n".to_string(),
726 exit_code: 1,
727 ..ExecResult::default()
728 });
729 }
730 }
731 } else if effective_args[0] == "-" {
732 match state.env.get("OLDPWD") {
734 Some(v) if !v.value.as_scalar().is_empty() => v.value.as_scalar().to_string(),
735 _ => {
736 return Ok(ExecResult {
737 stderr: "cd: OLDPWD not set\n".to_string(),
738 exit_code: 1,
739 ..ExecResult::default()
740 });
741 }
742 }
743 } else {
744 effective_args[0].clone()
745 };
746
747 let mut cd_printed_path = String::new();
750 let resolved = if !target.starts_with('/')
751 && !target.starts_with("./")
752 && !target.starts_with("../")
753 && target != "."
754 && target != ".."
755 {
756 if let Some(cdpath_var) = state.env.get("CDPATH") {
757 let cdpath = cdpath_var.value.as_scalar().to_string();
758 let mut found = None;
759 for dir in cdpath.split(':') {
760 let base = if dir.is_empty() { "." } else { dir };
761 let candidate = resolve_path(
762 &state.cwd,
763 &format!("{}/{}", base.trim_end_matches('/'), &target),
764 );
765 let path = Path::new(&candidate);
766 if state.fs.exists(path)
767 && state
768 .fs
769 .stat(path)
770 .is_ok_and(|m| m.node_type == NodeType::Directory)
771 {
772 cd_printed_path = candidate.clone();
773 found = Some(candidate);
774 break;
775 }
776 }
777 found.unwrap_or_else(|| resolve_path(&state.cwd, &target))
778 } else {
779 resolve_path(&state.cwd, &target)
780 }
781 } else {
782 resolve_path(&state.cwd, &target)
783 };
784
785 if target.contains('/') && !target.starts_with('/') {
787 let components: Vec<&str> = target.split('/').collect();
788 let mut check_path = state.cwd.clone();
789 for (i, comp) in components.iter().enumerate() {
790 if *comp == "." || comp.is_empty() {
791 continue;
792 }
793 if *comp == ".." {
794 continue;
796 }
797 check_path = resolve_path(&check_path, comp);
798 if i < components.len() - 1 && !state.fs.exists(Path::new(&check_path)) {
800 return Ok(ExecResult {
801 stderr: format!("cd: {target}: No such file or directory\n"),
802 exit_code: 1,
803 ..ExecResult::default()
804 });
805 }
806 }
807 }
808
809 let path = Path::new(&resolved);
811 if !state.fs.exists(path) {
812 return Ok(ExecResult {
813 stderr: format!("cd: {target}: No such file or directory\n"),
814 exit_code: 1,
815 ..ExecResult::default()
816 });
817 }
818
819 match state.fs.stat(path) {
820 Ok(meta) if meta.node_type == NodeType::Directory => {}
821 _ => {
822 return Ok(ExecResult {
823 stderr: format!("cd: {target}: Not a directory\n"),
824 exit_code: 1,
825 ..ExecResult::default()
826 });
827 }
828 }
829
830 let old_cwd = state.cwd.clone();
831 state.cwd = resolved;
832
833 let _ = set_variable(state, "OLDPWD", old_cwd);
835 if let Some(var) = state.env.get_mut("OLDPWD") {
836 var.attrs.insert(VariableAttrs::EXPORTED);
837 }
838 let new_cwd = state.cwd.clone();
839 let _ = set_variable(state, "PWD", new_cwd);
840 if let Some(var) = state.env.get_mut("PWD") {
841 var.attrs.insert(VariableAttrs::EXPORTED);
842 }
843
844 let stdout = if (!effective_args.is_empty() && effective_args[0] == "-")
846 || !cd_printed_path.is_empty()
847 {
848 format!("{}\n", state.cwd)
849 } else {
850 String::new()
851 };
852
853 Ok(ExecResult {
854 stdout,
855 ..ExecResult::default()
856 })
857}
858
859pub(crate) fn resolve_path(cwd: &str, path: &str) -> String {
861 if path.starts_with('/') {
862 normalize_path(path)
863 } else {
864 let combined = if cwd.ends_with('/') {
865 format!("{cwd}{path}")
866 } else {
867 format!("{cwd}/{path}")
868 };
869 normalize_path(&combined)
870 }
871}
872
873fn normalize_path(path: &str) -> String {
874 let mut parts: Vec<&str> = Vec::new();
875 for component in path.split('/') {
876 match component {
877 "" | "." => {}
878 ".." => {
879 parts.pop();
880 }
881 other => parts.push(other),
882 }
883 }
884 if parts.is_empty() {
885 "/".to_string()
886 } else {
887 format!("/{}", parts.join("/"))
888 }
889}
890
891fn builtin_export(
894 args: &[String],
895 state: &mut InterpreterState,
896) -> Result<ExecResult, RustBashError> {
897 if args.is_empty() || args == ["-p"] {
898 let mut lines: Vec<String> = state
900 .env
901 .iter()
902 .filter(|(_, v)| v.exported())
903 .map(|(k, v)| format_declare_line(k, v))
904 .collect();
905 lines.sort();
906 return Ok(ExecResult {
907 stdout: lines.join(""),
908 ..ExecResult::default()
909 });
910 }
911
912 let mut unexport = false;
913 let mut exit_code = 0;
914 let mut stderr = String::new();
915 for arg in args {
916 if arg == "-n" {
917 unexport = true;
918 continue;
919 }
920 if arg.starts_with('-') && !arg.contains('=') {
921 continue; }
923
924 if let Err(msg) = validate_var_arg(arg, "export") {
925 stderr.push_str(&msg);
926 exit_code = 1;
927 continue;
928 }
929
930 if let Some((name, value)) = arg.split_once("+=") {
931 let current = state
933 .env
934 .get(name)
935 .map(|v| v.value.as_scalar().to_string())
936 .unwrap_or_default();
937 let new_val = format!("{current}{value}");
938 set_variable(state, name, new_val)?;
939 if let Some(var) = state.env.get_mut(name) {
940 var.attrs.insert(VariableAttrs::EXPORTED);
941 }
942 } else if let Some((name, value)) = arg.split_once('=') {
943 set_variable(state, name, value.to_string())?;
944 if let Some(var) = state.env.get_mut(name) {
945 if unexport {
946 var.attrs.remove(VariableAttrs::EXPORTED);
947 } else {
948 var.attrs.insert(VariableAttrs::EXPORTED);
949 }
950 }
951 } else if unexport {
952 if let Some(var) = state.env.get_mut(arg.as_str()) {
954 var.attrs.remove(VariableAttrs::EXPORTED);
955 }
956 } else {
957 if let Some(var) = state.env.get_mut(arg.as_str()) {
959 var.attrs.insert(VariableAttrs::EXPORTED);
960 } else {
961 state.env.insert(
963 arg.clone(),
964 Variable {
965 value: VariableValue::Scalar(String::new()),
966 attrs: VariableAttrs::EXPORTED,
967 },
968 );
969 }
970 }
971 }
972
973 Ok(ExecResult {
974 exit_code,
975 stderr,
976 ..ExecResult::default()
977 })
978}
979
980fn builtin_unset(
983 args: &[String],
984 state: &mut InterpreterState,
985) -> Result<ExecResult, RustBashError> {
986 let mut unset_func = false;
987 let mut names_start = 0;
988 for (i, arg) in args.iter().enumerate() {
989 if arg == "-f" {
990 unset_func = true;
991 names_start = i + 1;
992 } else if arg == "-v" {
993 unset_func = false;
994 names_start = i + 1;
995 } else if arg.starts_with('-') {
996 names_start = i + 1;
997 } else {
998 break;
999 }
1000 }
1001 for arg in &args[names_start..] {
1002 if unset_func {
1003 state.functions.remove(arg.as_str());
1004 continue;
1005 }
1006 if let Some(bracket_pos) = arg.find('[')
1008 && arg.ends_with(']')
1009 {
1010 let name = &arg[..bracket_pos];
1011 let index_str = &arg[bracket_pos + 1..arg.len() - 1];
1012 if let Some(var) = state.env.get(name)
1013 && var.readonly()
1014 {
1015 return Ok(ExecResult {
1016 stderr: format!("unset: {name}: cannot unset: readonly variable\n"),
1017 exit_code: 1,
1018 ..ExecResult::default()
1019 });
1020 }
1021 let is_indexed = state
1023 .env
1024 .get(name)
1025 .is_some_and(|v| matches!(v.value, VariableValue::IndexedArray(_)));
1026 let is_assoc = state
1027 .env
1028 .get(name)
1029 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
1030 let is_scalar = state
1031 .env
1032 .get(name)
1033 .is_some_and(|v| matches!(v.value, VariableValue::Scalar(_)));
1034
1035 if is_indexed {
1036 if let Ok(idx) = crate::interpreter::arithmetic::eval_arithmetic(index_str, state) {
1037 let actual_idx = if idx < 0 {
1038 let max_key = state.env.get(name).and_then(|v| {
1040 if let VariableValue::IndexedArray(map) = &v.value {
1041 map.keys().next_back().copied()
1042 } else {
1043 None
1044 }
1045 });
1046 if let Some(mk) = max_key {
1047 let resolved = mk as i64 + 1 + idx;
1048 if resolved < 0 {
1049 return Ok(ExecResult {
1050 stderr: format!(
1051 "unset: {name}[{index_str}]: bad array subscript\n"
1052 ),
1053 exit_code: 1,
1054 ..ExecResult::default()
1055 });
1056 }
1057 Some(resolved as usize)
1058 } else {
1059 None
1060 }
1061 } else {
1062 Some(idx as usize)
1063 };
1064 if let Some(actual) = actual_idx
1065 && let Some(var) = state.env.get_mut(name)
1066 && let VariableValue::IndexedArray(map) = &mut var.value
1067 {
1068 map.remove(&actual);
1069 }
1070 }
1071 } else if is_assoc {
1072 let word = brush_parser::ast::Word {
1074 value: index_str.to_string(),
1075 loc: None,
1076 };
1077 let expanded_key =
1078 crate::interpreter::expansion::expand_word_to_string_mut(&word, state)?;
1079 if let Some(var) = state.env.get_mut(name)
1080 && let VariableValue::AssociativeArray(map) = &mut var.value
1081 {
1082 map.remove(&expanded_key);
1083 }
1084 } else if is_scalar
1085 && index_str == "0"
1086 && let Some(var) = state.env.get_mut(name)
1087 {
1088 var.value = VariableValue::Scalar(String::new());
1089 }
1090 continue;
1091 }
1092 if let Some(var) = state.env.get(arg.as_str())
1093 && var.readonly()
1094 {
1095 return Ok(ExecResult {
1096 stderr: format!("unset: {arg}: cannot unset: readonly variable\n"),
1097 exit_code: 1,
1098 ..ExecResult::default()
1099 });
1100 }
1101 let is_nameref = state
1103 .env
1104 .get(arg.as_str())
1105 .is_some_and(|v| v.attrs.contains(VariableAttrs::NAMEREF));
1106 if is_nameref {
1107 let target = crate::interpreter::resolve_nameref_or_self(arg, state);
1108 if target != *arg {
1109 if let Some(var) = state.env.get(target.as_str())
1111 && var.readonly()
1112 {
1113 return Ok(ExecResult {
1114 stderr: format!("unset: {target}: cannot unset: readonly variable\n"),
1115 exit_code: 1,
1116 ..ExecResult::default()
1117 });
1118 }
1119 state.env.remove(target.as_str());
1120 continue;
1121 }
1122 }
1123 state.env.remove(arg.as_str());
1124 }
1125 Ok(ExecResult::default())
1126}
1127
1128fn builtin_set(args: &[String], state: &mut InterpreterState) -> Result<ExecResult, RustBashError> {
1131 if args.is_empty() {
1132 let mut lines: Vec<String> = state
1134 .env
1135 .iter()
1136 .map(|(k, v)| match &v.value {
1137 VariableValue::IndexedArray(map) => {
1138 let elements: Vec<String> = map
1139 .iter()
1140 .map(|(idx, val)| format!("[{idx}]=\"{val}\""))
1141 .collect();
1142 format!("{k}=({})\n", elements.join(" "))
1143 }
1144 VariableValue::AssociativeArray(map) => {
1145 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1146 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1147 let elements: Vec<String> = entries
1148 .iter()
1149 .map(|(key, val)| {
1150 if key.contains(' ') || key.contains('"') {
1151 format!("[\"{key}\"]=\"{val}\"")
1152 } else {
1153 format!("[{key}]=\"{val}\"")
1154 }
1155 })
1156 .collect();
1157 if elements.is_empty() {
1158 format!("{k}=()\n")
1159 } else {
1160 format!("{k}=({} )\n", elements.join(" "))
1162 }
1163 }
1164 VariableValue::Scalar(s) => format!("{k}={s}\n"),
1165 })
1166 .collect();
1167 lines.sort();
1168 return Ok(ExecResult {
1169 stdout: lines.join(""),
1170 ..ExecResult::default()
1171 });
1172 }
1173
1174 let mut i = 0;
1175 while i < args.len() {
1176 let arg = &args[i];
1177 if arg == "--" {
1178 state.positional_params = args[i + 1..].to_vec();
1180 return Ok(ExecResult::default());
1181 } else if arg.starts_with('+') || arg.starts_with('-') {
1182 let enable = arg.starts_with('-');
1183 if arg == "-o" || arg == "+o" {
1184 i += 1;
1185 if i < args.len() {
1186 apply_option_name(&args[i], enable, state);
1187 } else if !enable {
1188 let mut out = String::new();
1190 for name in SET_O_OPTIONS {
1191 let val = get_set_option(name, state).unwrap_or(false);
1192 let flag = if val { "-o" } else { "+o" };
1193 out.push_str(&format!("set {flag} {name}\n"));
1194 }
1195 return Ok(ExecResult {
1196 stdout: out,
1197 ..ExecResult::default()
1198 });
1199 } else {
1200 return Ok(ExecResult {
1202 stdout: format_options(state),
1203 ..ExecResult::default()
1204 });
1205 }
1206 } else {
1207 let chars: Vec<char> = arg[1..].chars().collect();
1208 let mut saw_o = false;
1209 for c in &chars {
1210 if *c == 'o' {
1211 saw_o = true;
1212 } else {
1213 apply_option_char(*c, enable, state);
1214 }
1215 }
1216 if saw_o {
1217 i += 1;
1219 if i < args.len() {
1220 apply_option_name(&args[i], enable, state);
1221 }
1222 }
1223 }
1224 } else {
1225 state.positional_params = args[i..].to_vec();
1227 return Ok(ExecResult::default());
1228 }
1229 i += 1;
1230 }
1231
1232 Ok(ExecResult::default())
1233}
1234
1235fn apply_option_char(c: char, enable: bool, state: &mut InterpreterState) {
1236 match c {
1237 'e' => state.shell_opts.errexit = enable,
1238 'u' => state.shell_opts.nounset = enable,
1239 'x' => state.shell_opts.xtrace = enable,
1240 'v' => state.shell_opts.verbose = enable,
1241 'n' => state.shell_opts.noexec = enable,
1242 'C' => state.shell_opts.noclobber = enable,
1243 'a' => state.shell_opts.allexport = enable,
1244 'f' => state.shell_opts.noglob = enable,
1245 _ => {}
1246 }
1247}
1248
1249fn apply_option_name(name: &str, enable: bool, state: &mut InterpreterState) {
1250 match name {
1251 "errexit" => state.shell_opts.errexit = enable,
1252 "nounset" => state.shell_opts.nounset = enable,
1253 "pipefail" => state.shell_opts.pipefail = enable,
1254 "xtrace" => state.shell_opts.xtrace = enable,
1255 "verbose" => state.shell_opts.verbose = enable,
1256 "noexec" => state.shell_opts.noexec = enable,
1257 "noclobber" => state.shell_opts.noclobber = enable,
1258 "allexport" => state.shell_opts.allexport = enable,
1259 "noglob" => state.shell_opts.noglob = enable,
1260 "posix" => state.shell_opts.posix = enable,
1261 "vi" => {
1262 state.shell_opts.vi_mode = enable;
1263 if enable {
1264 state.shell_opts.emacs_mode = false;
1265 }
1266 }
1267 "emacs" => {
1268 state.shell_opts.emacs_mode = enable;
1269 if enable {
1270 state.shell_opts.vi_mode = false;
1271 }
1272 }
1273 _ => {}
1274 }
1275}
1276
1277fn format_options(state: &InterpreterState) -> String {
1278 let mut out = String::new();
1279 for name in SET_O_OPTIONS {
1280 let val = get_set_option(name, state).unwrap_or(false);
1281 let status = if val { "on" } else { "off" };
1282 out.push_str(&format!("{name:<23}\t{status}\n"));
1283 }
1284 out
1285}
1286
1287fn builtin_shift(
1290 args: &[String],
1291 state: &mut InterpreterState,
1292) -> Result<ExecResult, RustBashError> {
1293 let n = if let Some(arg) = args.first() {
1294 match arg.parse::<usize>() {
1295 Ok(n) => n,
1296 Err(_) => {
1297 return Ok(ExecResult {
1298 stderr: format!("shift: {arg}: numeric argument required\n"),
1299 exit_code: 1,
1300 ..ExecResult::default()
1301 });
1302 }
1303 }
1304 } else {
1305 1
1306 };
1307
1308 if n > state.positional_params.len() {
1309 return Ok(ExecResult {
1310 stderr: format!("shift: {n}: shift count out of range\n"),
1311 exit_code: 1,
1312 ..ExecResult::default()
1313 });
1314 }
1315
1316 state.positional_params = state.positional_params[n..].to_vec();
1317 Ok(ExecResult::default())
1318}
1319
1320fn builtin_readonly(
1323 args: &[String],
1324 state: &mut InterpreterState,
1325) -> Result<ExecResult, RustBashError> {
1326 if args.is_empty() || args == ["-p"] {
1327 let mut lines: Vec<String> = state
1328 .env
1329 .iter()
1330 .filter(|(_, v)| v.readonly())
1331 .map(|(k, v)| format_declare_line(k, v))
1332 .collect();
1333 lines.sort();
1334 return Ok(ExecResult {
1335 stdout: lines.join(""),
1336 ..ExecResult::default()
1337 });
1338 }
1339
1340 let mut exit_code = 0;
1341 let mut stderr = String::new();
1342 for arg in args {
1343 if arg.starts_with('-') {
1344 continue; }
1346 if let Err(msg) = validate_var_arg(arg, "readonly") {
1347 stderr.push_str(&msg);
1348 exit_code = 1;
1349 continue;
1350 }
1351 if let Some((name, value)) = arg.split_once("+=") {
1352 let current = state
1353 .env
1354 .get(name)
1355 .map(|v| v.value.as_scalar().to_string())
1356 .unwrap_or_default();
1357 let new_val = format!("{current}{value}");
1358 set_variable(state, name, new_val)?;
1359 if let Some(var) = state.env.get_mut(name) {
1360 var.attrs.insert(VariableAttrs::READONLY);
1361 }
1362 } else if let Some((name, value)) = arg.split_once('=') {
1363 set_variable(state, name, value.to_string())?;
1364 if let Some(var) = state.env.get_mut(name) {
1365 var.attrs.insert(VariableAttrs::READONLY);
1366 }
1367 } else {
1368 if let Some(var) = state.env.get_mut(arg.as_str()) {
1370 var.attrs.insert(VariableAttrs::READONLY);
1371 } else {
1372 state.env.insert(
1373 arg.clone(),
1374 Variable {
1375 value: VariableValue::Scalar(String::new()),
1376 attrs: VariableAttrs::READONLY,
1377 },
1378 );
1379 }
1380 }
1381 }
1382
1383 Ok(ExecResult {
1384 exit_code,
1385 stderr,
1386 ..ExecResult::default()
1387 })
1388}
1389
1390fn builtin_declare(
1393 args: &[String],
1394 state: &mut InterpreterState,
1395) -> Result<ExecResult, RustBashError> {
1396 let mut make_readonly = false;
1397 let mut make_exported = false;
1398 let mut make_indexed_array = false;
1399 let mut make_assoc_array = false;
1400 let mut make_integer = false;
1401 let mut make_lowercase = false;
1402 let mut make_uppercase = false;
1403 let mut make_nameref = false;
1404 let mut print_mode = false;
1405 let mut func_mode = false; let mut func_names_mode = false; let mut global_mode = false; let mut remove_exported = false; let mut remove_readonly = false; let mut var_args: Vec<&String> = Vec::new();
1411
1412 for arg in args {
1413 if let Some(flags) = arg.strip_prefix('-') {
1414 if flags.is_empty() {
1415 var_args.push(arg);
1416 continue;
1417 }
1418 for c in flags.chars() {
1419 match c {
1420 'r' => make_readonly = true,
1421 'x' => make_exported = true,
1422 'a' => make_indexed_array = true,
1423 'A' => make_assoc_array = true,
1424 'i' => make_integer = true,
1425 'l' => make_lowercase = true,
1426 'u' => make_uppercase = true,
1427 'n' => make_nameref = true,
1428 'p' => print_mode = true,
1429 'f' => func_mode = true,
1430 'F' => func_names_mode = true,
1431 'g' => global_mode = true,
1432 _ => {}
1433 }
1434 }
1435 } else if let Some(flags) = arg.strip_prefix('+') {
1436 for c in flags.chars() {
1437 match c {
1438 'x' => remove_exported = true,
1439 'r' => remove_readonly = true,
1440 _ => {}
1441 }
1442 }
1443 } else {
1444 var_args.push(arg);
1445 }
1446 }
1447
1448 if func_mode || func_names_mode {
1450 return declare_functions(state, &var_args, func_names_mode);
1451 }
1452
1453 if print_mode {
1455 return declare_print(
1456 state,
1457 &var_args,
1458 make_readonly,
1459 make_exported,
1460 make_nameref,
1461 make_indexed_array,
1462 make_assoc_array,
1463 );
1464 }
1465
1466 if remove_exported || remove_readonly {
1470 for arg in &var_args {
1471 let (name, opt_value) = if let Some((n, v)) = arg.split_once('=') {
1472 (n, Some(v))
1473 } else {
1474 (arg.as_str(), None)
1475 };
1476 if remove_exported && let Some(var) = state.env.get_mut(name) {
1477 var.attrs.remove(VariableAttrs::EXPORTED);
1478 }
1479 if let Some(value) = opt_value {
1481 let is_ro = state.env.get(name).is_some_and(|v| v.readonly());
1482 if !is_ro {
1483 set_variable(state, name, value.to_string())?;
1484 }
1485 }
1486 }
1487 return Ok(ExecResult::default());
1488 }
1489
1490 let _ = global_mode; let has_any_flag = make_readonly
1493 || make_exported
1494 || make_indexed_array
1495 || make_assoc_array
1496 || make_integer
1497 || make_lowercase
1498 || make_uppercase
1499 || make_nameref;
1500
1501 if var_args.is_empty() && !has_any_flag {
1502 return declare_list_all(state);
1504 }
1505
1506 let mut flag_attrs = VariableAttrs::empty();
1508 if make_readonly {
1509 flag_attrs.insert(VariableAttrs::READONLY);
1510 }
1511 if make_exported {
1512 flag_attrs.insert(VariableAttrs::EXPORTED);
1513 }
1514 if make_integer {
1515 flag_attrs.insert(VariableAttrs::INTEGER);
1516 }
1517 if make_lowercase {
1518 flag_attrs.insert(VariableAttrs::LOWERCASE);
1519 }
1520 if make_uppercase {
1521 flag_attrs.insert(VariableAttrs::UPPERCASE);
1522 }
1523 if make_nameref {
1524 flag_attrs.insert(VariableAttrs::NAMEREF);
1525 }
1526
1527 let mut exit_code = 0;
1528 let mut result_stderr = String::new();
1529 for arg in var_args {
1530 if let Err(msg) = validate_var_arg(arg, "declare") {
1531 result_stderr.push_str(&msg);
1532 exit_code = 1;
1533 continue;
1534 }
1535
1536 if let Some((name, value)) = arg.split_once("+=") {
1538 declare_append_value(
1539 state,
1540 name,
1541 value,
1542 flag_attrs,
1543 make_assoc_array,
1544 make_indexed_array,
1545 )?;
1546 } else if let Some((name, value)) = arg.split_once('=') {
1547 declare_with_value(
1548 state,
1549 name,
1550 value,
1551 flag_attrs,
1552 make_assoc_array,
1553 make_indexed_array,
1554 make_nameref,
1555 )?;
1556 } else {
1557 declare_without_value(state, arg, flag_attrs, make_assoc_array, make_indexed_array)?;
1558 }
1559 }
1560
1561 Ok(ExecResult {
1562 exit_code,
1563 stderr: result_stderr,
1564 ..ExecResult::default()
1565 })
1566}
1567
1568fn declare_functions(
1570 state: &InterpreterState,
1571 var_args: &[&String],
1572 names_only: bool,
1573) -> Result<ExecResult, RustBashError> {
1574 if var_args.is_empty() {
1575 let mut lines: Vec<String> = Vec::new();
1577 for name in state.functions.keys() {
1578 if names_only {
1579 lines.push(format!("declare -f {name}\n"));
1580 } else {
1581 lines.push(format!("{name} () {{ :; }}\n")); }
1583 }
1584 lines.sort();
1585 return Ok(ExecResult {
1586 stdout: lines.join(""),
1587 ..ExecResult::default()
1588 });
1589 }
1590 let mut exit_code = 0;
1592 let mut stdout = String::new();
1593 for name in var_args {
1594 if state.functions.contains_key(name.as_str()) {
1595 if names_only {
1596 stdout.push_str(&format!("declare -f {name}\n"));
1597 }
1598 } else {
1599 exit_code = 1;
1600 }
1601 }
1602 Ok(ExecResult {
1603 stdout,
1604 exit_code,
1605 ..ExecResult::default()
1606 })
1607}
1608
1609fn declare_print(
1611 state: &InterpreterState,
1612 var_args: &[&String],
1613 filter_readonly: bool,
1614 filter_exported: bool,
1615 filter_nameref: bool,
1616 filter_indexed: bool,
1617 filter_assoc: bool,
1618) -> Result<ExecResult, RustBashError> {
1619 let has_filter =
1620 filter_readonly || filter_exported || filter_nameref || filter_indexed || filter_assoc;
1621
1622 if var_args.is_empty() {
1623 if has_filter {
1624 let mut entries: Vec<(&String, &Variable)> = state
1626 .env
1627 .iter()
1628 .filter(|(_, v)| {
1629 if filter_readonly && v.attrs.contains(VariableAttrs::READONLY) {
1630 return true;
1631 }
1632 if filter_exported && v.attrs.contains(VariableAttrs::EXPORTED) {
1633 return true;
1634 }
1635 if filter_nameref && v.attrs.contains(VariableAttrs::NAMEREF) {
1636 return true;
1637 }
1638 if filter_indexed && matches!(v.value, VariableValue::IndexedArray(_)) {
1639 return true;
1640 }
1641 if filter_assoc && matches!(v.value, VariableValue::AssociativeArray(_)) {
1642 return true;
1643 }
1644 false
1645 })
1646 .collect();
1647 entries.sort_by_key(|(name, _)| name.as_str());
1648 let stdout: String = entries
1649 .iter()
1650 .map(|(k, v)| format_declare_line(k, v))
1651 .collect();
1652 return Ok(ExecResult {
1653 stdout,
1654 ..ExecResult::default()
1655 });
1656 }
1657 let mut entries: Vec<(&String, &Variable)> = state.env.iter().collect();
1659 entries.sort_by_key(|(name, _)| name.as_str());
1660 let stdout: String = entries
1661 .iter()
1662 .map(|(k, v)| format_declare_line(k, v))
1663 .collect();
1664 return Ok(ExecResult {
1665 stdout,
1666 ..ExecResult::default()
1667 });
1668 }
1669 let mut stdout = String::new();
1670 let mut stderr = String::new();
1671 let mut exit_code = 0;
1672 for name in var_args {
1673 if let Some(var) = state.env.get(name.as_str()) {
1674 stdout.push_str(&format_declare_line(name, var));
1675 } else {
1676 stderr.push_str(&format!("declare: {name}: not found\n"));
1677 exit_code = 1;
1678 }
1679 }
1680 Ok(ExecResult {
1681 stdout,
1682 stderr,
1683 exit_code,
1684 stdout_bytes: None,
1685 })
1686}
1687
1688fn declare_list_all(state: &InterpreterState) -> Result<ExecResult, RustBashError> {
1690 let mut entries: Vec<(&String, &Variable)> = state.env.iter().collect();
1692 entries.sort_by_key(|(name, _)| name.as_str());
1693 let stdout: String = entries
1694 .iter()
1695 .map(|(name, var)| format_simple_line(name, var))
1696 .collect();
1697 Ok(ExecResult {
1698 stdout,
1699 ..ExecResult::default()
1700 })
1701}
1702
1703fn format_simple_line(name: &str, var: &Variable) -> String {
1705 match &var.value {
1706 VariableValue::Scalar(s) => format!("{name}={s}\n"),
1707 VariableValue::IndexedArray(map) => {
1708 let elems: Vec<String> = map.iter().map(|(k, v)| format!("[{k}]=\"{v}\"")).collect();
1709 format!("{name}=({})\n", elems.join(" "))
1710 }
1711 VariableValue::AssociativeArray(map) => {
1712 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1713 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1714 let elems: Vec<String> = entries
1715 .iter()
1716 .map(|(k, v)| format!("[{k}]=\"{v}\""))
1717 .collect();
1718 if elems.is_empty() {
1719 format!("{name}=()\n")
1720 } else {
1721 format!("{name}=({} )\n", elems.join(" "))
1722 }
1723 }
1724 }
1725}
1726
1727fn format_declare_line(name: &str, var: &Variable) -> String {
1729 let mut flags = String::new();
1730 if matches!(var.value, VariableValue::IndexedArray(_)) {
1732 flags.push('a');
1733 }
1734 if matches!(var.value, VariableValue::AssociativeArray(_)) {
1735 flags.push('A');
1736 }
1737 if var.attrs.contains(VariableAttrs::INTEGER) {
1738 flags.push('i');
1739 }
1740 if var.attrs.contains(VariableAttrs::LOWERCASE) {
1741 flags.push('l');
1742 }
1743 if var.attrs.contains(VariableAttrs::NAMEREF) {
1744 flags.push('n');
1745 }
1746 if var.attrs.contains(VariableAttrs::READONLY) {
1747 flags.push('r');
1748 }
1749 if var.attrs.contains(VariableAttrs::UPPERCASE) {
1750 flags.push('u');
1751 }
1752 if var.attrs.contains(VariableAttrs::EXPORTED) {
1753 flags.push('x');
1754 }
1755
1756 let flag_str = if flags.is_empty() {
1757 "-- ".to_string()
1758 } else {
1759 format!("-{flags} ")
1760 };
1761
1762 match &var.value {
1763 VariableValue::Scalar(s) => format!("declare {flag_str}{name}=\"{s}\"\n"),
1764 VariableValue::IndexedArray(map) => {
1765 let elems: Vec<String> = map.iter().map(|(k, v)| format!("[{k}]=\"{v}\"")).collect();
1766 format!("declare {flag_str}{name}=({})\n", elems.join(" "))
1767 }
1768 VariableValue::AssociativeArray(map) => {
1769 let mut entries: Vec<(&String, &String)> = map.iter().collect();
1770 entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1771 let elems: Vec<String> = entries
1772 .iter()
1773 .map(|(k, v)| format!("[{k}]=\"{v}\""))
1774 .collect();
1775 if elems.is_empty() {
1776 format!("declare {flag_str}{name}=()\n")
1777 } else {
1778 format!("declare {flag_str}{name}=({} )\n", elems.join(" "))
1780 }
1781 }
1782 }
1783}
1784
1785fn declare_append_value(
1787 state: &mut InterpreterState,
1788 name: &str,
1789 value: &str,
1790 flag_attrs: VariableAttrs,
1791 make_assoc_array: bool,
1792 _make_indexed_array: bool,
1793) -> Result<(), RustBashError> {
1794 if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1796 let is_assoc = make_assoc_array
1798 || state
1799 .env
1800 .get(name)
1801 .is_some_and(|v| matches!(v.value, VariableValue::AssociativeArray(_)));
1802
1803 if is_assoc {
1804 if !state.env.contains_key(name) {
1806 state.env.insert(
1807 name.to_string(),
1808 Variable {
1809 value: VariableValue::AssociativeArray(std::collections::BTreeMap::new()),
1810 attrs: flag_attrs,
1811 },
1812 );
1813 }
1814 parse_and_set_assoc_array_append(state, name, inner)?;
1815 } else {
1816 let start_idx = match state.env.get(name) {
1818 Some(var) => match &var.value {
1819 VariableValue::IndexedArray(map) => {
1820 map.keys().next_back().map(|k| k + 1).unwrap_or(0)
1821 }
1822 VariableValue::Scalar(s) if s.is_empty() => 0,
1823 VariableValue::Scalar(_) => 1,
1824 VariableValue::AssociativeArray(_) => 0,
1825 },
1826 None => 0,
1827 };
1828
1829 if !state.env.contains_key(name) {
1831 state.env.insert(
1832 name.to_string(),
1833 Variable {
1834 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
1835 attrs: flag_attrs,
1836 },
1837 );
1838 }
1839
1840 if let Some(var) = state.env.get_mut(name)
1842 && let VariableValue::Scalar(s) = &var.value
1843 {
1844 let mut map = std::collections::BTreeMap::new();
1845 if !s.is_empty() {
1846 map.insert(0, s.clone());
1847 }
1848 var.value = VariableValue::IndexedArray(map);
1849 }
1850
1851 let words = shell_split_array_body(inner);
1852 let mut idx = start_idx;
1853 for word in &words {
1854 let val = unquote_simple(word);
1855 crate::interpreter::set_array_element(state, name, idx, val)?;
1856 idx += 1;
1857 }
1858
1859 if let Some(var) = state.env.get_mut(name) {
1860 var.attrs.insert(flag_attrs);
1861 }
1862 }
1863 } else {
1864 let current = state
1866 .env
1867 .get(name)
1868 .map(|v| v.value.as_scalar().to_string())
1869 .unwrap_or_default();
1870 let new_val = format!("{current}{value}");
1871 set_variable(state, name, new_val)?;
1872 if let Some(var) = state.env.get_mut(name) {
1873 var.attrs.insert(flag_attrs);
1874 }
1875 }
1876 Ok(())
1877}
1878
1879fn declare_with_value(
1881 state: &mut InterpreterState,
1882 name: &str,
1883 value: &str,
1884 flag_attrs: VariableAttrs,
1885 make_assoc_array: bool,
1886 make_indexed_array: bool,
1887 make_nameref: bool,
1888) -> Result<(), RustBashError> {
1889 if make_nameref {
1890 let var = state
1892 .env
1893 .entry(name.to_string())
1894 .or_insert_with(|| Variable {
1895 value: VariableValue::Scalar(String::new()),
1896 attrs: VariableAttrs::empty(),
1897 });
1898 var.value = VariableValue::Scalar(value.to_string());
1899 var.attrs.insert(flag_attrs);
1900 return Ok(());
1901 }
1902
1903 if make_assoc_array {
1904 let var = state
1905 .env
1906 .entry(name.to_string())
1907 .or_insert_with(|| Variable {
1908 value: VariableValue::AssociativeArray(std::collections::BTreeMap::new()),
1909 attrs: VariableAttrs::empty(),
1910 });
1911 var.attrs.insert(flag_attrs);
1912 if !matches!(var.value, VariableValue::AssociativeArray(_)) {
1913 var.value = VariableValue::AssociativeArray(std::collections::BTreeMap::new());
1914 }
1915 if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1917 parse_and_set_assoc_array(state, name, inner)?;
1918 }
1919 } else if make_indexed_array {
1920 let var = state
1921 .env
1922 .entry(name.to_string())
1923 .or_insert_with(|| Variable {
1924 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
1925 attrs: VariableAttrs::empty(),
1926 });
1927 var.attrs.insert(flag_attrs);
1928 if !matches!(var.value, VariableValue::IndexedArray(_)) {
1929 var.value = VariableValue::IndexedArray(std::collections::BTreeMap::new());
1930 }
1931 if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1933 parse_and_set_indexed_array(state, name, inner)?;
1934 } else if !value.is_empty() {
1935 crate::interpreter::set_array_element(state, name, 0, value.to_string())?;
1936 }
1937 } else if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1938 let var = state
1940 .env
1941 .entry(name.to_string())
1942 .or_insert_with(|| Variable {
1943 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
1944 attrs: VariableAttrs::empty(),
1945 });
1946 var.attrs.insert(flag_attrs);
1947 if !matches!(var.value, VariableValue::IndexedArray(_)) {
1948 var.value = VariableValue::IndexedArray(std::collections::BTreeMap::new());
1949 }
1950 parse_and_set_indexed_array(state, name, inner)?;
1951 } else {
1952 let non_readonly_attrs = flag_attrs - VariableAttrs::READONLY;
1953 let var = state
1954 .env
1955 .entry(name.to_string())
1956 .or_insert_with(|| Variable {
1957 value: VariableValue::Scalar(String::new()),
1958 attrs: VariableAttrs::empty(),
1959 });
1960 var.attrs.insert(non_readonly_attrs);
1961 set_variable(state, name, value.to_string())?;
1963 if flag_attrs.contains(VariableAttrs::READONLY)
1965 && let Some(var) = state.env.get_mut(name)
1966 {
1967 var.attrs.insert(VariableAttrs::READONLY);
1968 }
1969 }
1970 Ok(())
1971}
1972
1973fn declare_without_value(
1975 state: &mut InterpreterState,
1976 name: &str,
1977 flag_attrs: VariableAttrs,
1978 make_assoc_array: bool,
1979 make_indexed_array: bool,
1980) -> Result<(), RustBashError> {
1981 if let Some(var) = state.env.get_mut(name) {
1982 var.attrs.insert(flag_attrs);
1983 if make_assoc_array && !matches!(var.value, VariableValue::AssociativeArray(_)) {
1984 var.value = VariableValue::AssociativeArray(std::collections::BTreeMap::new());
1985 }
1986 if make_indexed_array && !matches!(var.value, VariableValue::IndexedArray(_)) {
1987 var.value = VariableValue::IndexedArray(std::collections::BTreeMap::new());
1988 }
1989 } else {
1990 let value = if make_assoc_array {
1991 VariableValue::AssociativeArray(std::collections::BTreeMap::new())
1992 } else if make_indexed_array {
1993 VariableValue::IndexedArray(std::collections::BTreeMap::new())
1994 } else {
1995 VariableValue::Scalar(String::new())
1996 };
1997 state.env.insert(
1998 name.to_string(),
1999 Variable {
2000 value,
2001 attrs: flag_attrs,
2002 },
2003 );
2004 }
2005 Ok(())
2006}
2007
2008fn parse_and_set_indexed_array(
2011 state: &mut InterpreterState,
2012 name: &str,
2013 body: &str,
2014) -> Result<(), RustBashError> {
2015 let words = shell_split_array_body(body);
2017 if let Some(var) = state.env.get_mut(name) {
2019 var.value = VariableValue::IndexedArray(std::collections::BTreeMap::new());
2020 }
2021 let mut idx: usize = 0;
2022 for word in &words {
2023 if let Some(rest) = word.strip_prefix('[') {
2024 if let Some(eq_pos) = rest.find("]=") {
2026 let index_str = &rest[..eq_pos];
2027 let value_part = &rest[eq_pos + 2..];
2028 let value = unquote_simple(value_part);
2029 if let Ok(i) = index_str.parse::<usize>() {
2030 crate::interpreter::set_array_element(state, name, i, value)?;
2031 idx = i + 1;
2032 }
2033 }
2034 } else {
2035 let value = unquote_simple(word);
2036 crate::interpreter::set_array_element(state, name, idx, value)?;
2037 idx += 1;
2038 }
2039 }
2040 Ok(())
2041}
2042
2043fn parse_and_set_assoc_array(
2045 state: &mut InterpreterState,
2046 name: &str,
2047 body: &str,
2048) -> Result<(), RustBashError> {
2049 let words = shell_split_array_body(body);
2050 if let Some(var) = state.env.get_mut(name) {
2052 var.value = VariableValue::AssociativeArray(std::collections::BTreeMap::new());
2053 }
2054 for word in &words {
2055 if let Some(rest) = word.strip_prefix('[') {
2056 if let Some(eq_pos) = rest.find("]=") {
2058 let key = unquote_simple(&rest[..eq_pos]);
2059 let value = unquote_simple(&rest[eq_pos + 2..]);
2060 crate::interpreter::set_assoc_element(state, name, key, value)?;
2061 } else if let Some(key_str) = rest.strip_suffix(']') {
2062 let key = unquote_simple(key_str);
2064 crate::interpreter::set_assoc_element(state, name, key, String::new())?;
2065 }
2066 }
2067 }
2069 Ok(())
2070}
2071
2072fn parse_and_set_assoc_array_append(
2074 state: &mut InterpreterState,
2075 name: &str,
2076 body: &str,
2077) -> Result<(), RustBashError> {
2078 let words = shell_split_array_body(body);
2079 for word in &words {
2080 if let Some(rest) = word.strip_prefix('[') {
2081 if let Some(eq_pos) = rest.find("]=") {
2082 let key = unquote_simple(&rest[..eq_pos]);
2083 let value = unquote_simple(&rest[eq_pos + 2..]);
2084 crate::interpreter::set_assoc_element(state, name, key, value)?;
2085 } else if let Some(key_str) = rest.strip_suffix(']') {
2086 let key = unquote_simple(key_str);
2087 crate::interpreter::set_assoc_element(state, name, key, String::new())?;
2088 }
2089 }
2090 }
2091 Ok(())
2092}
2093
2094fn shell_split_array_body(s: &str) -> Vec<String> {
2096 let mut words = Vec::new();
2097 let mut current = String::new();
2098 let mut chars = s.chars().peekable();
2099 while let Some(&c) = chars.peek() {
2100 match c {
2101 ' ' | '\t' | '\n' => {
2102 if !current.is_empty() {
2103 words.push(std::mem::take(&mut current));
2104 }
2105 chars.next();
2106 }
2107 '"' => {
2108 chars.next();
2109 current.push('"');
2110 while let Some(&ch) = chars.peek() {
2111 if ch == '"' {
2112 current.push('"');
2113 chars.next();
2114 break;
2115 }
2116 if ch == '\\' {
2117 chars.next();
2118 current.push('\\');
2119 if let Some(&esc) = chars.peek() {
2120 current.push(esc);
2121 chars.next();
2122 }
2123 } else {
2124 current.push(ch);
2125 chars.next();
2126 }
2127 }
2128 }
2129 '\'' => {
2130 chars.next();
2131 current.push('\'');
2132 while let Some(&ch) = chars.peek() {
2133 if ch == '\'' {
2134 current.push('\'');
2135 chars.next();
2136 break;
2137 }
2138 current.push(ch);
2139 chars.next();
2140 }
2141 }
2142 _ => {
2143 current.push(c);
2144 chars.next();
2145 }
2146 }
2147 }
2148 if !current.is_empty() {
2149 words.push(current);
2150 }
2151 words
2152}
2153
2154fn unquote_simple(s: &str) -> String {
2156 if s.len() >= 2
2157 && ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')))
2158 {
2159 return s[1..s.len() - 1].to_string();
2160 }
2161 s.to_string()
2162}
2163
2164fn builtin_read(
2167 args: &[String],
2168 state: &mut InterpreterState,
2169 stdin: &str,
2170) -> Result<ExecResult, RustBashError> {
2171 let mut raw_mode = false;
2172 let mut array_name: Option<String> = None;
2173 let mut delimiter: Option<char> = None; let mut read_until_eof = false; let mut n_count: Option<usize> = None; let mut big_n_count: Option<usize> = None; let mut var_names: Vec<&str> = Vec::new();
2178 let mut i = 0;
2179
2180 while i < args.len() {
2182 let arg = &args[i];
2183 if arg == "--" {
2184 for a in &args[i + 1..] {
2186 var_names.push(a);
2187 }
2188 break;
2189 } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") {
2190 let flag_chars: Vec<char> = arg[1..].chars().collect();
2191 let mut j = 0;
2192 while j < flag_chars.len() {
2193 match flag_chars[j] {
2194 'r' => raw_mode = true,
2195 's' => { }
2196 'a' => {
2197 let rest: String = flag_chars[j + 1..].iter().collect();
2199 if rest.is_empty() {
2200 i += 1;
2201 if i < args.len() {
2202 array_name = Some(args[i].clone());
2203 }
2204 } else {
2205 array_name = Some(rest);
2206 }
2207 j = flag_chars.len(); continue;
2209 }
2210 'd' => {
2211 let rest: String = flag_chars[j + 1..].iter().collect();
2213 let delim_str = if rest.is_empty() {
2214 i += 1;
2215 if i < args.len() { args[i].as_str() } else { "" }
2216 } else {
2217 rest.as_str()
2218 };
2219 if delim_str.is_empty() {
2220 read_until_eof = true;
2221 } else {
2222 delimiter = Some(delim_str.chars().next().unwrap());
2223 }
2224 j = flag_chars.len();
2225 continue;
2226 }
2227 'n' => {
2228 let rest: String = flag_chars[j + 1..].iter().collect();
2230 let count_str = if rest.is_empty() {
2231 i += 1;
2232 if i < args.len() {
2233 args[i].as_str()
2234 } else {
2235 "0"
2236 }
2237 } else {
2238 rest.as_str()
2239 };
2240 n_count = count_str.parse().ok();
2241 j = flag_chars.len();
2242 continue;
2243 }
2244 'N' => {
2245 let rest: String = flag_chars[j + 1..].iter().collect();
2247 let count_str = if rest.is_empty() {
2248 i += 1;
2249 if i < args.len() {
2250 args[i].as_str()
2251 } else {
2252 "0"
2253 }
2254 } else {
2255 rest.as_str()
2256 };
2257 big_n_count = count_str.parse().ok();
2258 j = flag_chars.len();
2259 continue;
2260 }
2261 'p' => {
2262 let rest: String = flag_chars[j + 1..].iter().collect();
2264 if rest.is_empty() {
2265 i += 1; }
2267 j = flag_chars.len();
2268 continue;
2269 }
2270 't' => {
2271 let rest: String = flag_chars[j + 1..].iter().collect();
2273 if rest.is_empty() {
2274 i += 1;
2275 }
2276 j = flag_chars.len();
2277 continue;
2278 }
2279 _ => { }
2280 }
2281 j += 1;
2282 }
2283 } else {
2284 var_names.push(arg);
2285 }
2286 i += 1;
2287 }
2288
2289 if array_name.is_none() && var_names.is_empty() {
2291 var_names.push("REPLY");
2292 }
2293
2294 let effective_stdin = if state.stdin_offset < stdin.len() {
2296 &stdin[state.stdin_offset..]
2297 } else {
2298 ""
2299 };
2300
2301 if effective_stdin.is_empty() {
2305 return Ok(ExecResult {
2306 exit_code: 1,
2307 ..ExecResult::default()
2308 });
2309 }
2310
2311 let mut hit_eof = false;
2313
2314 let line = if let Some(count) = big_n_count {
2315 let chars: String = effective_stdin.chars().take(count).collect();
2317 state.stdin_offset += chars.len();
2318 if chars.chars().count() < count {
2319 hit_eof = true;
2320 }
2321 chars
2322 } else if let Some(count) = n_count {
2323 let mut result = String::new();
2325 let mut found_newline = false;
2326 for ch in effective_stdin.chars().take(count) {
2327 if ch == '\n' {
2328 state.stdin_offset += 1; found_newline = true;
2330 break;
2331 }
2332 result.push(ch);
2333 }
2334 state.stdin_offset += result.len();
2335 if !found_newline && state.stdin_offset >= stdin.len() {
2336 hit_eof = true;
2337 }
2338 result
2339 } else if read_until_eof {
2340 hit_eof = true;
2342 let data = effective_stdin.to_string();
2343 state.stdin_offset += data.len();
2344 data
2345 } else if let Some(delim) = delimiter {
2346 let mut result = String::new();
2348 let mut found_delim = false;
2349 for ch in effective_stdin.chars() {
2350 if ch == delim {
2351 state.stdin_offset += ch.len_utf8(); found_delim = true;
2353 break;
2354 }
2355 result.push(ch);
2356 }
2357 state.stdin_offset += result.len();
2358 if !found_delim {
2359 hit_eof = true;
2360 }
2361 result
2362 } else {
2363 match effective_stdin.lines().next() {
2365 Some(l) => {
2366 state.stdin_offset += l.len();
2367 if state.stdin_offset < stdin.len()
2368 && stdin.as_bytes().get(state.stdin_offset) == Some(&b'\n')
2369 {
2370 state.stdin_offset += 1;
2371 } else {
2372 hit_eof = true;
2373 }
2374 l.to_string()
2375 }
2376 None => {
2377 return Ok(ExecResult {
2378 exit_code: 1,
2379 ..ExecResult::default()
2380 });
2381 }
2382 }
2383 };
2384
2385 if line.len() > state.limits.max_string_length {
2387 return Err(RustBashError::LimitExceeded {
2388 limit_name: "max_string_length",
2389 limit_value: state.limits.max_string_length,
2390 actual_value: line.len(),
2391 });
2392 }
2393
2394 let line = if raw_mode || big_n_count.is_some() {
2397 line
2398 } else {
2399 let mut result = String::new();
2400 let mut chars = line.chars().peekable();
2401 while let Some(c) = chars.next() {
2402 if c == '\\' {
2403 if let Some(&next) = chars.peek() {
2404 if next == '\n' {
2405 chars.next(); } else {
2407 result.push(next);
2408 chars.next();
2409 }
2410 }
2411 } else {
2412 result.push(c);
2413 }
2414 }
2415 result
2416 };
2417
2418 let ifs = state
2420 .env
2421 .get("IFS")
2422 .map(|v| v.value.as_scalar().to_string())
2423 .unwrap_or_else(|| " \t\n".to_string());
2424
2425 if let Some(ref arr_name) = array_name {
2426 let fields: Vec<&str> = if line.is_empty() {
2428 vec![]
2430 } else if ifs.is_empty() {
2431 vec![line.as_str()]
2432 } else {
2433 split_by_ifs(&line, &ifs)
2434 };
2435
2436 state.env.insert(
2438 arr_name.to_string(),
2439 Variable {
2440 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
2441 attrs: VariableAttrs::empty(),
2442 },
2443 );
2444
2445 for (idx, field) in fields.iter().enumerate() {
2446 set_array_element(state, arr_name, idx, field.to_string())?;
2447 }
2448 } else if big_n_count.is_some() {
2449 let var_name = var_names.first().copied().unwrap_or("REPLY");
2451 set_variable(state, var_name, line)?;
2452 for extra_var in var_names.iter().skip(1) {
2454 set_variable(state, extra_var, String::new())?;
2455 }
2456 } else {
2457 assign_fields_to_vars(state, &line, &ifs, &var_names)?;
2459 }
2460
2461 Ok(ExecResult {
2462 exit_code: i32::from(hit_eof),
2463 ..ExecResult::default()
2464 })
2465}
2466
2467fn assign_fields_to_vars(
2470 state: &mut InterpreterState,
2471 line: &str,
2472 ifs: &str,
2473 var_names: &[&str],
2474) -> Result<(), RustBashError> {
2475 if ifs.is_empty() || var_names.len() <= 1 {
2476 let value = if var_names.first().copied() == Some("REPLY") && var_names.len() == 1 {
2480 line.to_string()
2482 } else if ifs.is_empty() {
2483 line.to_string()
2484 } else {
2485 let ifs_ws = |c: char| (c == ' ' || c == '\t' || c == '\n') && ifs.contains(c);
2486 line.trim_matches(ifs_ws).to_string()
2487 };
2488 let var_name = var_names.first().copied().unwrap_or("REPLY");
2489 return set_variable(state, var_name, value);
2490 }
2491
2492 let ifs_is_ws = |c: char| (c == ' ' || c == '\t' || c == '\n') && ifs.contains(c);
2494 let ifs_is_delim = |c: char| ifs.contains(c);
2495 let has_ws = ifs.contains(' ') || ifs.contains('\t') || ifs.contains('\n');
2496
2497 let mut pos = 0;
2498 if has_ws {
2500 while pos < line.len() {
2501 let ch = line[pos..].chars().next().unwrap();
2502 if ifs_is_ws(ch) {
2503 pos += ch.len_utf8();
2504 } else {
2505 break;
2506 }
2507 }
2508 }
2509
2510 for (i, var_name) in var_names.iter().enumerate() {
2511 if i == var_names.len() - 1 {
2512 let rest = &line[pos..];
2514 let trimmed = if has_ws {
2515 rest.trim_end_matches(ifs_is_ws)
2516 } else {
2517 rest
2518 };
2519 set_variable(state, var_name, trimmed.to_string())?;
2520 } else {
2521 let field_start = pos;
2523 while pos < line.len() {
2524 let ch = line[pos..].chars().next().unwrap();
2525 if ifs_is_delim(ch) {
2526 break;
2527 }
2528 pos += ch.len_utf8();
2529 }
2530 let field = &line[field_start..pos];
2531 set_variable(state, var_name, field.to_string())?;
2532
2533 if has_ws {
2535 while pos < line.len() {
2536 let ch = line[pos..].chars().next().unwrap();
2537 if ifs_is_ws(ch) {
2538 pos += ch.len_utf8();
2539 } else {
2540 break;
2541 }
2542 }
2543 }
2544 if pos < line.len() {
2546 let ch = line[pos..].chars().next().unwrap();
2547 if ifs_is_delim(ch) && !ifs_is_ws(ch) {
2548 pos += ch.len_utf8();
2549 if has_ws {
2551 while pos < line.len() {
2552 let ch2 = line[pos..].chars().next().unwrap();
2553 if ifs_is_ws(ch2) {
2554 pos += ch2.len_utf8();
2555 } else {
2556 break;
2557 }
2558 }
2559 }
2560 }
2561 }
2562 }
2563 }
2564 Ok(())
2565}
2566
2567fn split_by_ifs<'a>(s: &'a str, ifs: &str) -> Vec<&'a str> {
2568 let has_whitespace = ifs.contains(' ') || ifs.contains('\t') || ifs.contains('\n');
2569
2570 if has_whitespace {
2571 s.split(|c: char| ifs.contains(c))
2574 .filter(|s| !s.is_empty())
2575 .collect()
2576 } else {
2577 s.split(|c: char| ifs.contains(c)).collect()
2579 }
2580}
2581
2582fn builtin_eval(
2585 args: &[String],
2586 state: &mut InterpreterState,
2587) -> Result<ExecResult, RustBashError> {
2588 if args.is_empty() {
2589 return Ok(ExecResult::default());
2590 }
2591
2592 let args = if args.first().map(|a| a.as_str()) == Some("--") {
2594 &args[1..]
2595 } else {
2596 args
2597 };
2598
2599 if args.is_empty() {
2600 return Ok(ExecResult::default());
2601 }
2602
2603 let input = args.join(" ");
2604 if input.is_empty() {
2605 return Ok(ExecResult::default());
2606 }
2607
2608 state.counters.call_depth += 1;
2609 if state.counters.call_depth > state.limits.max_call_depth {
2610 let actual = state.counters.call_depth;
2611 state.counters.call_depth -= 1;
2612 return Err(RustBashError::LimitExceeded {
2613 limit_name: "max_call_depth",
2614 limit_value: state.limits.max_call_depth,
2615 actual_value: actual,
2616 });
2617 }
2618
2619 let program = match parse(&input) {
2620 Ok(p) => p,
2621 Err(e) => {
2622 state.counters.call_depth -= 1;
2623 let msg = format!("{e}");
2624 return Ok(ExecResult {
2625 stderr: if msg.is_empty() {
2626 String::new()
2627 } else {
2628 format!("eval: {msg}\n")
2629 },
2630 exit_code: 1,
2631 ..ExecResult::default()
2632 });
2633 }
2634 };
2635 let result = execute_program(&program, state);
2636 state.counters.call_depth -= 1;
2637 result
2638}
2639
2640const SIGNAL_NAMES: &[&str] = &[
2642 "EXIT", "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV",
2643 "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", "TTOU",
2644 "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", "ERR", "DEBUG", "RETURN",
2645];
2646
2647fn normalize_signal(name: &str) -> String {
2649 let upper = name.to_uppercase();
2650 upper.strip_prefix("SIG").unwrap_or(&upper).to_string()
2651}
2652
2653fn builtin_trap(
2654 args: &[String],
2655 state: &mut InterpreterState,
2656) -> Result<ExecResult, RustBashError> {
2657 if args.is_empty() {
2659 let mut out = String::new();
2660 let mut names: Vec<&String> = state.traps.keys().collect();
2661 names.sort();
2662 for name in names {
2663 let cmd = &state.traps[name];
2664 out.push_str(&format!(
2665 "trap -- '{}' {}\n",
2666 cmd.replace('\'', "'\\''"),
2667 name
2668 ));
2669 }
2670 return Ok(ExecResult {
2671 stdout: out,
2672 ..ExecResult::default()
2673 });
2674 }
2675
2676 if args.len() == 1 && args[0] == "-l" {
2678 let out: String = SIGNAL_NAMES
2679 .iter()
2680 .enumerate()
2681 .map(|(i, s)| {
2682 if i > 0 && i % 8 == 0 {
2683 format!("\n{:2}) SIG{}", i, s)
2684 } else {
2685 format!("{:>3}) SIG{}", i, s)
2686 }
2687 })
2688 .collect::<Vec<_>>()
2689 .join(" ");
2690 return Ok(ExecResult {
2691 stdout: format!("{out}\n"),
2692 ..ExecResult::default()
2693 });
2694 }
2695
2696 if args.first().map(|s| s.as_str()) == Some("-") {
2698 for sig in &args[1..] {
2699 state.traps.remove(&normalize_signal(sig));
2700 }
2701 return Ok(ExecResult::default());
2702 }
2703
2704 if args.len() < 2 {
2706 return Ok(ExecResult {
2707 stderr: "trap: usage: trap [-lp] [[arg] signal_spec ...]\n".to_string(),
2708 exit_code: 2,
2709 ..ExecResult::default()
2710 });
2711 }
2712
2713 let command = &args[0];
2714 for sig in &args[1..] {
2715 let name = normalize_signal(sig);
2716 if command.is_empty() {
2717 state.traps.insert(name, String::new());
2719 } else {
2720 state.traps.insert(name, command.clone());
2721 }
2722 }
2723
2724 Ok(ExecResult::default())
2725}
2726
2727const SHOPT_OPTIONS: &[&str] = &[
2731 "assoc_expand_once",
2732 "autocd",
2733 "cdable_vars",
2734 "cdspell",
2735 "checkhash",
2736 "checkjobs",
2737 "checkwinsize",
2738 "cmdhist",
2739 "complete_fullquote",
2740 "direxpand",
2741 "dirspell",
2742 "dotglob",
2743 "execfail",
2744 "expand_aliases",
2745 "extdebug",
2746 "extglob",
2747 "extquote",
2748 "failglob",
2749 "force_fignore",
2750 "globasciiranges",
2751 "globskipdots",
2752 "globstar",
2753 "gnu_errfmt",
2754 "histappend",
2755 "histreedit",
2756 "histverify",
2757 "hostcomplete",
2758 "huponexit",
2759 "inherit_errexit",
2760 "interactive_comments",
2761 "lastpipe",
2762 "lithist",
2763 "localvar_inherit",
2764 "localvar_unset",
2765 "login_shell",
2766 "mailwarn",
2767 "no_empty_cmd_completion",
2768 "nocaseglob",
2769 "nocasematch",
2770 "nullglob",
2771 "patsub_replacement",
2772 "progcomp",
2773 "progcomp_alias",
2774 "promptvars",
2775 "shift_verbose",
2776 "sourcepath",
2777 "varredir_close",
2778 "xpg_echo",
2779];
2780
2781fn get_shopt(state: &InterpreterState, name: &str) -> Option<bool> {
2782 let o = &state.shopt_opts;
2783 match name {
2784 "assoc_expand_once" => Some(o.assoc_expand_once),
2785 "autocd" => Some(o.autocd),
2786 "cdable_vars" => Some(o.cdable_vars),
2787 "cdspell" => Some(o.cdspell),
2788 "checkhash" => Some(o.checkhash),
2789 "checkjobs" => Some(o.checkjobs),
2790 "checkwinsize" => Some(o.checkwinsize),
2791 "cmdhist" => Some(o.cmdhist),
2792 "complete_fullquote" => Some(o.complete_fullquote),
2793 "direxpand" => Some(o.direxpand),
2794 "dirspell" => Some(o.dirspell),
2795 "dotglob" => Some(o.dotglob),
2796 "execfail" => Some(o.execfail),
2797 "expand_aliases" => Some(o.expand_aliases),
2798 "extdebug" => Some(o.extdebug),
2799 "extglob" => Some(o.extglob),
2800 "extquote" => Some(o.extquote),
2801 "failglob" => Some(o.failglob),
2802 "force_fignore" => Some(o.force_fignore),
2803 "globasciiranges" => Some(o.globasciiranges),
2804 "globskipdots" => Some(o.globskipdots),
2805 "globstar" => Some(o.globstar),
2806 "gnu_errfmt" => Some(o.gnu_errfmt),
2807 "histappend" => Some(o.histappend),
2808 "histreedit" => Some(o.histreedit),
2809 "histverify" => Some(o.histverify),
2810 "hostcomplete" => Some(o.hostcomplete),
2811 "huponexit" => Some(o.huponexit),
2812 "inherit_errexit" => Some(o.inherit_errexit),
2813 "interactive_comments" => Some(o.interactive_comments),
2814 "lastpipe" => Some(o.lastpipe),
2815 "lithist" => Some(o.lithist),
2816 "localvar_inherit" => Some(o.localvar_inherit),
2817 "localvar_unset" => Some(o.localvar_unset),
2818 "login_shell" => Some(o.login_shell),
2819 "mailwarn" => Some(o.mailwarn),
2820 "no_empty_cmd_completion" => Some(o.no_empty_cmd_completion),
2821 "nocaseglob" => Some(o.nocaseglob),
2822 "nocasematch" => Some(o.nocasematch),
2823 "nullglob" => Some(o.nullglob),
2824 "patsub_replacement" => Some(o.patsub_replacement),
2825 "progcomp" => Some(o.progcomp),
2826 "progcomp_alias" => Some(o.progcomp_alias),
2827 "promptvars" => Some(o.promptvars),
2828 "shift_verbose" => Some(o.shift_verbose),
2829 "sourcepath" => Some(o.sourcepath),
2830 "varredir_close" => Some(o.varredir_close),
2831 "xpg_echo" => Some(o.xpg_echo),
2832 _ => None,
2833 }
2834}
2835
2836fn set_shopt(state: &mut InterpreterState, name: &str, value: bool) -> bool {
2837 let o = &mut state.shopt_opts;
2838 match name {
2839 "assoc_expand_once" => o.assoc_expand_once = value,
2840 "autocd" => o.autocd = value,
2841 "cdable_vars" => o.cdable_vars = value,
2842 "cdspell" => o.cdspell = value,
2843 "checkhash" => o.checkhash = value,
2844 "checkjobs" => o.checkjobs = value,
2845 "checkwinsize" => o.checkwinsize = value,
2846 "cmdhist" => o.cmdhist = value,
2847 "complete_fullquote" => o.complete_fullquote = value,
2848 "direxpand" => o.direxpand = value,
2849 "dirspell" => o.dirspell = value,
2850 "dotglob" => o.dotglob = value,
2851 "execfail" => o.execfail = value,
2852 "expand_aliases" => o.expand_aliases = value,
2853 "extdebug" => o.extdebug = value,
2854 "extglob" => o.extglob = value,
2855 "extquote" => o.extquote = value,
2856 "failglob" => o.failglob = value,
2857 "force_fignore" => o.force_fignore = value,
2858 "globasciiranges" => o.globasciiranges = value,
2859 "globskipdots" => o.globskipdots = value,
2860 "globstar" => o.globstar = value,
2861 "gnu_errfmt" => o.gnu_errfmt = value,
2862 "histappend" => o.histappend = value,
2863 "histreedit" => o.histreedit = value,
2864 "histverify" => o.histverify = value,
2865 "hostcomplete" => o.hostcomplete = value,
2866 "huponexit" => o.huponexit = value,
2867 "inherit_errexit" => o.inherit_errexit = value,
2868 "interactive_comments" => o.interactive_comments = value,
2869 "lastpipe" => o.lastpipe = value,
2870 "lithist" => o.lithist = value,
2871 "localvar_inherit" => o.localvar_inherit = value,
2872 "localvar_unset" => o.localvar_unset = value,
2873 "login_shell" => o.login_shell = value,
2874 "mailwarn" => o.mailwarn = value,
2875 "no_empty_cmd_completion" => o.no_empty_cmd_completion = value,
2876 "nocaseglob" => o.nocaseglob = value,
2877 "nocasematch" => o.nocasematch = value,
2878 "nullglob" => o.nullglob = value,
2879 "patsub_replacement" => o.patsub_replacement = value,
2880 "progcomp" => o.progcomp = value,
2881 "progcomp_alias" => o.progcomp_alias = value,
2882 "promptvars" => o.promptvars = value,
2883 "shift_verbose" => o.shift_verbose = value,
2884 "sourcepath" => o.sourcepath = value,
2885 "varredir_close" => o.varredir_close = value,
2886 "xpg_echo" => o.xpg_echo = value,
2887 _ => return false,
2888 }
2889 true
2890}
2891
2892fn builtin_shopt(
2893 args: &[String],
2894 state: &mut InterpreterState,
2895) -> Result<ExecResult, RustBashError> {
2896 let mut set_flag = false; let mut unset_flag = false; let mut query_flag = false; let mut print_flag = false; let mut o_flag = false; let mut opt_names: Vec<&str> = Vec::new();
2903
2904 let mut i = 0;
2905 while i < args.len() {
2906 let arg = &args[i];
2907 if arg.starts_with('-') && arg.len() > 1 && opt_names.is_empty() {
2908 for c in arg[1..].chars() {
2909 match c {
2910 's' => set_flag = true,
2911 'u' => unset_flag = true,
2912 'q' => query_flag = true,
2913 'p' => print_flag = true,
2914 'o' => o_flag = true,
2915 _ => {
2916 return Ok(ExecResult {
2917 stderr: format!("shopt: -{c}: invalid option\n"),
2918 exit_code: 2,
2919 ..ExecResult::default()
2920 });
2921 }
2922 }
2923 }
2924 } else {
2925 opt_names.push(arg);
2926 }
2927 i += 1;
2928 }
2929
2930 if o_flag {
2932 return shopt_o_mode(
2933 set_flag, unset_flag, query_flag, print_flag, &opt_names, state,
2934 );
2935 }
2936
2937 if set_flag {
2939 if opt_names.is_empty() {
2940 let mut out = String::new();
2941 for name in SHOPT_OPTIONS {
2942 if get_shopt(state, name) == Some(true) {
2943 out.push_str(&format!("{name:<20}on\n"));
2944 }
2945 }
2946 return Ok(ExecResult {
2947 stdout: out,
2948 ..ExecResult::default()
2949 });
2950 }
2951 for name in &opt_names {
2952 if !set_shopt(state, name, true) {
2953 return Ok(ExecResult {
2954 stderr: format!("shopt: {name}: invalid shell option name\n"),
2955 exit_code: 1,
2956 ..ExecResult::default()
2957 });
2958 }
2959 }
2960 return Ok(ExecResult::default());
2961 }
2962
2963 if unset_flag {
2965 if opt_names.is_empty() {
2966 let mut out = String::new();
2967 for name in SHOPT_OPTIONS {
2968 if get_shopt(state, name) == Some(false) {
2969 out.push_str(&format!("{name:<20}off\n"));
2970 }
2971 }
2972 return Ok(ExecResult {
2973 stdout: out,
2974 ..ExecResult::default()
2975 });
2976 }
2977 let exit_code = 0;
2978 for name in &opt_names {
2979 if !set_shopt(state, name, false) {
2980 return Ok(ExecResult {
2981 stderr: format!("shopt: {name}: invalid shell option name\n"),
2982 exit_code: 1,
2983 ..ExecResult::default()
2984 });
2985 }
2986 }
2987 return Ok(ExecResult {
2988 exit_code,
2989 ..ExecResult::default()
2990 });
2991 }
2992
2993 if query_flag {
2995 for name in &opt_names {
2996 match get_shopt(state, name) {
2997 Some(true) => {}
2998 Some(false) => {
2999 return Ok(ExecResult {
3000 exit_code: 1,
3001 ..ExecResult::default()
3002 });
3003 }
3004 None => {
3005 return Ok(ExecResult {
3007 stderr: format!("shopt: {name}: invalid shell option name\n"),
3008 exit_code: 1,
3009 ..ExecResult::default()
3010 });
3011 }
3012 }
3013 }
3014 return Ok(ExecResult::default());
3015 }
3016
3017 if print_flag || (!set_flag && !unset_flag && !query_flag) {
3019 let no_args = opt_names.is_empty();
3020 let names: Vec<&str> = if no_args {
3021 SHOPT_OPTIONS.to_vec()
3022 } else {
3023 opt_names
3024 };
3025
3026 if !print_flag && no_args {
3028 let mut out = String::new();
3029 for name in SHOPT_OPTIONS {
3030 let val = get_shopt(state, name).unwrap_or(false);
3031 let status = if val { "on" } else { "off" };
3032 out.push_str(&format!("{name:<20}{status}\n"));
3033 }
3034 return Ok(ExecResult {
3035 stdout: out,
3036 ..ExecResult::default()
3037 });
3038 }
3039
3040 let mut out = String::new();
3042 let mut stderr = String::new();
3043 let mut any_invalid = false;
3044 let mut any_unset = false;
3045 for name in &names {
3046 match get_shopt(state, name) {
3047 Some(val) => {
3048 if !val {
3049 any_unset = true;
3050 }
3051 if print_flag {
3052 let flag = if val { "-s" } else { "-u" };
3053 out.push_str(&format!("shopt {flag} {name}\n"));
3054 } else {
3055 let status = if val { "on" } else { "off" };
3056 out.push_str(&format!("{name:<24}{status}\n"));
3057 }
3058 }
3059 None => {
3060 stderr.push_str(&format!("shopt: {name}: invalid shell option name\n"));
3061 any_invalid = true;
3062 }
3063 }
3064 }
3065 let exit_code = if any_invalid || (!no_args && any_unset) {
3067 1
3068 } else {
3069 0
3070 };
3071 return Ok(ExecResult {
3072 stdout: out,
3073 stderr,
3074 exit_code,
3075 ..ExecResult::default()
3076 });
3077 }
3078
3079 Ok(ExecResult::default())
3080}
3081
3082const SET_O_OPTIONS: &[&str] = &[
3085 "allexport",
3086 "braceexpand",
3087 "emacs",
3088 "errexit",
3089 "hashall",
3090 "histexpand",
3091 "history",
3092 "interactive-comments",
3093 "monitor",
3094 "noclobber",
3095 "noexec",
3096 "noglob",
3097 "nounset",
3098 "pipefail",
3099 "posix",
3100 "verbose",
3101 "vi",
3102 "xtrace",
3103];
3104
3105fn get_set_option(name: &str, state: &InterpreterState) -> Option<bool> {
3106 match name {
3107 "allexport" => Some(state.shell_opts.allexport),
3108 "braceexpand" => Some(true), "emacs" => Some(state.shell_opts.emacs_mode),
3110 "errexit" => Some(state.shell_opts.errexit),
3111 "hashall" => Some(true), "histexpand" => Some(false),
3113 "history" => Some(false),
3114 "interactive-comments" => Some(true),
3115 "monitor" => Some(false),
3116 "noclobber" => Some(state.shell_opts.noclobber),
3117 "noexec" => Some(state.shell_opts.noexec),
3118 "noglob" => Some(state.shell_opts.noglob),
3119 "nounset" => Some(state.shell_opts.nounset),
3120 "pipefail" => Some(state.shell_opts.pipefail),
3121 "posix" => Some(state.shell_opts.posix),
3122 "verbose" => Some(state.shell_opts.verbose),
3123 "vi" => Some(state.shell_opts.vi_mode),
3124 "xtrace" => Some(state.shell_opts.xtrace),
3125 _ => None,
3126 }
3127}
3128
3129fn shopt_o_mode(
3130 set_flag: bool,
3131 unset_flag: bool,
3132 query_flag: bool,
3133 print_flag: bool,
3134 opt_names: &[&str],
3135 state: &mut InterpreterState,
3136) -> Result<ExecResult, RustBashError> {
3137 if set_flag {
3139 if opt_names.is_empty() {
3140 let mut out = String::new();
3141 for name in SET_O_OPTIONS {
3142 if get_set_option(name, state) == Some(true) {
3143 out.push_str(&format!("{name:<20}on\n"));
3144 }
3145 }
3146 return Ok(ExecResult {
3147 stdout: out,
3148 ..ExecResult::default()
3149 });
3150 }
3151 for name in opt_names {
3152 if get_set_option(name, state).is_none() {
3153 return Ok(ExecResult {
3154 stderr: format!("shopt: {name}: invalid shell option name\n"),
3155 exit_code: 1,
3156 ..ExecResult::default()
3157 });
3158 }
3159 apply_option_name(name, true, state);
3160 }
3161 return Ok(ExecResult::default());
3162 }
3163
3164 if unset_flag {
3166 if opt_names.is_empty() {
3167 let mut out = String::new();
3168 for name in SET_O_OPTIONS {
3169 if get_set_option(name, state) == Some(false) {
3170 out.push_str(&format!("{name:<20}off\n"));
3171 }
3172 }
3173 return Ok(ExecResult {
3174 stdout: out,
3175 ..ExecResult::default()
3176 });
3177 }
3178 for name in opt_names {
3179 if get_set_option(name, state).is_none() {
3180 return Ok(ExecResult {
3181 stderr: format!("shopt: {name}: invalid shell option name\n"),
3182 exit_code: 1,
3183 ..ExecResult::default()
3184 });
3185 }
3186 apply_option_name(name, false, state);
3187 }
3188 return Ok(ExecResult::default());
3189 }
3190
3191 if query_flag {
3193 for name in opt_names {
3194 match get_set_option(name, state) {
3195 Some(true) => {}
3196 Some(false) => {
3197 return Ok(ExecResult {
3198 exit_code: 1,
3199 ..ExecResult::default()
3200 });
3201 }
3202 None => {
3203 return Ok(ExecResult {
3204 stderr: format!("shopt: {name}: invalid shell option name\n"),
3205 exit_code: 2,
3206 ..ExecResult::default()
3207 });
3208 }
3209 }
3210 }
3211 return Ok(ExecResult::default());
3212 }
3213
3214 let no_args = opt_names.is_empty();
3216 let names: Vec<&str> = if no_args {
3217 SET_O_OPTIONS.to_vec()
3218 } else {
3219 opt_names.to_vec()
3220 };
3221
3222 if !print_flag && no_args {
3223 let mut out = String::new();
3224 for name in SET_O_OPTIONS {
3225 let val = get_set_option(name, state).unwrap_or(false);
3226 let status = if val { "on" } else { "off" };
3227 out.push_str(&format!("{name:<20}{status}\n"));
3228 }
3229 return Ok(ExecResult {
3230 stdout: out,
3231 ..ExecResult::default()
3232 });
3233 }
3234
3235 let mut out = String::new();
3236 let mut stderr = String::new();
3237 let mut any_invalid = false;
3238 let mut any_unset = false;
3239 for name in &names {
3240 match get_set_option(name, state) {
3241 Some(val) => {
3242 if !val {
3243 any_unset = true;
3244 }
3245 let flag = if val { "-o" } else { "+o" };
3246 out.push_str(&format!("set {flag} {name}\n"));
3247 }
3248 None => {
3249 stderr.push_str(&format!("shopt: {name}: invalid shell option name\n"));
3250 any_invalid = true;
3251 }
3252 }
3253 }
3254 let exit_code = if any_invalid || (!no_args && any_unset) {
3255 1
3256 } else {
3257 0
3258 };
3259 Ok(ExecResult {
3260 stdout: out,
3261 stderr,
3262 exit_code,
3263 ..ExecResult::default()
3264 })
3265}
3266
3267fn builtin_source(
3270 args: &[String],
3271 state: &mut InterpreterState,
3272) -> Result<ExecResult, RustBashError> {
3273 let args = if args.first().map(|a| a.as_str()) == Some("--") {
3275 &args[1..]
3276 } else {
3277 args
3278 };
3279
3280 let path_arg = match args.first() {
3281 Some(p) => p,
3282 None => {
3283 return Ok(ExecResult {
3284 stderr: "source: filename argument required\n".to_string(),
3285 exit_code: 2,
3286 ..ExecResult::default()
3287 });
3288 }
3289 };
3290
3291 let resolved = resolve_path(&state.cwd, path_arg);
3292 let content = match state.fs.read_file(Path::new(&resolved)) {
3293 Ok(bytes) => String::from_utf8_lossy(&bytes).into_owned(),
3294 Err(_) => {
3295 return Ok(ExecResult {
3296 stderr: format!("source: {path_arg}: No such file or directory\n"),
3297 exit_code: 1,
3298 ..ExecResult::default()
3299 });
3300 }
3301 };
3302
3303 state.counters.call_depth += 1;
3304 if state.counters.call_depth > state.limits.max_call_depth {
3305 let actual = state.counters.call_depth;
3306 state.counters.call_depth -= 1;
3307 return Err(RustBashError::LimitExceeded {
3308 limit_name: "max_call_depth",
3309 limit_value: state.limits.max_call_depth,
3310 actual_value: actual,
3311 });
3312 }
3313
3314 let program = match parse(&content) {
3315 Ok(p) => p,
3316 Err(e) => {
3317 state.counters.call_depth -= 1;
3318 let msg = format!("{e}");
3319 return Ok(ExecResult {
3320 stderr: if msg.is_empty() {
3321 String::new()
3322 } else {
3323 format!("{path_arg}: {msg}\n")
3324 },
3325 exit_code: 1,
3326 ..ExecResult::default()
3327 });
3328 }
3329 };
3330 let result = execute_program(&program, state);
3331 state.counters.call_depth -= 1;
3332 result
3333}
3334
3335fn builtin_local(
3338 args: &[String],
3339 state: &mut InterpreterState,
3340) -> Result<ExecResult, RustBashError> {
3341 let mut make_indexed_array = false;
3343 let mut make_assoc_array = false;
3344 let mut make_readonly = false;
3345 let mut make_exported = false;
3346 let mut make_integer = false;
3347 let mut make_nameref = false;
3348 let mut var_args: Vec<&String> = Vec::new();
3349
3350 for arg in args {
3351 if let Some(flags) = arg.strip_prefix('-') {
3352 if flags.is_empty() {
3353 var_args.push(arg);
3354 continue;
3355 }
3356 for c in flags.chars() {
3357 match c {
3358 'a' => make_indexed_array = true,
3359 'A' => make_assoc_array = true,
3360 'r' => make_readonly = true,
3361 'x' => make_exported = true,
3362 'i' => make_integer = true,
3363 'n' => make_nameref = true,
3364 _ => {}
3365 }
3366 }
3367 } else {
3368 var_args.push(arg);
3369 }
3370 }
3371
3372 if var_args.is_empty() {
3374 let mut stdout = String::new();
3375 if let Some(scope) = state.local_scopes.last() {
3376 let mut names: Vec<&String> = scope.keys().collect();
3377 names.sort();
3378 for name in names {
3379 if let Some(var) = state.env.get(name.as_str()) {
3380 stdout.push_str(&format_simple_line(name, var));
3381 }
3382 }
3383 }
3384 return Ok(ExecResult {
3385 stdout,
3386 ..ExecResult::default()
3387 });
3388 }
3389
3390 let mut exit_code = 0;
3391 let mut result_stderr = String::new();
3392 for arg in &var_args {
3393 if let Err(msg) = validate_var_arg(arg, "local") {
3394 result_stderr.push_str(&msg);
3395 exit_code = 1;
3396 continue;
3397 }
3398
3399 if let Some((raw_name, value)) = arg.split_once("+=") {
3400 let name = raw_name;
3402 if let Some(scope) = state.local_scopes.last_mut() {
3403 scope
3404 .entry(name.to_string())
3405 .or_insert_with(|| state.env.get(name).cloned());
3406 }
3407 if value.starts_with('(') && value.ends_with(')') {
3408 let inner = &value[1..value.len() - 1];
3410 let start_idx = match state.env.get(name) {
3411 Some(var) => match &var.value {
3412 VariableValue::IndexedArray(map) => {
3413 map.keys().next_back().map(|k| k + 1).unwrap_or(0)
3414 }
3415 VariableValue::Scalar(s) if s.is_empty() => 0,
3416 VariableValue::Scalar(_) => 1,
3417 _ => 0,
3418 },
3419 None => 0,
3420 };
3421 if !state.env.contains_key(name) {
3422 state.env.insert(
3423 name.to_string(),
3424 Variable {
3425 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
3426 attrs: VariableAttrs::empty(),
3427 },
3428 );
3429 }
3430 let words = shell_split_array_body(inner);
3431 let mut idx = start_idx;
3432 for word in &words {
3433 let val = unquote_simple(word);
3434 crate::interpreter::set_array_element(state, name, idx, val)?;
3435 idx += 1;
3436 }
3437 } else {
3438 let current = state
3439 .env
3440 .get(name)
3441 .map(|v| v.value.as_scalar().to_string())
3442 .unwrap_or_default();
3443 let new_val = format!("{current}{value}");
3444 set_variable(state, name, new_val)?;
3445 }
3446 } else if let Some((name, value)) = arg.split_once('=') {
3447 if let Some(scope) = state.local_scopes.last_mut() {
3449 scope
3450 .entry(name.to_string())
3451 .or_insert_with(|| state.env.get(name).cloned());
3452 }
3453
3454 if make_assoc_array {
3455 if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
3456 state.env.insert(
3457 name.to_string(),
3458 Variable {
3459 value: VariableValue::AssociativeArray(
3460 std::collections::BTreeMap::new(),
3461 ),
3462 attrs: VariableAttrs::empty(),
3463 },
3464 );
3465 let words = shell_split_array_body(inner);
3467 for word in &words {
3468 if let Some(rest) = word.strip_prefix('[')
3469 && let Some(eq_pos) = rest.find("]=")
3470 {
3471 let key = &rest[..eq_pos];
3472 let val = unquote_simple(&rest[eq_pos + 2..]);
3473 if let Some(var) = state.env.get_mut(name)
3474 && let VariableValue::AssociativeArray(map) = &mut var.value
3475 {
3476 map.insert(key.to_string(), val);
3477 }
3478 }
3479 }
3480 } else {
3481 set_variable(state, name, value.to_string())?;
3482 }
3483 } else if make_indexed_array || value.starts_with('(') && value.ends_with(')') {
3484 if let Some(inner) = value.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
3485 state.env.insert(
3486 name.to_string(),
3487 Variable {
3488 value: VariableValue::IndexedArray(std::collections::BTreeMap::new()),
3489 attrs: VariableAttrs::empty(),
3490 },
3491 );
3492 parse_and_set_indexed_array(state, name, inner)?;
3493 } else {
3494 set_variable(state, name, value.to_string())?;
3495 }
3496 } else {
3497 set_variable(state, name, value.to_string())?;
3498 }
3499
3500 if let Some(var) = state.env.get_mut(name) {
3502 if make_readonly {
3503 var.attrs.insert(VariableAttrs::READONLY);
3504 }
3505 if make_exported {
3506 var.attrs.insert(VariableAttrs::EXPORTED);
3507 }
3508 if make_integer {
3509 var.attrs.insert(VariableAttrs::INTEGER);
3510 }
3511 if make_nameref {
3512 var.attrs.insert(VariableAttrs::NAMEREF);
3513 }
3514 }
3515 } else {
3516 if let Some(scope) = state.local_scopes.last_mut() {
3518 scope
3519 .entry(arg.to_string())
3520 .or_insert_with(|| state.env.get(arg.as_str()).cloned());
3521 }
3522 if state.in_function_depth > 0 || !state.env.contains_key(arg.as_str()) {
3524 let value = if make_indexed_array {
3525 VariableValue::IndexedArray(std::collections::BTreeMap::new())
3526 } else if make_assoc_array {
3527 VariableValue::AssociativeArray(std::collections::BTreeMap::new())
3528 } else {
3529 VariableValue::Scalar(String::new())
3530 };
3531 state.env.insert(
3532 arg.to_string(),
3533 Variable {
3534 value,
3535 attrs: VariableAttrs::empty(),
3536 },
3537 );
3538 }
3539 }
3540 }
3541 Ok(ExecResult {
3542 exit_code,
3543 stderr: result_stderr,
3544 ..ExecResult::default()
3545 })
3546}
3547
3548fn builtin_return(
3551 args: &[String],
3552 state: &mut InterpreterState,
3553) -> Result<ExecResult, RustBashError> {
3554 if state.in_function_depth == 0 {
3555 return Ok(ExecResult {
3556 stderr: "return: can only `return' from a function or sourced script\n".to_string(),
3557 exit_code: 1,
3558 ..ExecResult::default()
3559 });
3560 }
3561
3562 let code = if let Some(arg) = args.first() {
3563 match arg.parse::<i32>() {
3564 Ok(n) => n & 0xFF,
3565 Err(_) => {
3566 return Ok(ExecResult {
3567 stderr: format!("return: {arg}: numeric argument required\n"),
3568 exit_code: 2,
3569 ..ExecResult::default()
3570 });
3571 }
3572 }
3573 } else {
3574 state.last_exit_code
3575 };
3576
3577 state.control_flow = Some(ControlFlow::Return(code));
3578 Ok(ExecResult {
3579 exit_code: code,
3580 ..ExecResult::default()
3581 })
3582}
3583
3584fn builtin_let(args: &[String], state: &mut InterpreterState) -> Result<ExecResult, RustBashError> {
3587 if args.is_empty() {
3588 return Err(RustBashError::Execution(
3589 "let: usage: let arg [arg ...]".into(),
3590 ));
3591 }
3592 let mut last_val: i64 = 0;
3593 for arg in args {
3594 last_val = crate::interpreter::arithmetic::eval_arithmetic(arg, state)?;
3595 }
3596 Ok(ExecResult {
3598 exit_code: if last_val != 0 { 0 } else { 1 },
3599 ..ExecResult::default()
3600 })
3601}
3602
3603fn search_path(cmd: &str, state: &InterpreterState) -> Option<String> {
3607 let path_var = state
3608 .env
3609 .get("PATH")
3610 .map(|v| v.value.as_scalar().to_string())
3611 .unwrap_or_else(|| "/usr/bin:/bin".to_string());
3612
3613 for dir in path_var.split(':') {
3614 let candidate = if dir.is_empty() {
3615 format!("./{cmd}")
3616 } else {
3617 format!("{dir}/{cmd}")
3618 };
3619 let p = Path::new(&candidate);
3620 if state.fs.exists(p)
3621 && let Ok(meta) = state.fs.stat(p)
3622 && matches!(meta.node_type, NodeType::File)
3623 {
3624 return Some(candidate);
3625 }
3626 }
3627 None
3628}
3629
3630fn builtin_type(
3633 args: &[String],
3634 state: &mut InterpreterState,
3635) -> Result<ExecResult, RustBashError> {
3636 let mut t_flag = false;
3637 let mut a_flag = false;
3638 let mut p_flag = false;
3639 let mut big_p_flag = false;
3640 let mut f_flag = false;
3641 let mut names: Vec<&str> = Vec::new();
3642
3643 for arg in args {
3644 if arg.starts_with('-') && names.is_empty() {
3645 for c in arg[1..].chars() {
3646 match c {
3647 't' => t_flag = true,
3648 'a' => a_flag = true,
3649 'p' => p_flag = true,
3650 'P' => big_p_flag = true,
3651 'f' => f_flag = true,
3652 _ => {
3653 return Ok(ExecResult {
3654 stderr: format!("type: -{c}: invalid option\n"),
3655 exit_code: 2,
3656 ..ExecResult::default()
3657 });
3658 }
3659 }
3660 }
3661 } else {
3662 names.push(arg);
3663 }
3664 }
3665
3666 if names.is_empty() {
3667 return Ok(ExecResult::default());
3668 }
3669
3670 let mut stdout = String::new();
3671 let mut stderr = String::new();
3672 let mut exit_code = 0;
3673
3674 for name in &names {
3675 let mut found = false;
3676
3677 if big_p_flag {
3679 let paths = search_path_all(name, state);
3680 if paths.is_empty() {
3681 exit_code = 1;
3682 } else {
3683 for path in &paths {
3684 stdout.push_str(&format!("{path}\n"));
3685 found = true;
3686 if !a_flag {
3687 break;
3688 }
3689 }
3690 }
3691 if !found {
3692 exit_code = 1;
3693 }
3694 continue;
3695 }
3696
3697 if let Some(expansion) = state.aliases.get(*name) {
3699 if t_flag {
3700 stdout.push_str("alias\n");
3701 } else if !p_flag {
3702 stdout.push_str(&format!("{name} is aliased to `{expansion}'\n"));
3703 }
3704 found = true;
3705 if !a_flag {
3706 continue;
3707 }
3708 }
3709
3710 if is_shell_keyword(name) {
3712 if t_flag {
3713 stdout.push_str("keyword\n");
3714 } else if !p_flag {
3715 stdout.push_str(&format!("{name} is a shell keyword\n"));
3716 }
3717 found = true;
3718 if !a_flag {
3719 continue;
3720 }
3721 }
3722
3723 if !f_flag && let Some(func) = state.functions.get(*name) {
3725 if t_flag {
3726 stdout.push_str("function\n");
3727 } else if !p_flag {
3728 stdout.push_str(&format!("{name} is a function\n"));
3729 let body_str = format_function_body(name, &func.body);
3731 stdout.push_str(&body_str);
3732 stdout.push('\n');
3733 }
3734 found = true;
3735 if !a_flag {
3736 continue;
3737 }
3738 }
3739
3740 if is_builtin(name) {
3742 if t_flag {
3743 stdout.push_str("builtin\n");
3744 } else if !p_flag {
3745 stdout.push_str(&format!("{name} is a shell builtin\n"));
3746 }
3747 found = true;
3748 if !a_flag {
3749 continue;
3750 }
3751 }
3752
3753 if !is_builtin(name) && state.commands.contains_key(*name) {
3755 if t_flag {
3756 stdout.push_str("builtin\n");
3757 } else if !p_flag {
3758 stdout.push_str(&format!("{name} is a shell builtin\n"));
3759 }
3760 found = true;
3761 if !a_flag {
3762 continue;
3763 }
3764 }
3765
3766 let paths = search_path_all(name, state);
3768 for path in &paths {
3769 if t_flag {
3770 stdout.push_str("file\n");
3771 } else if p_flag {
3772 stdout.push_str(&format!("{path}\n"));
3773 } else {
3774 stdout.push_str(&format!("{name} is {path}\n"));
3775 }
3776 found = true;
3777 if !a_flag {
3778 break;
3779 }
3780 }
3781
3782 if !found {
3783 if !t_flag {
3785 stderr.push_str(&format!("type: {name}: not found\n"));
3786 }
3787 exit_code = 1;
3788 }
3789 }
3790
3791 Ok(ExecResult {
3792 stdout,
3793 stderr,
3794 exit_code,
3795 stdout_bytes: None,
3796 })
3797}
3798
3799fn format_function_body(name: &str, _body: &brush_parser::ast::FunctionBody) -> String {
3801 format!("{name} () \n{{ \n ...\n}}")
3803}
3804
3805fn search_path_all(cmd: &str, state: &InterpreterState) -> Vec<String> {
3807 let path_var = state
3808 .env
3809 .get("PATH")
3810 .map(|v| v.value.as_scalar().to_string())
3811 .unwrap_or_else(|| "/usr/bin:/bin".to_string());
3812 let mut results = Vec::new();
3813 for dir in path_var.split(':') {
3814 let candidate = if dir.is_empty() {
3815 format!("./{cmd}")
3816 } else {
3817 format!("{dir}/{cmd}")
3818 };
3819 let p = Path::new(&candidate);
3820 if state.fs.exists(p)
3821 && let Ok(meta) = state.fs.stat(p)
3822 && matches!(meta.node_type, NodeType::File | NodeType::Symlink)
3823 {
3824 results.push(candidate);
3825 }
3826 }
3827 results
3828}
3829
3830fn builtin_command(
3833 args: &[String],
3834 state: &mut InterpreterState,
3835 stdin: &str,
3836) -> Result<ExecResult, RustBashError> {
3837 let mut v_flag = false;
3838 let mut big_v_flag = false;
3839 let mut cmd_start = 0;
3840
3841 for (i, arg) in args.iter().enumerate() {
3843 if arg.starts_with('-') && cmd_start == i {
3844 let mut consumed = true;
3845 for c in arg[1..].chars() {
3846 match c {
3847 'v' => v_flag = true,
3848 'V' => big_v_flag = true,
3849 'p' => { }
3850 _ => {
3851 consumed = false;
3852 break;
3853 }
3854 }
3855 }
3856 if consumed {
3857 cmd_start = i + 1;
3858 } else {
3859 break;
3860 }
3861 } else {
3862 break;
3863 }
3864 }
3865
3866 let remaining = &args[cmd_start..];
3867 if remaining.is_empty() {
3868 return Ok(ExecResult::default());
3869 }
3870
3871 let name = &remaining[0];
3872
3873 if v_flag {
3875 return command_v(name, state);
3876 }
3877
3878 if big_v_flag {
3880 return command_big_v(name, state);
3881 }
3882
3883 let cmd_args = &remaining[1..];
3885 let cmd_args_owned: Vec<String> = cmd_args.to_vec();
3886
3887 if cmd_args_owned.first().map(|a| a.as_str()) == Some("--help")
3889 && let Some(help) = check_help(name, state)
3890 {
3891 return Ok(help);
3892 }
3893
3894 if let Some(result) = execute_builtin(name, &cmd_args_owned, state, stdin)? {
3896 return Ok(result);
3897 }
3898
3899 if state.commands.contains_key(name.as_str()) {
3901 let env: std::collections::HashMap<String, String> = state
3904 .env
3905 .iter()
3906 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
3907 .collect();
3908 let fs = std::sync::Arc::clone(&state.fs);
3909 let cwd = state.cwd.clone();
3910 let limits = state.limits.clone();
3911 let network_policy = state.network_policy.clone();
3912
3913 let ctx = crate::commands::CommandContext {
3914 fs: &*fs,
3915 cwd: &cwd,
3916 env: &env,
3917 variables: None,
3918 stdin,
3919 stdin_bytes: None,
3920 limits: &limits,
3921 network_policy: &network_policy,
3922 exec: None,
3923 shell_opts: None,
3924 };
3925
3926 let cmd = state.commands.get(name.as_str()).unwrap();
3927 let cmd_result = cmd.execute(&cmd_args_owned, &ctx);
3928 return Ok(ExecResult {
3929 stdout: cmd_result.stdout,
3930 stderr: cmd_result.stderr,
3931 exit_code: cmd_result.exit_code,
3932 stdout_bytes: cmd_result.stdout_bytes,
3933 });
3934 }
3935
3936 Ok(ExecResult {
3938 stderr: format!("{name}: command not found\n"),
3939 exit_code: 127,
3940 ..ExecResult::default()
3941 })
3942}
3943
3944const SHELL_KEYWORDS: &[&str] = &[
3946 "if", "then", "else", "elif", "fi", "case", "esac", "for", "select", "while", "until", "do",
3947 "done", "in", "function", "time", "{", "}", "!", "[[", "]]", "coproc",
3948];
3949
3950fn is_shell_keyword(name: &str) -> bool {
3951 SHELL_KEYWORDS.contains(&name)
3952}
3953
3954fn command_v(name: &str, state: &InterpreterState) -> Result<ExecResult, RustBashError> {
3955 if is_shell_keyword(name) {
3957 return Ok(ExecResult {
3958 stdout: format!("{name}\n"),
3959 ..ExecResult::default()
3960 });
3961 }
3962
3963 if let Some(expansion) = state.aliases.get(name) {
3965 return Ok(ExecResult {
3966 stdout: format!("alias {name}='{expansion}'\n"),
3967 ..ExecResult::default()
3968 });
3969 }
3970
3971 if state.functions.contains_key(name) {
3973 return Ok(ExecResult {
3974 stdout: format!("{name}\n"),
3975 ..ExecResult::default()
3976 });
3977 }
3978
3979 if is_builtin(name) || state.commands.contains_key(name) {
3981 return Ok(ExecResult {
3982 stdout: format!("{name}\n"),
3983 ..ExecResult::default()
3984 });
3985 }
3986
3987 if let Some(path) = search_path(name, state) {
3989 return Ok(ExecResult {
3990 stdout: format!("{path}\n"),
3991 ..ExecResult::default()
3992 });
3993 }
3994
3995 Ok(ExecResult {
3996 exit_code: 1,
3997 ..ExecResult::default()
3998 })
3999}
4000
4001fn command_big_v(name: &str, state: &InterpreterState) -> Result<ExecResult, RustBashError> {
4002 if is_shell_keyword(name) {
4003 return Ok(ExecResult {
4004 stdout: format!("{name} is a shell keyword\n"),
4005 ..ExecResult::default()
4006 });
4007 }
4008
4009 if let Some(expansion) = state.aliases.get(name) {
4010 return Ok(ExecResult {
4011 stdout: format!("{name} is aliased to `{expansion}'\n"),
4012 ..ExecResult::default()
4013 });
4014 }
4015
4016 if state.functions.contains_key(name) {
4017 return Ok(ExecResult {
4018 stdout: format!("{name} is a function\n"),
4019 ..ExecResult::default()
4020 });
4021 }
4022
4023 if is_builtin(name) || state.commands.contains_key(name) {
4024 return Ok(ExecResult {
4025 stdout: format!("{name} is a shell builtin\n"),
4026 ..ExecResult::default()
4027 });
4028 }
4029
4030 if let Some(path) = search_path(name, state) {
4031 return Ok(ExecResult {
4032 stdout: format!("{name} is {path}\n"),
4033 ..ExecResult::default()
4034 });
4035 }
4036
4037 Ok(ExecResult {
4038 stderr: format!("command: {name}: not found\n"),
4039 exit_code: 1,
4040 ..ExecResult::default()
4041 })
4042}
4043
4044fn builtin_builtin(
4047 args: &[String],
4048 state: &mut InterpreterState,
4049 stdin: &str,
4050) -> Result<ExecResult, RustBashError> {
4051 if args.is_empty() {
4052 return Ok(ExecResult::default());
4053 }
4054
4055 let name = &args[0];
4056 let sub_args: Vec<String> = args[1..].to_vec();
4057
4058 if sub_args.first().map(|a| a.as_str()) == Some("--help")
4060 && let Some(help) = check_help(name, state)
4061 {
4062 return Ok(help);
4063 }
4064
4065 if let Some(result) = execute_builtin(name, &sub_args, state, stdin)? {
4067 return Ok(result);
4068 }
4069
4070 if let Some(cmd) = state.commands.get(name.as_str()) {
4072 let env: std::collections::HashMap<String, String> = state
4073 .env
4074 .iter()
4075 .map(|(k, v)| (k.clone(), v.value.as_scalar().to_string()))
4076 .collect();
4077 let fs = std::sync::Arc::clone(&state.fs);
4078 let cwd = state.cwd.clone();
4079 let limits = state.limits.clone();
4080 let network_policy = state.network_policy.clone();
4081
4082 let ctx = crate::commands::CommandContext {
4083 fs: &*fs,
4084 cwd: &cwd,
4085 env: &env,
4086 variables: None,
4087 stdin,
4088 stdin_bytes: None,
4089 limits: &limits,
4090 network_policy: &network_policy,
4091 exec: None,
4092 shell_opts: None,
4093 };
4094
4095 let cmd_result = cmd.execute(&sub_args, &ctx);
4096 return Ok(ExecResult {
4097 stdout: cmd_result.stdout,
4098 stderr: cmd_result.stderr,
4099 exit_code: cmd_result.exit_code,
4100 stdout_bytes: cmd_result.stdout_bytes,
4101 });
4102 }
4103
4104 Ok(ExecResult {
4105 stderr: format!("builtin: {name}: not a shell builtin\n"),
4106 exit_code: 1,
4107 ..ExecResult::default()
4108 })
4109}
4110
4111fn builtin_getopts(
4114 args: &[String],
4115 state: &mut InterpreterState,
4116) -> Result<ExecResult, RustBashError> {
4117 if args.len() < 2 {
4118 return Ok(ExecResult {
4119 stderr: "getopts: usage: getopts optstring name [arg ...]\n".to_string(),
4120 exit_code: 2,
4121 ..ExecResult::default()
4122 });
4123 }
4124
4125 let optstring = &args[0];
4126 let var_name = &args[1];
4127
4128 let option_args: Vec<String> = if args.len() > 2 {
4130 args[2..].to_vec()
4131 } else {
4132 state.positional_params.clone()
4133 };
4134
4135 loop {
4138 let optind: usize = state
4139 .env
4140 .get("OPTIND")
4141 .and_then(|v| v.value.as_scalar().parse().ok())
4142 .unwrap_or(1);
4143
4144 let idx = optind.saturating_sub(1);
4145
4146 if idx >= option_args.len() {
4147 set_variable(state, var_name, "?".to_string())?;
4148 return Ok(ExecResult {
4149 exit_code: 1,
4150 ..ExecResult::default()
4151 });
4152 }
4153
4154 let current_arg = &option_args[idx];
4155
4156 if !current_arg.starts_with('-') || current_arg == "-" || current_arg == "--" {
4157 set_variable(state, var_name, "?".to_string())?;
4158 if current_arg == "--" {
4159 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4160 }
4161 return Ok(ExecResult {
4162 exit_code: 1,
4163 ..ExecResult::default()
4164 });
4165 }
4166
4167 let opt_chars: Vec<char> = current_arg[1..].chars().collect();
4168
4169 let sub_pos: usize = state
4170 .env
4171 .get("__GETOPTS_SUBPOS")
4172 .and_then(|v| v.value.as_scalar().parse().ok())
4173 .unwrap_or(0);
4174
4175 if sub_pos >= opt_chars.len() {
4176 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4178 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4179 continue;
4180 }
4181
4182 let opt_char = opt_chars[sub_pos];
4183 let silent = optstring.starts_with(':');
4184 let optstring_chars: &str = if silent { &optstring[1..] } else { optstring };
4185 let opt_pos = optstring_chars.find(opt_char);
4186
4187 if let Some(pos) = opt_pos {
4188 let takes_arg = optstring_chars.chars().nth(pos + 1) == Some(':');
4189
4190 if takes_arg {
4191 let rest: String = opt_chars[sub_pos + 1..].iter().collect();
4192 if !rest.is_empty() {
4193 set_variable(state, "OPTARG", rest)?;
4194 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4195 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4196 } else if idx + 1 < option_args.len() {
4197 set_variable(state, "OPTARG", option_args[idx + 1].clone())?;
4198 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4199 set_variable(state, "OPTIND", (optind + 2).to_string())?;
4200 } else {
4201 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4203 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4204 if silent {
4205 set_variable(state, var_name, ":".to_string())?;
4206 set_variable(state, "OPTARG", opt_char.to_string())?;
4207 return Ok(ExecResult::default());
4208 }
4209 set_variable(state, var_name, "?".to_string())?;
4210 return Ok(ExecResult {
4211 stderr: format!("getopts: option requires an argument -- '{opt_char}'\n"),
4212 ..ExecResult::default()
4213 });
4214 }
4215 } else {
4216 state.env.remove("OPTARG");
4217 if sub_pos + 1 < opt_chars.len() {
4218 set_variable(state, "__GETOPTS_SUBPOS", (sub_pos + 1).to_string())?;
4219 } else {
4220 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4221 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4222 }
4223 }
4224 set_variable(state, var_name, opt_char.to_string())?;
4225 return Ok(ExecResult::default());
4226 }
4227
4228 if silent {
4230 set_variable(state, var_name, "?".to_string())?;
4231 set_variable(state, "OPTARG", opt_char.to_string())?;
4232 } else {
4233 set_variable(state, var_name, "?".to_string())?;
4234 }
4235 if sub_pos + 1 < opt_chars.len() {
4236 set_variable(state, "__GETOPTS_SUBPOS", (sub_pos + 1).to_string())?;
4237 } else {
4238 set_variable(state, "__GETOPTS_SUBPOS", "0".to_string())?;
4239 set_variable(state, "OPTIND", (optind + 1).to_string())?;
4240 }
4241 let stderr = if silent {
4242 String::new()
4243 } else {
4244 format!("getopts: illegal option -- '{opt_char}'\n")
4245 };
4246 return Ok(ExecResult {
4247 stderr,
4248 ..ExecResult::default()
4249 });
4250 }
4251}
4252
4253fn builtin_mapfile(
4256 args: &[String],
4257 state: &mut InterpreterState,
4258 stdin: &str,
4259) -> Result<ExecResult, RustBashError> {
4260 let mut strip_newline = false;
4261 let mut delimiter = '\n';
4262 let mut max_count: Option<usize> = None;
4263 let mut skip_count: usize = 0;
4264 let mut array_name = "MAPFILE".to_string();
4265 let mut i = 0;
4266
4267 while i < args.len() {
4268 let arg = &args[i];
4269 if arg.starts_with('-') && arg.len() > 1 {
4270 let mut chars = arg[1..].chars();
4271 while let Some(c) = chars.next() {
4272 match c {
4273 't' => strip_newline = true,
4274 'd' => {
4275 let rest: String = chars.collect();
4276 let delim_str = if rest.is_empty() {
4277 i += 1;
4278 if i < args.len() { args[i].as_str() } else { "" }
4279 } else {
4280 &rest
4281 };
4282 delimiter = delim_str.chars().next().unwrap_or('\0');
4283 break;
4284 }
4285 'n' => {
4286 let rest: String = chars.collect();
4287 let count_str = if rest.is_empty() {
4288 i += 1;
4289 if i < args.len() {
4290 args[i].as_str()
4291 } else {
4292 "0"
4293 }
4294 } else {
4295 &rest
4296 };
4297 max_count = count_str.parse().ok();
4298 break;
4299 }
4300 's' => {
4301 let rest: String = chars.collect();
4302 let count_str = if rest.is_empty() {
4303 i += 1;
4304 if i < args.len() {
4305 args[i].as_str()
4306 } else {
4307 "0"
4308 }
4309 } else {
4310 &rest
4311 };
4312 skip_count = count_str.parse().unwrap_or(0);
4313 break;
4314 }
4315 'C' | 'c' | 'O' | 'u' => {
4316 let rest: String = chars.collect();
4318 if rest.is_empty() {
4319 i += 1; }
4321 break;
4322 }
4323 _ => {
4324 return Ok(ExecResult {
4325 stderr: format!("mapfile: -{c}: invalid option\n"),
4326 exit_code: 2,
4327 ..ExecResult::default()
4328 });
4329 }
4330 }
4331 }
4332 } else {
4333 array_name = arg.clone();
4334 }
4335 i += 1;
4336 }
4337
4338 let lines: Vec<&str> = if delimiter == '\0' {
4340 stdin.split('\0').collect()
4342 } else {
4343 split_keeping_delimiter(stdin, delimiter)
4344 };
4345
4346 let mut map = std::collections::BTreeMap::new();
4348 let mut count = 0;
4349
4350 for (line_idx, line) in lines.iter().enumerate() {
4351 if line_idx < skip_count {
4352 continue;
4353 }
4354 if let Some(max) = max_count
4355 && count >= max
4356 {
4357 break;
4358 }
4359
4360 let value = if strip_newline {
4361 line.trim_end_matches(delimiter).to_string()
4362 } else {
4363 (*line).to_string()
4364 };
4365
4366 if map.len() >= state.limits.max_array_elements {
4367 return Err(RustBashError::LimitExceeded {
4368 limit_name: "max_array_elements",
4369 limit_value: state.limits.max_array_elements,
4370 actual_value: map.len() + 1,
4371 });
4372 }
4373 map.insert(count, value);
4374 count += 1;
4375 }
4376
4377 state.env.insert(
4378 array_name,
4379 Variable {
4380 value: VariableValue::IndexedArray(map),
4381 attrs: VariableAttrs::empty(),
4382 },
4383 );
4384
4385 Ok(ExecResult::default())
4386}
4387
4388fn split_keeping_delimiter(s: &str, delim: char) -> Vec<&str> {
4391 let mut result = Vec::new();
4392 let mut start = 0;
4393 for (i, c) in s.char_indices() {
4394 if c == delim {
4395 let end = i + c.len_utf8();
4396 result.push(&s[start..end]);
4397 start = end;
4398 }
4399 }
4400 if start < s.len() {
4402 result.push(&s[start..]);
4403 }
4404 result
4405}
4406
4407fn builtin_pushd(
4410 args: &[String],
4411 state: &mut InterpreterState,
4412) -> Result<ExecResult, RustBashError> {
4413 if args.is_empty() {
4414 if state.dir_stack.is_empty() {
4416 return Ok(ExecResult {
4417 stderr: "pushd: no other directory\n".to_string(),
4418 exit_code: 1,
4419 ..ExecResult::default()
4420 });
4421 }
4422 let top = state.dir_stack.remove(0);
4423 let old_cwd = state.cwd.clone();
4424 let result = builtin_cd(std::slice::from_ref(&top), state)?;
4426 if result.exit_code != 0 {
4427 state.dir_stack.insert(0, top);
4428 return Ok(result);
4429 }
4430 state.dir_stack.insert(0, old_cwd);
4431 return Ok(dirs_output(state));
4432 }
4433
4434 let mut positional = Vec::new();
4436 let mut saw_dashdash = false;
4437 for arg in args {
4438 if saw_dashdash {
4439 positional.push(arg);
4440 } else if arg == "--" {
4441 saw_dashdash = true;
4442 } else if arg == "-" {
4443 positional.push(arg);
4444 } else if arg.starts_with('-')
4445 && !arg[1..].chars().next().is_some_and(|c| c.is_ascii_digit())
4446 && !arg.starts_with('+')
4447 {
4448 return Ok(ExecResult {
4450 stderr: format!("pushd: {arg}: invalid option\n"),
4451 exit_code: 2,
4452 ..ExecResult::default()
4453 });
4454 } else {
4455 positional.push(arg);
4456 }
4457 }
4458
4459 if positional.len() > 1 {
4460 return Ok(ExecResult {
4461 stderr: "pushd: too many arguments\n".to_string(),
4462 exit_code: 1,
4463 ..ExecResult::default()
4464 });
4465 }
4466
4467 let arg = positional.first().copied().unwrap_or(&args[0]);
4468
4469 if (arg.starts_with('+') || arg.starts_with('-'))
4471 && let Ok(n) = arg[1..].parse::<usize>()
4472 {
4473 let stack_size = state.dir_stack.len() + 1; if n >= stack_size {
4475 return Ok(ExecResult {
4476 stderr: format!("pushd: {arg}: directory stack index out of range\n"),
4477 exit_code: 1,
4478 ..ExecResult::default()
4479 });
4480 }
4481
4482 let mut full_stack = vec![state.cwd.clone()];
4484 full_stack.extend(state.dir_stack.iter().cloned());
4485
4486 let rotate_n = if arg.starts_with('+') {
4487 n
4488 } else {
4489 stack_size - n
4490 };
4491 full_stack.rotate_left(rotate_n);
4492
4493 state.cwd = full_stack.remove(0);
4494 state.dir_stack = full_stack;
4495
4496 let cwd = state.cwd.clone();
4497 let _ = set_variable(state, "PWD", cwd);
4498 return Ok(dirs_output(state));
4499 }
4500
4501 let old_cwd = state.cwd.clone();
4503 let result = builtin_cd(std::slice::from_ref(arg), state)?;
4504 if result.exit_code != 0 {
4505 return Ok(result);
4506 }
4507 state.dir_stack.insert(0, old_cwd);
4508
4509 Ok(dirs_output(state))
4510}
4511
4512fn builtin_popd(
4515 args: &[String],
4516 state: &mut InterpreterState,
4517) -> Result<ExecResult, RustBashError> {
4518 if state.dir_stack.is_empty() {
4519 return Ok(ExecResult {
4520 stderr: "popd: directory stack empty\n".to_string(),
4521 exit_code: 1,
4522 ..ExecResult::default()
4523 });
4524 }
4525
4526 if !args.is_empty() {
4527 let arg = &args[0];
4528
4529 if arg == "--" {
4531 let top = state.dir_stack.remove(0);
4533 let result = builtin_cd(std::slice::from_ref(&top), state)?;
4534 if result.exit_code != 0 {
4535 state.dir_stack.insert(0, top);
4536 return Ok(result);
4537 }
4538 return Ok(dirs_output(state));
4539 }
4540
4541 if (arg.starts_with('+') || arg.starts_with('-'))
4543 && let Ok(n) = arg[1..].parse::<usize>()
4544 {
4545 let stack_size = state.dir_stack.len() + 1;
4546 if n >= stack_size {
4547 return Ok(ExecResult {
4548 stderr: format!("popd: {arg}: directory stack index out of range\n"),
4549 exit_code: 1,
4550 ..ExecResult::default()
4551 });
4552 }
4553 let idx = if arg.starts_with('+') {
4554 n
4555 } else {
4556 stack_size - 1 - n
4557 };
4558 if idx == 0 {
4559 let new_cwd = state.dir_stack.remove(0);
4561 state.cwd = new_cwd;
4562 let cwd = state.cwd.clone();
4563 let _ = set_variable(state, "PWD", cwd);
4564 } else {
4565 state.dir_stack.remove(idx - 1);
4566 }
4567 return Ok(dirs_output(state));
4568 }
4569
4570 return Ok(ExecResult {
4572 stderr: format!("popd: {arg}: invalid argument\n"),
4573 exit_code: 2,
4574 ..ExecResult::default()
4575 });
4576 }
4577
4578 let top = state.dir_stack.remove(0);
4580 let result = builtin_cd(std::slice::from_ref(&top), state)?;
4581 if result.exit_code != 0 {
4582 state.dir_stack.insert(0, top);
4583 return Ok(result);
4584 }
4585
4586 Ok(dirs_output(state))
4587}
4588
4589fn builtin_dirs(
4592 args: &[String],
4593 state: &mut InterpreterState,
4594) -> Result<ExecResult, RustBashError> {
4595 let mut clear = false;
4596 let mut per_line = false;
4597 let mut with_index = false;
4598 let mut long_format = false;
4599
4600 for arg in args {
4601 if let Some(flags) = arg.strip_prefix('-') {
4602 if flags.is_empty() {
4603 return Ok(ExecResult {
4605 stderr: "dirs: -: invalid option\n".to_string(),
4606 exit_code: 1,
4607 ..ExecResult::default()
4608 });
4609 }
4610 for c in flags.chars() {
4611 match c {
4612 'c' => clear = true,
4613 'p' => per_line = true,
4614 'v' => {
4615 with_index = true;
4616 per_line = true;
4617 }
4618 'l' => long_format = true,
4619 _ => {
4620 return Ok(ExecResult {
4621 stderr: format!("dirs: -{c}: invalid option\n"),
4622 exit_code: 2,
4623 ..ExecResult::default()
4624 });
4625 }
4626 }
4627 }
4628 } else if arg.starts_with('+') {
4629 continue;
4631 } else {
4632 return Ok(ExecResult {
4634 stderr: format!("dirs: {arg}: invalid argument\n"),
4635 exit_code: 1,
4636 ..ExecResult::default()
4637 });
4638 }
4639 }
4640
4641 if clear {
4642 state.dir_stack.clear();
4643 return Ok(ExecResult::default());
4644 }
4645
4646 let home = state
4647 .env
4648 .get("HOME")
4649 .map(|v| v.value.as_scalar().to_string())
4650 .unwrap_or_default();
4651
4652 let mut entries = vec![state.cwd.clone()];
4654 entries.extend(state.dir_stack.iter().cloned());
4655
4656 let mut stdout = String::new();
4657 if with_index {
4658 for (i, entry) in entries.iter().enumerate() {
4659 let display = if !long_format
4660 && !home.is_empty()
4661 && (*entry == home || entry.starts_with(&format!("{home}/")))
4662 {
4663 format!("~{}", &entry[home.len()..])
4664 } else {
4665 entry.clone()
4666 };
4667 stdout.push_str(&format!(" {i} {display}\n"));
4668 }
4669 } else if per_line {
4670 for entry in &entries {
4671 let display = if !long_format
4672 && !home.is_empty()
4673 && (*entry == home || entry.starts_with(&format!("{home}/")))
4674 {
4675 format!("~{}", &entry[home.len()..])
4676 } else {
4677 entry.clone()
4678 };
4679 stdout.push_str(&format!("{display}\n"));
4680 }
4681 } else {
4682 let display_entries: Vec<String> = entries
4683 .iter()
4684 .map(|e| {
4685 if !long_format
4686 && !home.is_empty()
4687 && (*e == home || e.starts_with(&format!("{home}/")))
4688 {
4689 format!("~{}", &e[home.len()..])
4690 } else {
4691 e.clone()
4692 }
4693 })
4694 .collect();
4695 stdout = display_entries.join(" ");
4696 stdout.push('\n');
4697 }
4698
4699 Ok(ExecResult {
4700 stdout,
4701 ..ExecResult::default()
4702 })
4703}
4704
4705fn dirs_output(state: &InterpreterState) -> ExecResult {
4707 let mut entries = vec![state.cwd.clone()];
4708 entries.extend(state.dir_stack.iter().cloned());
4709
4710 let home = state
4711 .env
4712 .get("HOME")
4713 .map(|v| v.value.as_scalar().to_string())
4714 .unwrap_or_default();
4715
4716 let display_entries: Vec<String> = entries
4717 .iter()
4718 .map(|e| {
4719 if !home.is_empty() && (*e == home || e.starts_with(&format!("{home}/"))) {
4720 format!("~{}", &e[home.len()..])
4721 } else {
4722 e.clone()
4723 }
4724 })
4725 .collect();
4726
4727 ExecResult {
4728 stdout: format!("{}\n", display_entries.join(" ")),
4729 ..ExecResult::default()
4730 }
4731}
4732
4733fn builtin_hash(
4736 args: &[String],
4737 state: &mut InterpreterState,
4738) -> Result<ExecResult, RustBashError> {
4739 if args.is_empty() {
4740 if state.command_hash.is_empty() {
4742 return Ok(ExecResult {
4743 stderr: "hash: hash table empty\n".to_string(),
4744 ..ExecResult::default()
4745 });
4746 }
4747 let mut stdout = String::new();
4748 let mut entries: Vec<(&String, &String)> = state.command_hash.iter().collect();
4749 entries.sort_by_key(|(k, _)| k.as_str());
4750 for (name, path) in entries {
4751 stdout.push_str(&format!("{name}={path}\n"));
4752 }
4753 return Ok(ExecResult {
4754 stdout,
4755 ..ExecResult::default()
4756 });
4757 }
4758
4759 let mut reset = false;
4760 let mut names: Vec<&str> = Vec::new();
4761
4762 for arg in args {
4763 if arg == "-r" {
4764 reset = true;
4765 } else if arg.starts_with('-') {
4766 } else {
4768 names.push(arg);
4769 }
4770 }
4771
4772 if reset {
4773 state.command_hash.clear();
4774 }
4775
4776 for name in &names {
4777 if let Some(path) = search_path(name, state) {
4778 state.command_hash.insert(name.to_string(), path);
4779 } else {
4780 return Ok(ExecResult {
4781 stderr: format!("hash: {name}: not found\n"),
4782 exit_code: 1,
4783 ..ExecResult::default()
4784 });
4785 }
4786 }
4787
4788 Ok(ExecResult::default())
4789}
4790
4791fn builtin_alias(
4794 args: &[String],
4795 state: &mut InterpreterState,
4796) -> Result<ExecResult, RustBashError> {
4797 if args.is_empty() {
4798 let mut entries: Vec<(&String, &String)> = state.aliases.iter().collect();
4800 entries.sort_by_key(|(k, _)| k.as_str());
4801 let mut stdout = String::new();
4802 for (name, value) in entries {
4803 stdout.push_str(&format!("alias {name}='{value}'\n"));
4804 }
4805 return Ok(ExecResult {
4806 stdout,
4807 ..ExecResult::default()
4808 });
4809 }
4810
4811 let mut exit_code = 0;
4812 let mut stdout = String::new();
4813 let mut stderr = String::new();
4814
4815 for arg in args {
4816 if arg.starts_with('-') {
4817 if arg == "-p" {
4819 let mut entries: Vec<(&String, &String)> = state.aliases.iter().collect();
4820 entries.sort_by_key(|(k, _)| k.as_str());
4821 for (name, value) in &entries {
4822 stdout.push_str(&format!("alias {name}='{value}'\n"));
4823 }
4824 }
4825 continue;
4826 }
4827
4828 if let Some(eq_pos) = arg.find('=') {
4829 let name = &arg[..eq_pos];
4831 let value = &arg[eq_pos + 1..];
4832 state.aliases.insert(name.to_string(), value.to_string());
4833 } else {
4834 if let Some(value) = state.aliases.get(arg.as_str()) {
4836 stdout.push_str(&format!("alias {arg}='{value}'\n"));
4837 } else {
4838 stderr.push_str(&format!("alias: {arg}: not found\n"));
4839 exit_code = 1;
4840 }
4841 }
4842 }
4843
4844 Ok(ExecResult {
4845 stdout,
4846 stderr,
4847 exit_code,
4848 stdout_bytes: None,
4849 })
4850}
4851
4852fn builtin_unalias(
4853 args: &[String],
4854 state: &mut InterpreterState,
4855) -> Result<ExecResult, RustBashError> {
4856 if args.is_empty() {
4857 return Ok(ExecResult {
4858 stderr: "unalias: usage: unalias [-a] name [name ...]\n".to_string(),
4859 exit_code: 2,
4860 ..ExecResult::default()
4861 });
4862 }
4863
4864 let mut exit_code = 0;
4865 let mut stderr = String::new();
4866
4867 for arg in args {
4868 if arg == "-a" {
4869 state.aliases.clear();
4870 continue;
4871 }
4872 if state.aliases.remove(arg.as_str()).is_none() {
4873 stderr.push_str(&format!("unalias: {arg}: not found\n"));
4874 exit_code = 1;
4875 }
4876 }
4877
4878 Ok(ExecResult {
4879 stderr,
4880 exit_code,
4881 ..ExecResult::default()
4882 })
4883}
4884
4885fn builtin_printf(
4888 args: &[String],
4889 state: &mut InterpreterState,
4890) -> Result<ExecResult, RustBashError> {
4891 if args.is_empty() {
4892 return Ok(ExecResult {
4893 stderr: "printf: usage: printf [-v var] format [arguments]\n".into(),
4894 exit_code: 2,
4895 ..ExecResult::default()
4896 });
4897 }
4898
4899 let mut var_name: Option<String> = None;
4900 let mut remaining_args = args;
4901
4902 if remaining_args.len() >= 2 && remaining_args[0] == "-v" {
4904 let vname = &remaining_args[1];
4905 let base_name = vname.split('[').next().unwrap_or(vname);
4907 let valid_base = !base_name.is_empty()
4908 && base_name
4909 .chars()
4910 .all(|c| c.is_ascii_alphanumeric() || c == '_')
4911 && !base_name.starts_with(|c: char| c.is_ascii_digit());
4912 let valid_subscript = if let Some(bracket_pos) = vname.find('[') {
4913 vname.ends_with(']') && bracket_pos + 1 < vname.len() - 1
4914 } else {
4915 true
4916 };
4917 if !valid_base || !valid_subscript {
4918 return Ok(ExecResult {
4919 stderr: format!("printf: `{vname}': not a valid identifier\n"),
4920 exit_code: 2,
4921 ..ExecResult::default()
4922 });
4923 }
4924 var_name = Some(vname.clone());
4925 remaining_args = &remaining_args[2..];
4926 }
4927
4928 if !remaining_args.is_empty() && remaining_args[0] == "--" {
4930 remaining_args = &remaining_args[1..];
4931 }
4932
4933 if remaining_args.is_empty() {
4934 return Ok(ExecResult {
4935 stderr: "printf: usage: printf [-v var] format [arguments]\n".into(),
4936 exit_code: 2,
4937 ..ExecResult::default()
4938 });
4939 }
4940
4941 let format_str = &remaining_args[0];
4942 let arguments = &remaining_args[1..];
4943 let result = crate::commands::text::run_printf_format(format_str, arguments);
4944
4945 let exit_code = if result.had_error { 1 } else { 0 };
4946
4947 if let Some(name) = var_name {
4948 set_variable(state, &name, result.stdout)?;
4949 Ok(ExecResult {
4950 stderr: result.stderr,
4951 exit_code,
4952 ..ExecResult::default()
4953 })
4954 } else {
4955 Ok(ExecResult {
4956 stdout: result.stdout,
4957 stderr: result.stderr,
4958 exit_code,
4959 ..ExecResult::default()
4960 })
4961 }
4962}
4963
4964fn builtin_sh(
4967 args: &[String],
4968 state: &mut InterpreterState,
4969 stdin: &str,
4970) -> Result<ExecResult, RustBashError> {
4971 if args.is_empty() {
4972 if stdin.is_empty() {
4973 return Ok(ExecResult::default());
4974 }
4975 let program = parse(stdin)?;
4976 return run_in_subshell(state, &program, &[], None);
4977 }
4978
4979 let mut i = 0;
4980 while i < args.len() {
4981 let arg = &args[i];
4982 if arg == "-c" {
4983 i += 1;
4984 if i < args.len() {
4985 let cmd = &args[i];
4986 let extra = &args[i + 1..];
4987 let shell_name_override = extra.first().map(|s| s.as_str());
4989 let positional: Vec<String> = if extra.len() > 1 {
4990 extra[1..].iter().map(|s| s.to_string()).collect()
4991 } else {
4992 Vec::new()
4993 };
4994 let program = parse(cmd)?;
4995 return run_in_subshell(state, &program, &positional, shell_name_override);
4996 } else {
4997 return Ok(ExecResult {
4998 stderr: "sh: -c: option requires an argument\n".into(),
4999 exit_code: 2,
5000 ..ExecResult::default()
5001 });
5002 }
5003 } else if arg.starts_with('-') && arg.len() > 1 {
5004 i += 1;
5005 continue;
5006 } else {
5007 let path = crate::interpreter::builtins::resolve_path(&state.cwd, arg);
5008 let path_buf = std::path::PathBuf::from(&path);
5009 match state.fs.read_file(&path_buf) {
5010 Ok(bytes) => {
5011 let script = String::from_utf8_lossy(&bytes).to_string();
5012 let positional = args[i + 1..]
5013 .iter()
5014 .map(|s| s.to_string())
5015 .collect::<Vec<_>>();
5016 let program = parse(&script)?;
5017 return run_in_subshell(state, &program, &positional, None);
5018 }
5019 Err(e) => {
5020 return Ok(ExecResult {
5021 stderr: format!("sh: {}: {}\n", arg, e),
5022 exit_code: 127,
5023 ..ExecResult::default()
5024 });
5025 }
5026 }
5027 }
5028 }
5029
5030 Ok(ExecResult::default())
5031}
5032
5033fn run_in_subshell(
5036 state: &mut InterpreterState,
5037 program: &brush_parser::ast::Program,
5038 positional: &[String],
5039 shell_name_override: Option<&str>,
5040) -> Result<ExecResult, RustBashError> {
5041 use std::collections::HashMap;
5042 let cloned_fs = state.fs.deep_clone();
5043 let mut sub_state = InterpreterState {
5044 fs: cloned_fs,
5045 env: state.env.clone(),
5046 cwd: state.cwd.clone(),
5047 functions: state.functions.clone(),
5048 last_exit_code: state.last_exit_code,
5049 commands: crate::interpreter::walker::clone_commands(&state.commands),
5050 shell_opts: state.shell_opts.clone(),
5051 shopt_opts: state.shopt_opts.clone(),
5052 limits: state.limits.clone(),
5053 counters: crate::interpreter::ExecutionCounters {
5054 command_count: state.counters.command_count,
5055 output_size: state.counters.output_size,
5056 start_time: state.counters.start_time,
5057 substitution_depth: state.counters.substitution_depth,
5058 call_depth: 0,
5059 },
5060 network_policy: state.network_policy.clone(),
5061 should_exit: false,
5062 loop_depth: 0,
5063 control_flow: None,
5064 positional_params: if positional.is_empty() {
5065 state.positional_params.clone()
5066 } else {
5067 positional.to_vec()
5068 },
5069 shell_name: shell_name_override
5070 .map(|s| s.to_string())
5071 .unwrap_or_else(|| state.shell_name.clone()),
5072 random_seed: state.random_seed,
5073 local_scopes: Vec::new(),
5074 in_function_depth: 0,
5075 traps: state.traps.clone(),
5076 in_trap: false,
5077 errexit_suppressed: 0,
5078 stdin_offset: 0,
5079 dir_stack: state.dir_stack.clone(),
5080 command_hash: state.command_hash.clone(),
5081 aliases: state.aliases.clone(),
5082 current_lineno: state.current_lineno,
5083 shell_start_time: state.shell_start_time,
5084 last_argument: state.last_argument.clone(),
5085 call_stack: state.call_stack.clone(),
5086 machtype: state.machtype.clone(),
5087 hosttype: state.hosttype.clone(),
5088 persistent_fds: state.persistent_fds.clone(),
5089 next_auto_fd: state.next_auto_fd,
5090 proc_sub_counter: state.proc_sub_counter,
5091 proc_sub_prealloc: HashMap::new(),
5092 pipe_stdin_bytes: None,
5093 pending_cmdsub_stderr: String::new(),
5094 };
5095
5096 let result = execute_program(program, &mut sub_state);
5097
5098 state.counters.command_count = sub_state.counters.command_count;
5100 state.counters.output_size = sub_state.counters.output_size;
5101
5102 result
5103}
5104
5105fn builtin_help(args: &[String], state: &InterpreterState) -> Result<ExecResult, RustBashError> {
5108 if args.is_empty() {
5109 let mut stdout = String::from("Shell builtin commands:\n\n");
5111 let mut names: Vec<&str> = builtin_names().to_vec();
5112 names.sort();
5113 for name in &names {
5114 if let Some(meta) = builtin_meta(name) {
5115 stdout.push_str(&format!(" {:<16} {}\n", name, meta.description));
5116 } else {
5117 stdout.push_str(&format!(" {}\n", name));
5118 }
5119 }
5120 return Ok(ExecResult {
5121 stdout,
5122 ..ExecResult::default()
5123 });
5124 }
5125
5126 let name = &args[0];
5127
5128 if let Some(meta) = builtin_meta(name) {
5130 return Ok(ExecResult {
5131 stdout: crate::commands::format_help(meta),
5132 ..ExecResult::default()
5133 });
5134 }
5135
5136 if let Some(cmd) = state.commands.get(name.as_str())
5138 && let Some(meta) = cmd.meta()
5139 {
5140 return Ok(ExecResult {
5141 stdout: crate::commands::format_help(meta),
5142 ..ExecResult::default()
5143 });
5144 }
5145
5146 Ok(ExecResult {
5147 stderr: format!("help: no help topics match '{}'\n", name),
5148 exit_code: 1,
5149 ..ExecResult::default()
5150 })
5151}
5152
5153#[cfg(test)]
5154mod tests {
5155 use super::*;
5156 use crate::interpreter::{ExecutionCounters, ExecutionLimits, ShellOpts, ShoptOpts};
5157 use crate::network::NetworkPolicy;
5158 use crate::platform::Instant;
5159 use crate::vfs::{InMemoryFs, VirtualFs};
5160 use std::collections::HashMap;
5161 use std::sync::Arc;
5162
5163 fn make_state() -> InterpreterState {
5164 let fs = Arc::new(InMemoryFs::new());
5165 fs.mkdir_p(Path::new("/home/user")).unwrap();
5166
5167 InterpreterState {
5168 fs,
5169 env: HashMap::new(),
5170 cwd: "/".to_string(),
5171 functions: HashMap::new(),
5172 last_exit_code: 0,
5173 commands: HashMap::new(),
5174 shell_opts: ShellOpts::default(),
5175 shopt_opts: ShoptOpts::default(),
5176 limits: ExecutionLimits::default(),
5177 counters: ExecutionCounters::default(),
5178 network_policy: NetworkPolicy::default(),
5179 should_exit: false,
5180 loop_depth: 0,
5181 control_flow: None,
5182 positional_params: Vec::new(),
5183 shell_name: "rust-bash".to_string(),
5184 random_seed: 42,
5185 local_scopes: Vec::new(),
5186 in_function_depth: 0,
5187 traps: HashMap::new(),
5188 in_trap: false,
5189 errexit_suppressed: 0,
5190 stdin_offset: 0,
5191 dir_stack: Vec::new(),
5192 command_hash: HashMap::new(),
5193 aliases: HashMap::new(),
5194 current_lineno: 0,
5195 shell_start_time: Instant::now(),
5196 last_argument: String::new(),
5197 call_stack: Vec::new(),
5198 machtype: "x86_64-pc-linux-gnu".to_string(),
5199 hosttype: "x86_64".to_string(),
5200 persistent_fds: HashMap::new(),
5201 next_auto_fd: 10,
5202 proc_sub_counter: 0,
5203 proc_sub_prealloc: HashMap::new(),
5204 pipe_stdin_bytes: None,
5205 pending_cmdsub_stderr: String::new(),
5206 }
5207 }
5208
5209 #[test]
5210 fn cd_to_directory() {
5211 let mut state = make_state();
5212 let result = builtin_cd(&["/home/user".to_string()], &mut state).unwrap();
5213 assert_eq!(result.exit_code, 0);
5214 assert_eq!(state.cwd, "/home/user");
5215 }
5216
5217 #[test]
5218 fn cd_nonexistent() {
5219 let mut state = make_state();
5220 let result = builtin_cd(&["/nonexistent".to_string()], &mut state).unwrap();
5221 assert_eq!(result.exit_code, 1);
5222 assert!(result.stderr.contains("No such file or directory"));
5223 }
5224
5225 #[test]
5226 fn cd_home() {
5227 let mut state = make_state();
5228 state.env.insert(
5229 "HOME".to_string(),
5230 Variable {
5231 value: VariableValue::Scalar("/home/user".to_string()),
5232 attrs: VariableAttrs::EXPORTED,
5233 },
5234 );
5235 let result = builtin_cd(&[], &mut state).unwrap();
5236 assert_eq!(result.exit_code, 0);
5237 assert_eq!(state.cwd, "/home/user");
5238 }
5239
5240 #[test]
5241 fn cd_dash() {
5242 let mut state = make_state();
5243 state.env.insert(
5244 "OLDPWD".to_string(),
5245 Variable {
5246 value: VariableValue::Scalar("/home/user".to_string()),
5247 attrs: VariableAttrs::EXPORTED,
5248 },
5249 );
5250 let result = builtin_cd(&["-".to_string()], &mut state).unwrap();
5251 assert_eq!(result.exit_code, 0);
5252 assert_eq!(state.cwd, "/home/user");
5253 assert!(result.stdout.contains("/home/user"));
5254 }
5255
5256 #[test]
5257 fn export_and_list() {
5258 let mut state = make_state();
5259 builtin_export(&["FOO=bar".to_string()], &mut state).unwrap();
5260 assert!(state.env.get("FOO").unwrap().exported());
5261 assert_eq!(state.env.get("FOO").unwrap().value.as_scalar(), "bar");
5262 }
5263
5264 #[test]
5265 fn unset_variable() {
5266 let mut state = make_state();
5267 set_variable(&mut state, "FOO", "bar".to_string()).unwrap();
5268 builtin_unset(&["FOO".to_string()], &mut state).unwrap();
5269 assert!(!state.env.contains_key("FOO"));
5270 }
5271
5272 #[test]
5273 fn unset_readonly_fails() {
5274 let mut state = make_state();
5275 state.env.insert(
5276 "FOO".to_string(),
5277 Variable {
5278 value: VariableValue::Scalar("bar".to_string()),
5279 attrs: VariableAttrs::READONLY,
5280 },
5281 );
5282 let result = builtin_unset(&["FOO".to_string()], &mut state).unwrap();
5283 assert_eq!(result.exit_code, 1);
5284 assert!(state.env.contains_key("FOO"));
5285 }
5286
5287 #[test]
5288 fn set_positional_params() {
5289 let mut state = make_state();
5290 builtin_set(
5291 &[
5292 "--".to_string(),
5293 "a".to_string(),
5294 "b".to_string(),
5295 "c".to_string(),
5296 ],
5297 &mut state,
5298 )
5299 .unwrap();
5300 assert_eq!(state.positional_params, vec!["a", "b", "c"]);
5301 }
5302
5303 #[test]
5304 fn set_errexit() {
5305 let mut state = make_state();
5306 builtin_set(&["-e".to_string()], &mut state).unwrap();
5307 assert!(state.shell_opts.errexit);
5308 builtin_set(&["+e".to_string()], &mut state).unwrap();
5309 assert!(!state.shell_opts.errexit);
5310 }
5311
5312 #[test]
5313 fn shift_params() {
5314 let mut state = make_state();
5315 state.positional_params = vec!["a".to_string(), "b".to_string(), "c".to_string()];
5316 builtin_shift(&[], &mut state).unwrap();
5317 assert_eq!(state.positional_params, vec!["b", "c"]);
5318 }
5319
5320 #[test]
5321 fn shift_too_many() {
5322 let mut state = make_state();
5323 state.positional_params = vec!["a".to_string()];
5324 let result = builtin_shift(&["5".to_string()], &mut state).unwrap();
5325 assert_eq!(result.exit_code, 1);
5326 }
5327
5328 #[test]
5329 fn readonly_variable() {
5330 let mut state = make_state();
5331 builtin_readonly(&["FOO=bar".to_string()], &mut state).unwrap();
5332 assert!(state.env.get("FOO").unwrap().readonly());
5333 assert_eq!(state.env.get("FOO").unwrap().value.as_scalar(), "bar");
5334 }
5335
5336 #[test]
5337 fn declare_readonly() {
5338 let mut state = make_state();
5339 builtin_declare(&["-r".to_string(), "X=42".to_string()], &mut state).unwrap();
5340 assert!(state.env.get("X").unwrap().readonly());
5341 }
5342
5343 #[test]
5344 fn read_single_var() {
5345 let mut state = make_state();
5346 builtin_read(&["NAME".to_string()], &mut state, "hello world\n").unwrap();
5347 assert_eq!(
5348 state.env.get("NAME").unwrap().value.as_scalar(),
5349 "hello world"
5350 );
5351 }
5352
5353 #[test]
5354 fn read_multiple_vars() {
5355 let mut state = make_state();
5356 builtin_read(
5357 &["A".to_string(), "B".to_string()],
5358 &mut state,
5359 "one two three\n",
5360 )
5361 .unwrap();
5362 assert_eq!(state.env.get("A").unwrap().value.as_scalar(), "one");
5363 assert_eq!(state.env.get("B").unwrap().value.as_scalar(), "two three");
5364 }
5365
5366 #[test]
5367 fn read_reply_default() {
5368 let mut state = make_state();
5369 builtin_read(&[], &mut state, "test input\n").unwrap();
5370 assert_eq!(
5371 state.env.get("REPLY").unwrap().value.as_scalar(),
5372 "test input"
5373 );
5374 }
5375
5376 #[test]
5377 fn read_eof_returns_1() {
5378 let mut state = make_state();
5379 let result = builtin_read(&["VAR".to_string()], &mut state, "").unwrap();
5380 assert_eq!(result.exit_code, 1);
5381 }
5382
5383 #[test]
5384 fn read_into_array() {
5385 let mut state = make_state();
5386 builtin_read(
5387 &["-r".to_string(), "-a".to_string(), "arr".to_string()],
5388 &mut state,
5389 "a b c\n",
5390 )
5391 .unwrap();
5392 let var = state.env.get("arr").unwrap();
5393 match &var.value {
5394 VariableValue::IndexedArray(map) => {
5395 assert_eq!(map.get(&0).unwrap(), "a");
5396 assert_eq!(map.get(&1).unwrap(), "b");
5397 assert_eq!(map.get(&2).unwrap(), "c");
5398 assert_eq!(map.len(), 3);
5399 }
5400 _ => panic!("expected indexed array"),
5401 }
5402 }
5403
5404 #[test]
5405 fn read_delimiter() {
5406 let mut state = make_state();
5407 builtin_read(
5408 &["-d".to_string(), ":".to_string(), "x".to_string()],
5409 &mut state,
5410 "a:b:c",
5411 )
5412 .unwrap();
5413 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "a");
5414 }
5415
5416 #[test]
5417 fn read_delimiter_empty_reads_until_eof() {
5418 let mut state = make_state();
5419 builtin_read(
5420 &["-d".to_string(), "".to_string(), "x".to_string()],
5421 &mut state,
5422 "hello\nworld",
5423 )
5424 .unwrap();
5425 assert_eq!(
5426 state.env.get("x").unwrap().value.as_scalar(),
5427 "hello\nworld"
5428 );
5429 }
5430
5431 #[test]
5432 fn read_n_count() {
5433 let mut state = make_state();
5434 builtin_read(
5435 &["-n".to_string(), "3".to_string(), "x".to_string()],
5436 &mut state,
5437 "hello\n",
5438 )
5439 .unwrap();
5440 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "hel");
5441 }
5442
5443 #[test]
5444 fn read_n_stops_at_newline() {
5445 let mut state = make_state();
5446 builtin_read(
5447 &["-n".to_string(), "10".to_string(), "x".to_string()],
5448 &mut state,
5449 "hi\nthere\n",
5450 )
5451 .unwrap();
5452 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "hi");
5453 }
5454
5455 #[test]
5456 fn read_big_n_includes_newlines() {
5457 let mut state = make_state();
5458 builtin_read(
5459 &["-N".to_string(), "4".to_string(), "x".to_string()],
5460 &mut state,
5461 "ab\ncd",
5462 )
5463 .unwrap();
5464 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "ab\nc");
5465 }
5466
5467 #[test]
5468 fn read_silent_flag_accepted() {
5469 let mut state = make_state();
5470 let result = builtin_read(
5471 &["-s".to_string(), "VAR".to_string()],
5472 &mut state,
5473 "secret\n",
5474 )
5475 .unwrap();
5476 assert_eq!(result.exit_code, 0);
5477 assert_eq!(state.env.get("VAR").unwrap().value.as_scalar(), "secret");
5478 }
5479
5480 #[test]
5481 fn read_timeout_stub_with_data() {
5482 let mut state = make_state();
5483 let result = builtin_read(
5484 &["-t".to_string(), "1".to_string(), "VAR".to_string()],
5485 &mut state,
5486 "data\n",
5487 )
5488 .unwrap();
5489 assert_eq!(result.exit_code, 0);
5490 assert_eq!(state.env.get("VAR").unwrap().value.as_scalar(), "data");
5491 }
5492
5493 #[test]
5494 fn read_timeout_stub_no_data() {
5495 let mut state = make_state();
5496 let result = builtin_read(
5497 &["-t".to_string(), "1".to_string(), "VAR".to_string()],
5498 &mut state,
5499 "",
5500 )
5501 .unwrap();
5502 assert_eq!(result.exit_code, 1);
5503 }
5504
5505 #[test]
5506 fn read_combined_ra_flags() {
5507 let mut state = make_state();
5508 builtin_read(
5509 &["-ra".to_string(), "arr".to_string()],
5510 &mut state,
5511 "x y z\n",
5512 )
5513 .unwrap();
5514 let var = state.env.get("arr").unwrap();
5515 match &var.value {
5516 VariableValue::IndexedArray(map) => {
5517 assert_eq!(map.len(), 3);
5518 assert_eq!(map.get(&0).unwrap(), "x");
5519 assert_eq!(map.get(&1).unwrap(), "y");
5520 assert_eq!(map.get(&2).unwrap(), "z");
5521 }
5522 _ => panic!("expected indexed array"),
5523 }
5524 }
5525
5526 #[test]
5527 fn read_delimiter_not_found_returns_1() {
5528 let mut state = make_state();
5529 let result = builtin_read(
5530 &["-d".to_string(), ":".to_string(), "x".to_string()],
5531 &mut state,
5532 "abc",
5533 )
5534 .unwrap();
5535 assert_eq!(result.exit_code, 1);
5536 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "abc");
5537 }
5538
5539 #[test]
5540 fn read_delimiter_empty_returns_1() {
5541 let mut state = make_state();
5542 let result = builtin_read(
5543 &["-d".to_string(), "".to_string(), "x".to_string()],
5544 &mut state,
5545 "hello\nworld",
5546 )
5547 .unwrap();
5548 assert_eq!(result.exit_code, 1);
5549 }
5550
5551 #[test]
5552 fn read_big_n_short_read_returns_1() {
5553 let mut state = make_state();
5554 let result = builtin_read(
5555 &["-N".to_string(), "10".to_string(), "x".to_string()],
5556 &mut state,
5557 "ab",
5558 )
5559 .unwrap();
5560 assert_eq!(result.exit_code, 1);
5561 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "ab");
5562 }
5563
5564 #[test]
5565 fn read_big_n_preserves_backslash() {
5566 let mut state = make_state();
5567 builtin_read(
5568 &["-N".to_string(), "4".to_string(), "x".to_string()],
5569 &mut state,
5570 "a\\bc",
5571 )
5572 .unwrap();
5573 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "a\\bc");
5574 }
5575
5576 #[test]
5577 fn read_n_zero_assigns_empty() {
5578 let mut state = make_state();
5579 let result = builtin_read(
5580 &["-n".to_string(), "0".to_string(), "x".to_string()],
5581 &mut state,
5582 "hello\n",
5583 )
5584 .unwrap();
5585 assert_eq!(result.exit_code, 0);
5586 assert_eq!(state.env.get("x").unwrap().value.as_scalar(), "");
5587 }
5588
5589 #[test]
5590 fn read_big_n_clears_extra_vars() {
5591 let mut state = make_state();
5592 builtin_read(
5593 &[
5594 "-N".to_string(),
5595 "4".to_string(),
5596 "a".to_string(),
5597 "b".to_string(),
5598 ],
5599 &mut state,
5600 "abcd",
5601 )
5602 .unwrap();
5603 assert_eq!(state.env.get("a").unwrap().value.as_scalar(), "abcd");
5604 assert_eq!(state.env.get("b").unwrap().value.as_scalar(), "");
5605 }
5606
5607 #[test]
5608 fn resolve_relative_path() {
5609 assert_eq!(resolve_path("/home/user", "docs"), "/home/user/docs");
5610 assert_eq!(resolve_path("/home/user", ".."), "/home");
5611 assert_eq!(resolve_path("/home/user", "/tmp"), "/tmp");
5612 }
5613
5614 #[test]
5615 fn builtin_names_is_nonempty() {
5616 assert!(
5617 !builtin_names().is_empty(),
5618 "builtin_names() should list at least one builtin"
5619 );
5620 for &name in builtin_names() {
5622 assert!(is_builtin(name));
5623 }
5624 }
5625
5626 #[test]
5627 fn all_builtins_have_meta() {
5628 let missing: Vec<&str> = builtin_names()
5629 .iter()
5630 .filter(|&&name| builtin_meta(name).is_none())
5631 .copied()
5632 .collect();
5633 assert!(missing.is_empty(), "Builtins missing meta: {:?}", missing);
5634 }
5635}