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