1use std::borrow::Cow;
2use std::collections::{HashMap, VecDeque};
3use std::fmt::Write as _;
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use rand::Rng;
9
10use crate::arithmetic::Evaluatable;
11use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment};
12use crate::interp::{self, Execute, ExecutionParameters, ExecutionResult};
13use crate::options::RuntimeOptions;
14use crate::sys::fs::PathExt;
15use crate::variables::{self, ShellValue, ShellVariable};
16use crate::{
17 builtins, commands, completion, env, error, expansion, functions, jobs, keywords, openfiles,
18 patterns, prompt, sys::users, traps,
19};
20use crate::{pathcache, sys, trace_categories};
21
22const BASH_MAJOR: u32 = 5;
23const BASH_MINOR: u32 = 2;
24const BASH_PATCH: u32 = 15;
25const BASH_BUILD: u32 = 1;
26const BASH_RELEASE: &str = "release";
27const BASH_MACHINE: &str = "unknown";
28
29pub struct Shell {
31 pub traps: traps::TrapHandlerConfig,
35 open_files: openfiles::OpenFiles,
37 pub working_dir: PathBuf,
39 pub env: ShellEnvironment,
41 pub funcs: functions::FunctionEnv,
43 pub options: RuntimeOptions,
45 pub jobs: jobs::JobManager,
47 pub aliases: HashMap<String, String>,
49
50 pub last_exit_status: u8,
54
55 pub last_pipeline_statuses: Vec<u8>,
57
58 depth: usize,
60
61 pub shell_name: Option<String>,
63
64 pub positional_parameters: Vec<String>,
66
67 pub shell_product_display_str: Option<String>,
69
70 script_call_stack: VecDeque<(ScriptCallType, String)>,
72
73 function_call_stack: VecDeque<FunctionCall>,
75
76 pub directory_stack: Vec<PathBuf>,
78
79 current_line_number: u32,
81
82 pub completion_config: completion::Config,
84
85 pub builtins: HashMap<String, builtins::Registration>,
87
88 pub program_location_cache: pathcache::PathCache,
90
91 last_stopwatch_time: std::time::SystemTime,
93
94 last_stopwatch_offset: u32,
96}
97
98impl Clone for Shell {
99 fn clone(&self) -> Self {
100 Self {
101 traps: self.traps.clone(),
102 open_files: self.open_files.clone(),
103 working_dir: self.working_dir.clone(),
104 env: self.env.clone(),
105 funcs: self.funcs.clone(),
106 options: self.options.clone(),
107 jobs: jobs::JobManager::new(),
108 aliases: self.aliases.clone(),
109 last_exit_status: self.last_exit_status,
110 last_pipeline_statuses: self.last_pipeline_statuses.clone(),
111 positional_parameters: self.positional_parameters.clone(),
112 shell_name: self.shell_name.clone(),
113 shell_product_display_str: self.shell_product_display_str.clone(),
114 function_call_stack: self.function_call_stack.clone(),
115 script_call_stack: self.script_call_stack.clone(),
116 directory_stack: self.directory_stack.clone(),
117 current_line_number: self.current_line_number,
118 completion_config: self.completion_config.clone(),
119 builtins: self.builtins.clone(),
120 program_location_cache: self.program_location_cache.clone(),
121 last_stopwatch_time: self.last_stopwatch_time,
122 last_stopwatch_offset: self.last_stopwatch_offset,
123 depth: self.depth + 1,
124 }
125 }
126}
127
128impl AsRef<Shell> for Shell {
129 fn as_ref(&self) -> &Shell {
130 self
131 }
132}
133
134impl AsMut<Shell> for Shell {
135 fn as_mut(&mut self) -> &mut Shell {
136 self
137 }
138}
139
140#[derive(Debug, Default)]
142pub struct CreateOptions {
143 pub disabled_shopt_options: Vec<String>,
145 pub enabled_shopt_options: Vec<String>,
147 pub disallow_overwriting_regular_files_via_output_redirection: bool,
149 pub do_not_execute_commands: bool,
151 pub exit_after_one_command: bool,
153 pub interactive: bool,
155 pub login: bool,
157 pub no_editing: bool,
159 pub no_profile: bool,
161 pub no_rc: bool,
163 pub do_not_inherit_env: bool,
165 pub posix: bool,
167 pub print_commands_and_arguments: bool,
169 pub read_commands_from_stdin: bool,
171 pub shell_name: Option<String>,
173 pub shell_product_display_str: Option<String>,
175 pub sh_mode: bool,
177 pub verbose: bool,
179 pub max_function_call_depth: Option<usize>,
181}
182
183#[derive(Clone, Debug)]
185pub enum ScriptCallType {
186 Sourced,
188 Executed,
190}
191
192#[derive(Clone, Debug)]
194pub struct FunctionCall {
195 function_name: String,
197 function_definition: Arc<brush_parser::ast::FunctionDefinition>,
199}
200
201impl Shell {
202 pub async fn new(options: &CreateOptions) -> Result<Shell, error::Error> {
208 let mut shell = Shell {
210 traps: traps::TrapHandlerConfig::default(),
211 open_files: openfiles::OpenFiles::default(),
212 working_dir: std::env::current_dir()?,
213 env: env::ShellEnvironment::new(),
214 funcs: functions::FunctionEnv::default(),
215 options: RuntimeOptions::defaults_from(options),
216 jobs: jobs::JobManager::new(),
217 aliases: HashMap::default(),
218 last_exit_status: 0,
219 last_pipeline_statuses: vec![0],
220 positional_parameters: vec![],
221 shell_name: options.shell_name.clone(),
222 shell_product_display_str: options.shell_product_display_str.clone(),
223 function_call_stack: VecDeque::new(),
224 script_call_stack: VecDeque::new(),
225 directory_stack: vec![],
226 current_line_number: 0,
227 completion_config: completion::Config::default(),
228 builtins: builtins::get_default_builtins(options),
229 program_location_cache: pathcache::PathCache::default(),
230 last_stopwatch_time: std::time::SystemTime::now(),
231 last_stopwatch_offset: 0,
232 depth: 0,
233 };
234
235 shell.options.extended_globbing = true;
238
239 shell.initialize_vars(options)?;
241
242 shell.load_config(options).await?;
244
245 Ok(shell)
246 }
247
248 #[allow(clippy::too_many_lines)]
249 #[allow(clippy::unwrap_in_result)]
250 fn initialize_vars(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
251 if !options.do_not_inherit_env {
253 for (k, v) in std::env::vars() {
254 let mut var = ShellVariable::new(ShellValue::String(v));
255 var.export();
256 self.env.set_global(k, var)?;
257 }
258 }
259
260 if let Some(shell_name) = &options.shell_name {
264 self.env
265 .set_global("BASH", ShellVariable::new(shell_name.into()))?;
266 }
267
268 let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic {
270 getter: |shell| shell.options.get_shopt_optstr().into(),
271 setter: |_| (),
272 });
273 bashopts_var.set_readonly();
274 self.env.set_global("BASHOPTS", bashopts_var)?;
275
276 #[cfg(not(target_family = "wasm"))]
278 {
279 let mut bashpid_var =
280 ShellVariable::new(ShellValue::String(std::process::id().to_string()));
281 bashpid_var.treat_as_integer();
282 self.env.set_global("BASHPID", bashpid_var)?;
283 }
284
285 self.env.set_global(
287 "BASH_ALIASES",
288 ShellVariable::new(ShellValue::Dynamic {
289 getter: |shell| {
290 let values = variables::ArrayLiteral(
291 shell
292 .aliases
293 .iter()
294 .map(|(k, v)| (Some(k.to_owned()), v.to_owned()))
295 .collect::<Vec<_>>(),
296 );
297
298 ShellValue::associative_array_from_literals(values).unwrap()
299 },
300 setter: |_| (),
301 }),
302 )?;
303
304 self.env.set_global(
310 "BASH_ARGV0",
311 ShellVariable::new(ShellValue::Dynamic {
312 getter: |shell| {
313 let argv0 = shell.shell_name.as_deref().unwrap_or_default();
314 argv0.to_string().into()
315 },
316 setter: |_| (),
318 }),
319 )?;
320
321 self.env.set_global(
323 "BASH_CMDS",
324 ShellVariable::new(ShellValue::Dynamic {
325 getter: |shell| shell.program_location_cache.to_value().unwrap(),
326 setter: |_| (),
327 }),
328 )?;
329
330 self.env.set_global(
336 "BASH_SOURCE",
337 ShellVariable::new(ShellValue::Dynamic {
338 getter: |shell| shell.get_bash_source_value(),
339 setter: |_| (),
340 }),
341 )?;
342
343 self.env.set_global(
345 "BASH_SUBSHELL",
346 ShellVariable::new(ShellValue::Dynamic {
347 getter: |shell| shell.depth.to_string().into(),
348 setter: |_| (),
349 }),
350 )?;
351
352 let mut bash_versinfo_var = ShellVariable::new(ShellValue::indexed_array_from_strs(
354 [
355 BASH_MAJOR.to_string().as_str(),
356 BASH_MINOR.to_string().as_str(),
357 BASH_PATCH.to_string().as_str(),
358 BASH_BUILD.to_string().as_str(),
359 BASH_RELEASE,
360 BASH_MACHINE,
361 ]
362 .as_slice(),
363 ));
364 bash_versinfo_var.set_readonly();
365 self.env.set_global("BASH_VERSINFO", bash_versinfo_var)?;
366
367 self.env.set_global(
369 "BASH_VERSION",
370 ShellVariable::new(
371 std::format!("{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}")
372 .into(),
373 ),
374 )?;
375
376 self.env.set_global(
378 "COMP_WORDBREAKS",
379 ShellVariable::new(" \t\n\"\'@><=;|&(:".into()),
380 )?;
381
382 self.env.set_global(
384 "DIRSTACK",
385 ShellVariable::new(ShellValue::Dynamic {
386 getter: |shell| {
387 shell
388 .directory_stack
389 .iter()
390 .map(|p| p.to_string_lossy().to_string())
391 .collect::<Vec<_>>()
392 .into()
393 },
394 setter: |_| (),
395 }),
396 )?;
397
398 self.env.set_global(
400 "EPOCHREALTIME",
401 ShellVariable::new(ShellValue::Dynamic {
402 getter: |_shell| {
403 let now = std::time::SystemTime::now();
404 let since_epoch = now
405 .duration_since(std::time::UNIX_EPOCH)
406 .unwrap_or_default();
407 since_epoch.as_secs_f64().to_string().into()
408 },
409 setter: |_| (),
410 }),
411 )?;
412
413 self.env.set_global(
415 "EPOCHSECONDS",
416 ShellVariable::new(ShellValue::Dynamic {
417 getter: |_shell| {
418 let now = std::time::SystemTime::now();
419 let since_epoch = now
420 .duration_since(std::time::UNIX_EPOCH)
421 .unwrap_or_default();
422 since_epoch.as_secs().to_string().into()
423 },
424 setter: |_| (),
425 }),
426 )?;
427
428 #[cfg(unix)]
430 {
431 let mut euid_var = ShellVariable::new(ShellValue::String(format!(
432 "{}",
433 uzers::get_effective_uid()
434 )));
435 euid_var.treat_as_integer().set_readonly();
436 self.env.set_global("EUID", euid_var)?;
437 }
438
439 self.env.set_global(
441 "FUNCNAME",
442 ShellVariable::new(ShellValue::Dynamic {
443 getter: |shell| shell.get_funcname_value(),
444 setter: |_| (),
445 }),
446 )?;
447
448 self.env.set_global(
452 "GROUPS",
453 ShellVariable::new(ShellValue::Dynamic {
454 getter: |_shell| {
455 let groups = sys::users::get_user_group_ids().unwrap_or_default();
456 ShellValue::indexed_array_from_strings(
457 groups.into_iter().map(|gid| gid.to_string()),
458 )
459 },
460 setter: |_| (),
461 }),
462 )?;
463
464 if !self.env.is_set("HISTFILE") {
468 if let Some(home_dir) = self.get_home_dir() {
469 let histfile = home_dir.join(".brush_history");
470 self.env.set_global(
471 "HISTFILE",
472 ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
473 )?;
474 }
475 }
476
477 self.env.set_global(
479 "HOSTNAME",
480 ShellVariable::new(
481 sys::network::get_hostname()
482 .unwrap_or_default()
483 .to_string_lossy()
484 .to_string()
485 .into(),
486 ),
487 )?;
488
489 #[cfg(unix)]
491 {
492 if let Ok(info) = nix::sys::utsname::uname() {
493 self.env.set_global(
494 "HOSTTYPE",
495 ShellVariable::new(info.machine().to_string_lossy().to_string().into()),
496 )?;
497 }
498 }
499
500 self.env
502 .set_global("IFS", ShellVariable::new(" \t\n".into()))?;
503
504 self.env.set_global(
506 "LINENO",
507 ShellVariable::new(ShellValue::Dynamic {
508 getter: |shell| shell.current_line_number.to_string().into(),
509 setter: |_| (),
510 }),
511 )?;
512
513 self.env
515 .set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE.into()))?;
516
517 if !self.env.is_set("OLDPWD") {
519 let mut oldpwd_var =
520 ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped));
521 oldpwd_var.export();
522 self.env.set_global("OLDPWD", oldpwd_var)?;
523 }
524
525 self.env
527 .set_global("OPTERR", ShellVariable::new("1".into()))?;
528
529 let mut optind_var = ShellVariable::new("1".into());
531 optind_var.treat_as_integer();
532 self.env.set_global("OPTIND", optind_var)?;
533
534 let os_type = match std::env::consts::OS {
536 "linux" => "linux-gnu",
537 "windows" => "windows",
538 _ => "unknown",
539 };
540 self.env
541 .set_global("OSTYPE", ShellVariable::new(os_type.into()))?;
542
543 #[cfg(unix)]
545 if !self.env.is_set("PATH") {
546 let default_path_str = sys::fs::get_default_executable_search_paths().join(":");
547 self.env
548 .set_global("PATH", ShellVariable::new(default_path_str.into()))?;
549 }
550
551 self.env.set_global(
555 "PIPESTATUS",
556 ShellVariable::new(ShellValue::Dynamic {
557 getter: |shell| {
558 ShellValue::indexed_array_from_strings(
559 shell.last_pipeline_statuses.iter().map(|s| s.to_string()),
560 )
561 },
562 setter: |_| (),
563 }),
564 )?;
565
566 if let Some(ppid) = sys::terminal::get_parent_process_id() {
568 let mut ppid_var = ShellVariable::new(ppid.to_string().into());
569 ppid_var.treat_as_integer().set_readonly();
570 self.env.set_global("PPID", ppid_var)?;
571 }
572
573 let mut random_var = ShellVariable::new(ShellValue::Dynamic {
575 getter: get_random_value,
576 setter: |_| (),
577 });
578 random_var.treat_as_integer();
579 self.env.set_global("RANDOM", random_var)?;
580
581 self.env.set_global(
583 "SECONDS",
584 ShellVariable::new(ShellValue::Dynamic {
585 getter: |shell| {
586 let now = std::time::SystemTime::now();
587 let since_last = now
588 .duration_since(shell.last_stopwatch_time)
589 .unwrap_or_default();
590 let total_seconds =
591 since_last.as_secs() + u64::from(shell.last_stopwatch_offset);
592 total_seconds.to_string().into()
593 },
594 setter: |_| (),
596 }),
597 )?;
598
599 if let Ok(exe_path) = std::env::current_exe() {
601 self.env.set_global(
602 "SHELL",
603 ShellVariable::new(exe_path.to_string_lossy().to_string().into()),
604 )?;
605 }
606
607 let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic {
609 getter: |shell| shell.options.get_set_o_optstr().into(),
610 setter: |_| (),
611 });
612 shellopts_var.set_readonly();
613 self.env.set_global("SHELLOPTS", shellopts_var)?;
614
615 let input_shlvl = self.get_env_str("SHLVL").unwrap_or("0".into());
617 let updated_shlvl = input_shlvl.as_ref().parse::<u32>().unwrap_or(0) + 1;
618 let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string().into());
619 shlvl_var.export();
620 self.env.set_global("SHLVL", shlvl_var)?;
621
622 let mut random_var = ShellVariable::new(ShellValue::Dynamic {
624 getter: get_srandom_value,
625 setter: |_| (),
626 });
627 random_var.treat_as_integer();
628 self.env.set_global("SRANDOM", random_var)?;
629
630 if options.interactive {
632 if !self.env.is_set("PS1") {
633 self.env
634 .set_global("PS1", ShellVariable::new(r"\s-\v\$ ".into()))?;
635 }
636
637 if !self.env.is_set("PS2") {
638 self.env
639 .set_global("PS2", ShellVariable::new("> ".into()))?;
640 }
641 }
642
643 if !self.env.is_set("PS4") {
645 self.env
646 .set_global("PS4", ShellVariable::new("+ ".into()))?;
647 }
648
649 let pwd = self.working_dir.to_string_lossy().to_string();
657 let mut pwd_var = ShellVariable::new(pwd.into());
658 pwd_var.export();
659 self.env.set_global("PWD", pwd_var)?;
660
661 #[cfg(unix)]
663 {
664 let mut uid_var =
665 ShellVariable::new(ShellValue::String(format!("{}", uzers::get_current_uid())));
666 uid_var.treat_as_integer().set_readonly();
667 self.env.set_global("UID", uid_var)?;
668 }
669
670 Ok(())
671 }
672
673 async fn load_config(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
674 let mut params = self.default_exec_params();
675 params.process_group_policy = interp::ProcessGroupPolicy::SameProcessGroup;
676
677 if options.login {
678 if options.no_profile {
680 return Ok(());
681 }
682
683 self.source_if_exists(Path::new("/etc/profile"), ¶ms)
692 .await?;
693 if let Some(home_path) = self.get_home_dir() {
694 if options.sh_mode {
695 self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
696 .await?;
697 } else {
698 if !self
699 .source_if_exists(home_path.join(".bash_profile").as_path(), ¶ms)
700 .await?
701 {
702 if !self
703 .source_if_exists(home_path.join(".bash_login").as_path(), ¶ms)
704 .await?
705 {
706 self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
707 .await?;
708 }
709 }
710 }
711 }
712 } else {
713 if options.interactive {
714 if options.no_rc || options.sh_mode {
716 return Ok(());
717 }
718
719 self.source_if_exists(Path::new("/etc/bash.bashrc"), ¶ms)
726 .await?;
727 if let Some(home_path) = self.get_home_dir() {
728 self.source_if_exists(home_path.join(".bashrc").as_path(), ¶ms)
729 .await?;
730 self.source_if_exists(home_path.join(".brushrc").as_path(), ¶ms)
731 .await?;
732 }
733 } else {
734 let env_var_name = if options.sh_mode { "ENV" } else { "BASH_ENV" };
735
736 if self.env.is_set(env_var_name) {
737 return error::unimp(
741 "load config from $ENV/BASH_ENV for non-interactive, non-login shell",
742 );
743 }
744 }
745 }
746
747 Ok(())
748 }
749
750 async fn source_if_exists(
751 &mut self,
752 path: &Path,
753 params: &ExecutionParameters,
754 ) -> Result<bool, error::Error> {
755 if path.exists() {
756 self.source_script(path, std::iter::empty::<String>(), params)
757 .await?;
758 Ok(true)
759 } else {
760 tracing::debug!("skipping non-existent file: {}", path.display());
761 Ok(false)
762 }
763 }
764
765 pub async fn source_script<S: AsRef<str>, I: Iterator<Item = S>>(
773 &mut self,
774 path: &Path,
775 args: I,
776 params: &ExecutionParameters,
777 ) -> Result<ExecutionResult, error::Error> {
778 self.parse_and_execute_script_file(path, args, params, ScriptCallType::Sourced)
779 .await
780 }
781
782 async fn parse_and_execute_script_file<S: AsRef<str>, I: Iterator<Item = S>>(
791 &mut self,
792 path: &Path,
793 args: I,
794 params: &ExecutionParameters,
795 call_type: ScriptCallType,
796 ) -> Result<ExecutionResult, error::Error> {
797 tracing::debug!("sourcing: {}", path.display());
798 let opened_file: openfiles::OpenFile = self
799 .open_file(path, params)
800 .map_err(|e| error::Error::FailedSourcingFile(path.to_owned(), e.into()))?;
801
802 if opened_file.is_dir() {
803 return Err(error::Error::FailedSourcingFile(
804 path.to_owned(),
805 error::Error::IsADirectory.into(),
806 ));
807 }
808
809 let source_info = brush_parser::SourceInfo {
810 source: path.to_string_lossy().to_string(),
811 };
812
813 self.source_file(opened_file, &source_info, args, params, call_type)
814 .await
815 }
816
817 async fn source_file<F: Read, S: AsRef<str>, I: Iterator<Item = S>>(
827 &mut self,
828 file: F,
829 source_info: &brush_parser::SourceInfo,
830 args: I,
831 params: &ExecutionParameters,
832 call_type: ScriptCallType,
833 ) -> Result<ExecutionResult, error::Error> {
834 let mut reader = std::io::BufReader::new(file);
835 let mut parser =
836 brush_parser::Parser::new(&mut reader, &self.parser_options(), source_info);
837
838 tracing::debug!(target: trace_categories::PARSE, "Parsing sourced file: {}", source_info.source);
839 let parse_result = parser.parse();
840
841 let mut other_positional_parameters = args.map(|s| s.as_ref().to_owned()).collect();
842 let mut other_shell_name = Some(source_info.source.clone());
843
844 std::mem::swap(&mut self.shell_name, &mut other_shell_name);
846 std::mem::swap(
847 &mut self.positional_parameters,
848 &mut other_positional_parameters,
849 );
850
851 self.script_call_stack
852 .push_front((call_type.clone(), source_info.source.clone()));
853
854 let result = self
855 .run_parsed_result(parse_result, source_info, params)
856 .await;
857
858 self.script_call_stack.pop_front();
859
860 std::mem::swap(&mut self.shell_name, &mut other_shell_name);
862 std::mem::swap(
863 &mut self.positional_parameters,
864 &mut other_positional_parameters,
865 );
866
867 result
868 }
869
870 pub async fn invoke_function(&mut self, name: &str, args: &[&str]) -> Result<u8, error::Error> {
877 let params = self.default_exec_params();
879
880 let command_name = String::from(name);
881
882 let func_registration = self
883 .funcs
884 .get(name)
885 .ok_or_else(|| error::Error::FunctionNotFound(name.to_owned()))?;
886
887 let func = func_registration.definition.clone();
888
889 let context = commands::ExecutionContext {
890 shell: self,
891 command_name,
892 params,
893 };
894
895 let command_args = args
896 .iter()
897 .map(|s| commands::CommandArg::String(String::from(*s)))
898 .collect::<Vec<_>>();
899
900 match commands::invoke_shell_function(func, context, &command_args).await? {
901 commands::CommandSpawnResult::SpawnedProcess(_) => {
902 error::unimp("child spawned from function invocation")
903 }
904 commands::CommandSpawnResult::ImmediateExit(code) => Ok(code),
905 commands::CommandSpawnResult::ExitShell(code) => Ok(code),
906 commands::CommandSpawnResult::ReturnFromFunctionOrScript(code) => Ok(code),
907 commands::CommandSpawnResult::BreakLoop(_)
908 | commands::CommandSpawnResult::ContinueLoop(_) => {
909 error::unimp("break or continue returned from function invocation")
910 }
911 }
912 }
913
914 pub async fn run_string<S: Into<String>>(
921 &mut self,
922 command: S,
923 params: &ExecutionParameters,
924 ) -> Result<ExecutionResult, error::Error> {
925 self.current_line_number += 1;
929
930 let parse_result = self.parse_string(command.into());
931 let source_info = brush_parser::SourceInfo {
932 source: String::from("main"),
933 };
934 self.run_parsed_result(parse_result, &source_info, params)
935 .await
936 }
937
938 pub fn parse_string<S: Into<String>>(
945 &self,
946 s: S,
947 ) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
948 parse_string_impl(s.into(), self.parser_options())
949 }
950
951 pub async fn basic_expand_string<S: AsRef<str>>(
957 &mut self,
958 params: &ExecutionParameters,
959 s: S,
960 ) -> Result<String, error::Error> {
961 let result = expansion::basic_expand_str(self, params, s.as_ref()).await?;
962 Ok(result)
963 }
964
965 pub async fn full_expand_and_split_string<S: AsRef<str>>(
972 &mut self,
973 params: &ExecutionParameters,
974 s: S,
975 ) -> Result<Vec<String>, error::Error> {
976 let result = expansion::full_expand_and_split_str(self, params, s.as_ref()).await?;
977 Ok(result)
978 }
979
980 pub fn default_exec_params(&self) -> ExecutionParameters {
982 ExecutionParameters {
983 open_files: self.open_files.clone(),
984 ..Default::default()
985 }
986 }
987
988 pub async fn run_script<S: AsRef<str>, I: Iterator<Item = S>>(
995 &mut self,
996 script_path: &Path,
997 args: I,
998 ) -> Result<ExecutionResult, error::Error> {
999 let params = self.default_exec_params();
1000 self.parse_and_execute_script_file(script_path, args, ¶ms, ScriptCallType::Executed)
1001 .await
1002 }
1003
1004 async fn run_parsed_result(
1005 &mut self,
1006 parse_result: Result<brush_parser::ast::Program, brush_parser::ParseError>,
1007 source_info: &brush_parser::SourceInfo,
1008 params: &ExecutionParameters,
1009 ) -> Result<ExecutionResult, error::Error> {
1010 let mut error_prefix = String::new();
1011
1012 if !source_info.source.is_empty() {
1013 error_prefix = format!("{}: ", source_info.source);
1014 }
1015
1016 let result = match parse_result {
1017 Ok(prog) => match self.run_program(prog, params).await {
1018 Ok(result) => result,
1019 Err(e) => {
1020 tracing::error!("error: {:#}", e);
1021 self.last_exit_status = 1;
1022 ExecutionResult::new(1)
1023 }
1024 },
1025 Err(brush_parser::ParseError::ParsingNearToken(token_near_error)) => {
1026 let error_loc = &token_near_error.location().start;
1027
1028 tracing::error!(
1029 "{}syntax error near token `{}' (line {} col {})",
1030 error_prefix,
1031 token_near_error.to_str(),
1032 error_loc.line,
1033 error_loc.column,
1034 );
1035 self.last_exit_status = 2;
1036 ExecutionResult::new(2)
1037 }
1038 Err(brush_parser::ParseError::ParsingAtEndOfInput) => {
1039 tracing::error!("{}syntax error at end of input", error_prefix);
1040
1041 self.last_exit_status = 2;
1042 ExecutionResult::new(2)
1043 }
1044 Err(brush_parser::ParseError::Tokenizing { inner, position }) => {
1045 let mut error_message = error_prefix.clone();
1046 error_message.push_str(inner.to_string().as_str());
1047
1048 if let Some(position) = position {
1049 write!(
1050 error_message,
1051 " (detected near line {} column {})",
1052 position.line, position.column
1053 )?;
1054 }
1055
1056 tracing::error!("{}", error_message);
1057
1058 self.last_exit_status = 2;
1059 ExecutionResult::new(2)
1060 }
1061 };
1062
1063 Ok(result)
1064 }
1065
1066 pub async fn run_program(
1073 &mut self,
1074 program: brush_parser::ast::Program,
1075 params: &ExecutionParameters,
1076 ) -> Result<ExecutionResult, error::Error> {
1077 program.execute(self, params).await
1078 }
1079
1080 fn default_prompt(&self) -> &'static str {
1081 if self.options.sh_mode {
1082 "$ "
1083 } else {
1084 "brush$ "
1085 }
1086 }
1087
1088 pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
1090 self.expand_prompt_var("PS0", "").await
1091 }
1092
1093 pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
1095 self.expand_prompt_var("PS1", self.default_prompt()).await
1096 }
1097
1098 #[allow(clippy::unused_async)]
1100 pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
1101 self.expand_prompt_var("BRUSH_PS_ALT", "").await
1103 }
1104
1105 pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
1107 self.expand_prompt_var("PS2", "> ").await
1108 }
1109
1110 async fn expand_prompt_var(
1111 &mut self,
1112 var_name: &str,
1113 default: &str,
1114 ) -> Result<String, error::Error> {
1115 let prompt_spec = self.parameter_or_default(var_name, default);
1122 if prompt_spec.is_empty() {
1123 return Ok(String::new());
1124 }
1125
1126 let formatted_prompt = prompt::expand_prompt(self, prompt_spec.into_owned())?;
1128
1129 let params = self.default_exec_params();
1131 expansion::basic_expand_str(self, ¶ms, &formatted_prompt).await
1132 }
1133
1134 pub fn last_result(&self) -> u8 {
1136 self.last_exit_status
1137 }
1138
1139 fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
1140 self.get_env_str(name).unwrap_or(default.into())
1141 }
1142
1143 pub fn parser_options(&self) -> brush_parser::ParserOptions {
1146 brush_parser::ParserOptions {
1147 enable_extended_globbing: self.options.extended_globbing,
1148 posix_mode: self.options.posix_mode,
1149 sh_mode: self.options.sh_mode,
1150 tilde_expansion: true,
1151 }
1152 }
1153
1154 pub(crate) fn in_sourced_script(&self) -> bool {
1156 self.script_call_stack
1157 .front()
1158 .is_some_and(|(ty, _)| matches!(ty, ScriptCallType::Sourced))
1159 }
1160
1161 pub(crate) fn in_function(&self) -> bool {
1163 !self.function_call_stack.is_empty()
1164 }
1165
1166 pub(crate) fn enter_function(
1174 &mut self,
1175 name: &str,
1176 function_def: &Arc<brush_parser::ast::FunctionDefinition>,
1177 ) -> Result<(), error::Error> {
1178 if let Some(max_call_depth) = self.options.max_function_call_depth {
1179 if self.function_call_stack.len() >= max_call_depth {
1180 return Err(error::Error::MaxFunctionCallDepthExceeded);
1181 }
1182 }
1183
1184 if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
1185 let depth = self.function_call_stack.len();
1186 let prefix = repeated_char_str(' ', depth);
1187 tracing::debug!(target: trace_categories::FUNCTIONS, "Entering func [depth={depth}]: {prefix}{name}");
1188 }
1189
1190 self.function_call_stack.push_front(FunctionCall {
1191 function_name: name.to_owned(),
1192 function_definition: function_def.clone(),
1193 });
1194 self.env.push_scope(env::EnvironmentScope::Local);
1195 Ok(())
1196 }
1197
1198 pub(crate) fn leave_function(&mut self) -> Result<(), error::Error> {
1201 self.env.pop_scope(env::EnvironmentScope::Local)?;
1202
1203 if let Some(exited_call) = self.function_call_stack.pop_front() {
1204 if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
1205 let depth = self.function_call_stack.len();
1206 let prefix = repeated_char_str(' ', depth);
1207 tracing::debug!(target: trace_categories::FUNCTIONS, "Exiting func [depth={depth}]: {prefix}{}", exited_call.function_name);
1208 }
1209 }
1210
1211 Ok(())
1212 }
1213
1214 fn get_funcname_value(&self) -> variables::ShellValue {
1215 if self.function_call_stack.is_empty() {
1216 ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray)
1217 } else {
1218 self.function_call_stack
1219 .iter()
1220 .map(|s| s.function_name.as_str())
1221 .collect::<Vec<_>>()
1222 .into()
1223 }
1224 }
1225
1226 fn get_bash_source_value(&self) -> variables::ShellValue {
1227 if self.function_call_stack.is_empty() {
1228 self.script_call_stack
1229 .front()
1230 .map_or_else(Vec::new, |(_call_type, s)| vec![s.as_ref()])
1231 .into()
1232 } else {
1233 self.function_call_stack
1234 .iter()
1235 .map(|s| s.function_definition.source.as_ref())
1236 .collect::<Vec<_>>()
1237 .into()
1238 }
1239 }
1240
1241 pub fn get_history_file_path(&self) -> Option<PathBuf> {
1243 self.get_env_str("HISTFILE")
1244 .map(|s| PathBuf::from(s.into_owned()))
1245 }
1246
1247 pub(crate) fn get_current_input_line_number(&self) -> u32 {
1249 self.current_line_number
1250 }
1251
1252 pub fn get_env_str(&self, name: &str) -> Option<Cow<'_, str>> {
1259 self.env.get_str(name, self)
1260 }
1261
1262 pub fn get_env_var(&self, name: &str) -> Option<&ShellVariable> {
1268 self.env.get(name).map(|(_, var)| var)
1269 }
1270
1271 pub(crate) fn get_ifs(&self) -> Cow<'_, str> {
1273 self.get_env_str("IFS").unwrap_or_else(|| " \t\n".into())
1274 }
1275
1276 pub(crate) fn get_ifs_first_char(&self) -> char {
1278 self.get_ifs().chars().next().unwrap_or(' ')
1279 }
1280
1281 pub async fn get_completions(
1288 &mut self,
1289 input: &str,
1290 position: usize,
1291 ) -> Result<completion::Completions, error::Error> {
1292 let completion_config = self.completion_config.clone();
1293 completion_config
1294 .get_completions(self, input, position)
1295 .await
1296 }
1297
1298 pub fn find_executables_in_path(&self, required_glob_pattern: &str) -> Vec<PathBuf> {
1304 self.find_executables_in(
1305 self.env
1306 .get_str("PATH", self)
1307 .unwrap_or_default()
1308 .split(':'),
1309 required_glob_pattern,
1310 )
1311 }
1312
1313 #[allow(clippy::manual_flatten)]
1320 pub fn find_executables_in<T: AsRef<str>>(
1321 &self,
1322 paths: impl Iterator<Item = T>,
1323 required_glob_pattern: &str,
1324 ) -> Vec<PathBuf> {
1325 let is_executable = |path: &Path| path.is_file() && path.executable();
1326
1327 let mut executables = vec![];
1328 for dir_str in paths {
1329 let dir_str = dir_str.as_ref();
1330 let pattern =
1331 patterns::Pattern::from(std::format!("{dir_str}/{required_glob_pattern}"))
1332 .set_extended_globbing(self.options.extended_globbing)
1333 .set_case_insensitive(self.options.case_insensitive_pathname_expansion);
1334
1335 if let Ok(entries) = pattern.expand(
1337 &self.working_dir,
1338 Some(&is_executable),
1339 &patterns::FilenameExpansionOptions::default(),
1340 ) {
1341 for entry in entries {
1342 executables.push(PathBuf::from(entry));
1343 }
1344 }
1345 }
1346
1347 executables
1348 }
1349
1350 pub fn find_first_executable_in_path<S: AsRef<str>>(
1357 &self,
1358 candidate_name: S,
1359 ) -> Option<PathBuf> {
1360 for dir_str in self.get_env_str("PATH").unwrap_or_default().split(':') {
1361 let candidate_path = Path::new(dir_str).join(candidate_name.as_ref());
1362 if candidate_path.executable() {
1363 return Some(candidate_path);
1364 }
1365 }
1366 None
1367 }
1368
1369 pub fn find_first_executable_in_path_using_cache<S: AsRef<str>>(
1377 &mut self,
1378 candidate_name: S,
1379 ) -> Option<PathBuf> {
1380 if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
1381 Some(cached_path)
1382 } else if let Some(found_path) = self.find_first_executable_in_path(&candidate_name) {
1383 self.program_location_cache
1384 .set(&candidate_name, found_path.clone());
1385 Some(found_path)
1386 } else {
1387 None
1388 }
1389 }
1390
1391 pub fn get_absolute_path(&self, path: &Path) -> PathBuf {
1397 if path.as_os_str().is_empty() || path.is_absolute() {
1398 path.to_owned()
1399 } else {
1400 self.working_dir.join(path)
1401 }
1402 }
1403
1404 pub(crate) fn open_file(
1411 &self,
1412 path: &Path,
1413 params: &ExecutionParameters,
1414 ) -> Result<openfiles::OpenFile, error::Error> {
1415 let path_to_open = self.get_absolute_path(path);
1416
1417 if let Some(parent) = path_to_open.parent() {
1421 if parent == Path::new("/dev/fd") {
1422 if let Some(filename) = path_to_open.file_name() {
1423 if let Ok(fd_num) = filename.to_string_lossy().to_string().parse::<u32>() {
1424 if let Some(open_file) = params.open_files.files.get(&fd_num) {
1425 return open_file.try_dup();
1426 }
1427 }
1428 }
1429 }
1430 }
1431
1432 Ok(std::fs::File::open(path_to_open)?.into())
1433 }
1434
1435 pub(crate) fn replace_open_files(&mut self, open_files: openfiles::OpenFiles) {
1441 self.open_files = open_files;
1442 }
1443
1444 pub fn set_working_dir(&mut self, target_dir: &Path) -> Result<(), error::Error> {
1450 let abs_path = self.get_absolute_path(target_dir);
1451
1452 match std::fs::metadata(&abs_path) {
1453 Ok(m) => {
1454 if !m.is_dir() {
1455 return Err(error::Error::NotADirectory(abs_path));
1456 }
1457 }
1458 Err(e) => {
1459 return Err(e.into());
1460 }
1461 }
1462
1463 let cleaned_path = abs_path.canonicalize()?;
1465
1466 let pwd = cleaned_path.to_string_lossy().to_string();
1467
1468 self.env.update_or_add(
1469 "PWD",
1470 variables::ShellValueLiteral::Scalar(pwd),
1471 |var| {
1472 var.export();
1473 Ok(())
1474 },
1475 EnvironmentLookup::Anywhere,
1476 EnvironmentScope::Global,
1477 )?;
1478 let oldpwd = std::mem::replace(&mut self.working_dir, cleaned_path);
1479
1480 self.env.update_or_add(
1481 "OLDPWD",
1482 variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
1483 |var| {
1484 var.export();
1485 Ok(())
1486 },
1487 EnvironmentLookup::Anywhere,
1488 EnvironmentScope::Global,
1489 )?;
1490
1491 Ok(())
1492 }
1493
1494 pub(crate) fn tilde_shorten(&self, s: String) -> String {
1500 if let Some(home_dir) = self.get_home_dir() {
1501 if let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref()) {
1502 return format!("~{stripped}");
1503 }
1504 }
1505 s
1506 }
1507
1508 pub(crate) fn get_home_dir(&self) -> Option<PathBuf> {
1510 Self::get_home_dir_with_env(&self.env, self)
1511 }
1512
1513 fn get_home_dir_with_env(env: &ShellEnvironment, shell: &Shell) -> Option<PathBuf> {
1514 if let Some(home) = env.get_str("HOME", shell) {
1515 Some(PathBuf::from(home.to_string()))
1516 } else {
1517 users::get_current_user_home_dir()
1519 }
1520 }
1521
1522 pub fn stdout(&self) -> openfiles::OpenFile {
1525 self.open_files.files.get(&1).unwrap().try_dup().unwrap()
1526 }
1527
1528 pub fn stderr(&self) -> openfiles::OpenFile {
1531 self.open_files.files.get(&2).unwrap().try_dup().unwrap()
1532 }
1533
1534 pub(crate) async fn trace_command<S: AsRef<str>>(
1540 &mut self,
1541 command: S,
1542 ) -> Result<(), error::Error> {
1543 let ps4 = self.as_mut().expand_prompt_var("PS4", "").await?;
1544
1545 let mut prefix = ps4.to_string();
1546
1547 let additional_depth = self.script_call_stack.len() + self.depth;
1548 if let Some(c) = prefix.chars().next() {
1549 for _ in 0..additional_depth {
1550 prefix.insert(0, c);
1551 }
1552 }
1553
1554 writeln!(self.stderr(), "{prefix}{}", command.as_ref())?;
1555 Ok(())
1556 }
1557
1558 pub(crate) fn get_keywords(&self) -> Vec<String> {
1560 if self.options.sh_mode {
1561 keywords::SH_MODE_KEYWORDS.iter().cloned().collect()
1562 } else {
1563 keywords::KEYWORDS.iter().cloned().collect()
1564 }
1565 }
1566
1567 pub fn is_keyword(&self, s: &str) -> bool {
1573 if self.options.sh_mode {
1574 keywords::SH_MODE_KEYWORDS.contains(s)
1575 } else {
1576 keywords::KEYWORDS.contains(s)
1577 }
1578 }
1579
1580 pub fn check_for_completed_jobs(&mut self) -> Result<(), error::Error> {
1582 let results = self.jobs.poll()?;
1583
1584 if self.options.enable_job_control {
1585 for (job, _result) in results {
1586 writeln!(self.stderr(), "{job}")?;
1587 }
1588 }
1589
1590 Ok(())
1591 }
1592
1593 pub fn eval_arithmetic(
1595 &mut self,
1596 expr: &brush_parser::ast::ArithmeticExpr,
1597 ) -> Result<i64, error::Error> {
1598 Ok(expr.eval(self)?)
1599 }
1600}
1601
1602#[cached::proc_macro::cached(size = 64, result = true)]
1603fn parse_string_impl(
1604 s: String,
1605 parser_options: brush_parser::ParserOptions,
1606) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
1607 let mut reader = std::io::BufReader::new(s.as_bytes());
1608 let source_info = brush_parser::SourceInfo {
1609 source: String::from("main"),
1610 };
1611 let mut parser: brush_parser::Parser<&mut std::io::BufReader<&[u8]>> =
1612 brush_parser::Parser::new(&mut reader, &parser_options, &source_info);
1613
1614 tracing::debug!(target: trace_categories::PARSE, "Parsing string as program...");
1615 parser.parse()
1616}
1617
1618fn repeated_char_str(c: char, count: usize) -> String {
1619 (0..count).map(|_| c).collect()
1620}
1621
1622fn get_random_value(_shell: &Shell) -> ShellValue {
1623 let mut rng = rand::rng();
1624 let num = rng.random_range(0..32768);
1625 let str = num.to_string();
1626 str.into()
1627}
1628
1629fn get_srandom_value(_shell: &Shell) -> ShellValue {
1630 let mut rng = rand::rng();
1631 let num: u32 = rng.random();
1632 let str = num.to_string();
1633 str.into()
1634}