brush_core/
shell.rs

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 normalize_path::NormalizePath;
9use rand::Rng;
10use tokio::sync::Mutex;
11
12use crate::arithmetic::Evaluatable;
13use crate::env::{EnvironmentLookup, EnvironmentScope, ShellEnvironment};
14use crate::interp::{self, Execute, ExecutionParameters, ExecutionResult};
15use crate::options::RuntimeOptions;
16use crate::sys::fs::PathExt;
17use crate::variables::{self, ShellValue, ShellVariable};
18use crate::{
19    builtins, commands, completion, env, error, expansion, functions, jobs, keywords, openfiles,
20    prompt, sys::users, traps,
21};
22use crate::{interfaces, pathcache, pathsearch, sys, trace_categories};
23
24const BASH_MAJOR: u32 = 5;
25const BASH_MINOR: u32 = 2;
26const BASH_PATCH: u32 = 15;
27const BASH_BUILD: u32 = 1;
28const BASH_RELEASE: &str = "release";
29const BASH_MACHINE: &str = "unknown";
30
31/// Type for storing a key bindings helper.
32pub type KeyBindingsHelper = Arc<Mutex<dyn interfaces::KeyBindings>>;
33
34/// Represents an instance of a shell.
35pub struct Shell {
36    //
37    // Core state required by specification
38    /// Trap handler configuration for the shell.
39    pub traps: traps::TrapHandlerConfig,
40    /// Manages files opened and accessible via redirection operators.
41    open_files: openfiles::OpenFiles,
42    /// The current working directory.
43    pub working_dir: PathBuf,
44    /// The shell environment, containing shell variables.
45    pub env: ShellEnvironment,
46    /// Shell function definitions.
47    pub funcs: functions::FunctionEnv,
48    /// Runtime shell options.
49    pub options: RuntimeOptions,
50    /// State of managed jobs.
51    pub(crate) jobs: jobs::JobManager,
52    /// Shell aliases.
53    pub aliases: HashMap<String, String>,
54
55    //
56    // Additional state
57    /// The status of the last completed command.
58    pub last_exit_status: u8,
59
60    /// The status of each of the commands in the last pipeline.
61    pub last_pipeline_statuses: Vec<u8>,
62
63    /// Clone depth from the original ancestor shell.
64    depth: usize,
65
66    /// Shell name (a.k.a. $0)
67    pub shell_name: Option<String>,
68
69    /// Positional parameters stack ($1 and beyond)
70    pub positional_parameters: Vec<String>,
71
72    /// Detailed display string for the shell
73    pub shell_product_display_str: Option<String>,
74
75    /// Script call stack.
76    script_call_stack: VecDeque<(ScriptCallType, String)>,
77
78    /// Function call stack.
79    function_call_stack: VecDeque<FunctionCall>,
80
81    /// Directory stack used by pushd et al.
82    pub directory_stack: Vec<PathBuf>,
83
84    /// Current line number being processed.
85    current_line_number: u32,
86
87    /// Completion configuration.
88    pub completion_config: completion::Config,
89
90    /// Shell built-in commands.
91    pub builtins: HashMap<String, builtins::Registration>,
92
93    /// Shell program location cache.
94    pub(crate) program_location_cache: pathcache::PathCache,
95
96    /// Last "SECONDS" captured time.
97    last_stopwatch_time: std::time::SystemTime,
98
99    /// Last "SECONDS" offset requested.
100    last_stopwatch_offset: u32,
101
102    /// Key bindings for the shell, optionally implemented by an interactive shell.
103    pub key_bindings: Option<KeyBindingsHelper>,
104}
105
106impl Clone for Shell {
107    fn clone(&self) -> Self {
108        Self {
109            traps: self.traps.clone(),
110            open_files: self.open_files.clone(),
111            working_dir: self.working_dir.clone(),
112            env: self.env.clone(),
113            funcs: self.funcs.clone(),
114            options: self.options.clone(),
115            jobs: jobs::JobManager::new(),
116            aliases: self.aliases.clone(),
117            last_exit_status: self.last_exit_status,
118            last_pipeline_statuses: self.last_pipeline_statuses.clone(),
119            positional_parameters: self.positional_parameters.clone(),
120            shell_name: self.shell_name.clone(),
121            shell_product_display_str: self.shell_product_display_str.clone(),
122            function_call_stack: self.function_call_stack.clone(),
123            script_call_stack: self.script_call_stack.clone(),
124            directory_stack: self.directory_stack.clone(),
125            current_line_number: self.current_line_number,
126            completion_config: self.completion_config.clone(),
127            builtins: self.builtins.clone(),
128            program_location_cache: self.program_location_cache.clone(),
129            last_stopwatch_time: self.last_stopwatch_time,
130            last_stopwatch_offset: self.last_stopwatch_offset,
131            key_bindings: self.key_bindings.clone(),
132            depth: self.depth + 1,
133        }
134    }
135}
136
137impl AsRef<Self> for Shell {
138    fn as_ref(&self) -> &Self {
139        self
140    }
141}
142
143impl AsMut<Self> for Shell {
144    fn as_mut(&mut self) -> &mut Self {
145        self
146    }
147}
148
149/// Options for creating a new shell.
150#[derive(Default)]
151pub struct CreateOptions {
152    /// Disabled shopt options.
153    pub disabled_shopt_options: Vec<String>,
154    /// Enabled shopt options.
155    pub enabled_shopt_options: Vec<String>,
156    /// Disallow overwriting regular files via output redirection.
157    pub disallow_overwriting_regular_files_via_output_redirection: bool,
158    /// Do not execute commands.
159    pub do_not_execute_commands: bool,
160    /// Exit after one command.
161    pub exit_after_one_command: bool,
162    /// Whether the shell is interactive.
163    pub interactive: bool,
164    /// Whether the shell is a login shell.
165    pub login: bool,
166    /// Whether to skip using a readline-like interface for input.
167    pub no_editing: bool,
168    /// Whether to skip sourcing the system profile.
169    pub no_profile: bool,
170    /// Whether to skip sourcing the user's rc file.
171    pub no_rc: bool,
172    /// Explicit override of rc file to load in interactive mode.
173    pub rc_file: Option<PathBuf>,
174    /// Whether to skip inheriting environment variables from the calling process.
175    pub do_not_inherit_env: bool,
176    /// Whether the shell is in POSIX compliance mode.
177    pub posix: bool,
178    /// Whether to print commands and arguments as they are read.
179    pub print_commands_and_arguments: bool,
180    /// Whether commands are being read from stdin.
181    pub read_commands_from_stdin: bool,
182    /// The name of the shell.
183    pub shell_name: Option<String>,
184    /// Optionally provides a display string describing the version and variant of the shell.
185    pub shell_product_display_str: Option<String>,
186    /// Whether to run in maximal POSIX sh compatibility mode.
187    pub sh_mode: bool,
188    /// Whether to print verbose output.
189    pub verbose: bool,
190    /// Maximum function call depth.
191    pub max_function_call_depth: Option<usize>,
192    /// Key bindings helper for the shell to use.
193    pub key_bindings: Option<KeyBindingsHelper>,
194    /// Brush implementation version.
195    pub shell_version: Option<String>,
196}
197
198/// Represents an executing script.
199#[derive(Clone, Debug)]
200pub enum ScriptCallType {
201    /// The script was sourced.
202    Sourced,
203    /// The script was executed.
204    Executed,
205}
206
207/// Represents an active shell function call.
208#[derive(Clone, Debug)]
209pub struct FunctionCall {
210    /// The name of the function invoked.
211    function_name: String,
212    /// The definition of the invoked function.
213    function_definition: Arc<brush_parser::ast::FunctionDefinition>,
214}
215
216impl Shell {
217    /// Returns a new shell instance created with the given options.
218    ///
219    /// # Arguments
220    ///
221    /// * `options` - The options to use when creating the shell.
222    pub async fn new(options: &CreateOptions) -> Result<Self, error::Error> {
223        // Instantiate the shell with some defaults.
224        let mut shell = Self {
225            traps: traps::TrapHandlerConfig::default(),
226            open_files: openfiles::OpenFiles::default(),
227            working_dir: std::env::current_dir()?,
228            env: env::ShellEnvironment::new(),
229            funcs: functions::FunctionEnv::default(),
230            options: RuntimeOptions::defaults_from(options),
231            jobs: jobs::JobManager::new(),
232            aliases: HashMap::default(),
233            last_exit_status: 0,
234            last_pipeline_statuses: vec![0],
235            positional_parameters: vec![],
236            shell_name: options.shell_name.clone(),
237            shell_product_display_str: options.shell_product_display_str.clone(),
238            function_call_stack: VecDeque::new(),
239            script_call_stack: VecDeque::new(),
240            directory_stack: vec![],
241            current_line_number: 0,
242            completion_config: completion::Config::default(),
243            builtins: builtins::get_default_builtins(options),
244            program_location_cache: pathcache::PathCache::default(),
245            last_stopwatch_time: std::time::SystemTime::now(),
246            last_stopwatch_offset: 0,
247            key_bindings: options.key_bindings.clone(),
248            depth: 0,
249        };
250
251        // TODO: Without this a script that sets extglob will fail because we
252        // parse the entire script with the same settings.
253        shell.options.extended_globbing = true;
254
255        // Initialize environment.
256        shell.initialize_vars(options)?;
257
258        // Load profiles/configuration.
259        shell.load_config(options).await?;
260
261        Ok(shell)
262    }
263
264    fn initialize_exported_func(
265        &mut self,
266        func_name: &str,
267        body_text: &str,
268    ) -> Result<(), error::Error> {
269        let mut parser = create_parser(body_text.as_bytes(), &self.parser_options());
270        let func_body = parser.parse_function_parens_and_body()?;
271
272        let func_def = brush_parser::ast::FunctionDefinition {
273            fname: func_name.to_owned(),
274            body: func_body,
275            source: String::new(),
276        };
277
278        let mut registration = functions::FunctionRegistration::from(func_def);
279        registration.export();
280
281        self.funcs.update(func_name.to_owned(), registration);
282
283        Ok(())
284    }
285
286    #[allow(clippy::too_many_lines)]
287    #[allow(clippy::unwrap_in_result)]
288    fn initialize_vars(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
289        // Seed parameters from environment (unless requested not to do so).
290        if !options.do_not_inherit_env {
291            for (k, v) in std::env::vars() {
292                // See if it's a function exported by an ancestor process.
293                if let Some(func_name) = k.strip_prefix("BASH_FUNC_") {
294                    if let Some(func_name) = func_name.strip_suffix("%%") {
295                        // Intentionally best-effort; don't fail out of the shell if we can't
296                        // parse an incoming function.
297                        let _ = self.initialize_exported_func(func_name, v.as_str());
298                        continue;
299                    }
300                }
301
302                let mut var = ShellVariable::new(ShellValue::String(v));
303                var.export();
304                self.env.set_global(k, var)?;
305            }
306        }
307        let shell_version = options.shell_version.clone();
308        self.env.set_global(
309            "BRUSH_VERSION",
310            ShellVariable::new(shell_version.unwrap_or_default().into()),
311        )?;
312
313        // TODO(#479): implement $_
314
315        // BASH
316        if let Some(shell_name) = &options.shell_name {
317            self.env
318                .set_global("BASH", ShellVariable::new(shell_name.into()))?;
319        }
320
321        // BASHOPTS
322        let mut bashopts_var = ShellVariable::new(ShellValue::Dynamic {
323            getter: |shell| shell.options.get_shopt_optstr().into(),
324            setter: |_| (),
325        });
326        bashopts_var.set_readonly();
327        self.env.set_global("BASHOPTS", bashopts_var)?;
328
329        // BASHPID
330        #[cfg(not(target_family = "wasm"))]
331        {
332            let mut bashpid_var =
333                ShellVariable::new(ShellValue::String(std::process::id().to_string()));
334            bashpid_var.treat_as_integer();
335            self.env.set_global("BASHPID", bashpid_var)?;
336        }
337
338        // BASH_ALIASES
339        self.env.set_global(
340            "BASH_ALIASES",
341            ShellVariable::new(ShellValue::Dynamic {
342                getter: |shell| {
343                    let values = variables::ArrayLiteral(
344                        shell
345                            .aliases
346                            .iter()
347                            .map(|(k, v)| (Some(k.to_owned()), v.to_owned()))
348                            .collect::<Vec<_>>(),
349                    );
350
351                    ShellValue::associative_array_from_literals(values).unwrap()
352                },
353                setter: |_| (),
354            }),
355        )?;
356
357        // TODO(vars): when extdebug is enabled, BASH_ARGC and BASH_ARGV are set to valid values
358        // TODO(vars): implement BASH_ARGC
359        // TODO(vars): implement BASH_ARGV
360
361        // BASH_ARGV0
362        self.env.set_global(
363            "BASH_ARGV0",
364            ShellVariable::new(ShellValue::Dynamic {
365                getter: |shell| {
366                    let argv0 = shell.shell_name.as_deref().unwrap_or_default();
367                    argv0.to_string().into()
368                },
369                // TODO(vars): implement updating BASH_ARGV0
370                setter: |_| (),
371            }),
372        )?;
373
374        // TODO(vars): implement mutation of BASH_CMDS
375        self.env.set_global(
376            "BASH_CMDS",
377            ShellVariable::new(ShellValue::Dynamic {
378                getter: |shell| shell.program_location_cache.to_value().unwrap(),
379                setter: |_| (),
380            }),
381        )?;
382
383        // TODO(vars): implement BASH_COMMAND
384        // TODO(vars): implement BASH_EXECUTIION_STRING
385        // TODO(vars): implement BASH_LINENO
386
387        // BASH_SOURCE
388        self.env.set_global(
389            "BASH_SOURCE",
390            ShellVariable::new(ShellValue::Dynamic {
391                getter: |shell| shell.get_bash_source_value(),
392                setter: |_| (),
393            }),
394        )?;
395
396        // BASH_SUBSHELL
397        self.env.set_global(
398            "BASH_SUBSHELL",
399            ShellVariable::new(ShellValue::Dynamic {
400                getter: |shell| shell.depth.to_string().into(),
401                setter: |_| (),
402            }),
403        )?;
404
405        // BASH_VERSINFO
406        let mut bash_versinfo_var = ShellVariable::new(ShellValue::indexed_array_from_strs(
407            [
408                BASH_MAJOR.to_string().as_str(),
409                BASH_MINOR.to_string().as_str(),
410                BASH_PATCH.to_string().as_str(),
411                BASH_BUILD.to_string().as_str(),
412                BASH_RELEASE,
413                BASH_MACHINE,
414            ]
415            .as_slice(),
416        ));
417        bash_versinfo_var.set_readonly();
418        self.env.set_global("BASH_VERSINFO", bash_versinfo_var)?;
419
420        // BASH_VERSION
421        // This is the Bash interface version. See BRUSH_VERSION for its implementation version.
422        self.env.set_global(
423            "BASH_VERSION",
424            ShellVariable::new(
425                std::format!("{BASH_MAJOR}.{BASH_MINOR}.{BASH_PATCH}({BASH_BUILD})-{BASH_RELEASE}")
426                    .into(),
427            ),
428        )?;
429
430        // COMP_WORDBREAKS
431        self.env.set_global(
432            "COMP_WORDBREAKS",
433            ShellVariable::new(" \t\n\"\'@><=;|&(:".into()),
434        )?;
435
436        // DIRSTACK
437        self.env.set_global(
438            "DIRSTACK",
439            ShellVariable::new(ShellValue::Dynamic {
440                getter: |shell| {
441                    shell
442                        .directory_stack
443                        .iter()
444                        .map(|p| p.to_string_lossy().to_string())
445                        .collect::<Vec<_>>()
446                        .into()
447                },
448                setter: |_| (),
449            }),
450        )?;
451
452        // EPOCHREALTIME
453        self.env.set_global(
454            "EPOCHREALTIME",
455            ShellVariable::new(ShellValue::Dynamic {
456                getter: |_shell| {
457                    let now = std::time::SystemTime::now();
458                    let since_epoch = now
459                        .duration_since(std::time::UNIX_EPOCH)
460                        .unwrap_or_default();
461                    since_epoch.as_secs_f64().to_string().into()
462                },
463                setter: |_| (),
464            }),
465        )?;
466
467        // EPOCHSECONDS
468        self.env.set_global(
469            "EPOCHSECONDS",
470            ShellVariable::new(ShellValue::Dynamic {
471                getter: |_shell| {
472                    let now = std::time::SystemTime::now();
473                    let since_epoch = now
474                        .duration_since(std::time::UNIX_EPOCH)
475                        .unwrap_or_default();
476                    since_epoch.as_secs().to_string().into()
477                },
478                setter: |_| (),
479            }),
480        )?;
481
482        // EUID
483        #[cfg(unix)]
484        {
485            let mut euid_var = ShellVariable::new(ShellValue::String(format!(
486                "{}",
487                uzers::get_effective_uid()
488            )));
489            euid_var.treat_as_integer().set_readonly();
490            self.env.set_global("EUID", euid_var)?;
491        }
492
493        // FUNCNAME
494        self.env.set_global(
495            "FUNCNAME",
496            ShellVariable::new(ShellValue::Dynamic {
497                getter: |shell| shell.get_funcname_value(),
498                setter: |_| (),
499            }),
500        )?;
501
502        // GROUPS
503        // N.B. We could compute this up front, but we choose to make it dynamic so that we
504        // don't have to make costly system calls if the user never accesses it.
505        self.env.set_global(
506            "GROUPS",
507            ShellVariable::new(ShellValue::Dynamic {
508                getter: |_shell| {
509                    let groups = get_current_user_gids();
510                    ShellValue::indexed_array_from_strings(
511                        groups.into_iter().map(|gid| gid.to_string()),
512                    )
513                },
514                setter: |_| (),
515            }),
516        )?;
517
518        // TODO(vars): implement HISTCMD
519
520        // HISTFILE (if not already set)
521        if !self.env.is_set("HISTFILE") {
522            if let Some(home_dir) = self.get_home_dir() {
523                let histfile = home_dir.join(".brush_history");
524                self.env.set_global(
525                    "HISTFILE",
526                    ShellVariable::new(ShellValue::String(histfile.to_string_lossy().to_string())),
527                )?;
528            }
529        }
530
531        // HOSTNAME
532        self.env.set_global(
533            "HOSTNAME",
534            ShellVariable::new(
535                sys::network::get_hostname()
536                    .unwrap_or_default()
537                    .to_string_lossy()
538                    .to_string()
539                    .into(),
540            ),
541        )?;
542
543        // HOSTTYPE
544        #[cfg(unix)]
545        {
546            if let Ok(info) = nix::sys::utsname::uname() {
547                self.env.set_global(
548                    "HOSTTYPE",
549                    ShellVariable::new(info.machine().to_string_lossy().to_string().into()),
550                )?;
551            }
552        }
553
554        // IFS
555        self.env
556            .set_global("IFS", ShellVariable::new(" \t\n".into()))?;
557
558        // LINENO
559        self.env.set_global(
560            "LINENO",
561            ShellVariable::new(ShellValue::Dynamic {
562                getter: |shell| shell.current_line_number.to_string().into(),
563                setter: |_| (),
564            }),
565        )?;
566
567        // MACHTYPE
568        self.env
569            .set_global("MACHTYPE", ShellVariable::new(BASH_MACHINE.into()))?;
570
571        // OLDPWD (initialization)
572        if !self.env.is_set("OLDPWD") {
573            let mut oldpwd_var =
574                ShellVariable::new(ShellValue::Unset(variables::ShellValueUnsetType::Untyped));
575            oldpwd_var.export();
576            self.env.set_global("OLDPWD", oldpwd_var)?;
577        }
578
579        // OPTERR
580        self.env
581            .set_global("OPTERR", ShellVariable::new("1".into()))?;
582
583        // OPTIND
584        let mut optind_var = ShellVariable::new("1".into());
585        optind_var.treat_as_integer();
586        self.env.set_global("OPTIND", optind_var)?;
587
588        // OSTYPE
589        let os_type = match std::env::consts::OS {
590            "linux" => "linux-gnu",
591            "windows" => "windows",
592            _ => "unknown",
593        };
594        self.env
595            .set_global("OSTYPE", ShellVariable::new(os_type.into()))?;
596
597        // PATH (if not already set)
598        #[cfg(unix)]
599        if !self.env.is_set("PATH") {
600            let default_path_str = sys::fs::get_default_executable_search_paths().join(":");
601            self.env
602                .set_global("PATH", ShellVariable::new(default_path_str.into()))?;
603        }
604
605        // PIPESTATUS
606        // TODO: Investigate what happens if this gets unset.
607        // TODO: Investigate if this needs to be saved/preserved across prompt display.
608        self.env.set_global(
609            "PIPESTATUS",
610            ShellVariable::new(ShellValue::Dynamic {
611                getter: |shell| {
612                    ShellValue::indexed_array_from_strings(
613                        shell.last_pipeline_statuses.iter().map(|s| s.to_string()),
614                    )
615                },
616                setter: |_| (),
617            }),
618        )?;
619
620        // PPID
621        if let Some(ppid) = sys::terminal::get_parent_process_id() {
622            let mut ppid_var = ShellVariable::new(ppid.to_string().into());
623            ppid_var.treat_as_integer().set_readonly();
624            self.env.set_global("PPID", ppid_var)?;
625        }
626
627        // RANDOM
628        let mut random_var = ShellVariable::new(ShellValue::Dynamic {
629            getter: get_random_value,
630            setter: |_| (),
631        });
632        random_var.treat_as_integer();
633        self.env.set_global("RANDOM", random_var)?;
634
635        // SECONDS
636        self.env.set_global(
637            "SECONDS",
638            ShellVariable::new(ShellValue::Dynamic {
639                getter: |shell| {
640                    let now = std::time::SystemTime::now();
641                    let since_last = now
642                        .duration_since(shell.last_stopwatch_time)
643                        .unwrap_or_default();
644                    let total_seconds =
645                        since_last.as_secs() + u64::from(shell.last_stopwatch_offset);
646                    total_seconds.to_string().into()
647                },
648                // TODO(vars): implement updating SECONDS
649                setter: |_| (),
650            }),
651        )?;
652
653        // SHELL
654        if let Ok(exe_path) = std::env::current_exe() {
655            self.env.set_global(
656                "SHELL",
657                ShellVariable::new(exe_path.to_string_lossy().to_string().into()),
658            )?;
659        }
660
661        // SHELLOPTS
662        let mut shellopts_var = ShellVariable::new(ShellValue::Dynamic {
663            getter: |shell| shell.options.get_set_o_optstr().into(),
664            setter: |_| (),
665        });
666        shellopts_var.set_readonly();
667        self.env.set_global("SHELLOPTS", shellopts_var)?;
668
669        // SHLVL
670        let input_shlvl = self.get_env_str("SHLVL").unwrap_or_else(|| "0".into());
671        let updated_shlvl = input_shlvl.as_ref().parse::<u32>().unwrap_or(0) + 1;
672        let mut shlvl_var = ShellVariable::new(updated_shlvl.to_string().into());
673        shlvl_var.export();
674        self.env.set_global("SHLVL", shlvl_var)?;
675
676        // SRANDOM
677        let mut random_var = ShellVariable::new(ShellValue::Dynamic {
678            getter: get_srandom_value,
679            setter: |_| (),
680        });
681        random_var.treat_as_integer();
682        self.env.set_global("SRANDOM", random_var)?;
683
684        // PS1 / PS2
685        if options.interactive {
686            if !self.env.is_set("PS1") {
687                self.env
688                    .set_global("PS1", ShellVariable::new(r"\s-\v\$ ".into()))?;
689            }
690
691            if !self.env.is_set("PS2") {
692                self.env
693                    .set_global("PS2", ShellVariable::new("> ".into()))?;
694            }
695        }
696
697        // PS4
698        if !self.env.is_set("PS4") {
699            self.env
700                .set_global("PS4", ShellVariable::new("+ ".into()))?;
701        }
702
703        //
704        // PWD
705        //
706        // Reflect our actual working directory. There's a chance
707        // we inherited an out-of-sync version of the variable. Future updates
708        // will be handled by set_working_dir().
709        //
710        let pwd = self.working_dir.to_string_lossy().to_string();
711        let mut pwd_var = ShellVariable::new(pwd.into());
712        pwd_var.export();
713        self.env.set_global("PWD", pwd_var)?;
714
715        // UID
716        #[cfg(unix)]
717        {
718            let mut uid_var =
719                ShellVariable::new(ShellValue::String(format!("{}", uzers::get_current_uid())));
720            uid_var.treat_as_integer().set_readonly();
721            self.env.set_global("UID", uid_var)?;
722        }
723
724        Ok(())
725    }
726
727    async fn load_config(&mut self, options: &CreateOptions) -> Result<(), error::Error> {
728        let mut params = self.default_exec_params();
729        params.process_group_policy = interp::ProcessGroupPolicy::SameProcessGroup;
730
731        if options.login {
732            // --noprofile means skip this.
733            if options.no_profile {
734                return Ok(());
735            }
736
737            //
738            // Source /etc/profile if it exists.
739            //
740            // Next source the first of these that exists and is readable (if any):
741            //     * ~/.bash_profile
742            //     * ~/.bash_login
743            //     * ~/.profile
744            //
745            self.source_if_exists(Path::new("/etc/profile"), &params)
746                .await?;
747            if let Some(home_path) = self.get_home_dir() {
748                if options.sh_mode {
749                    self.source_if_exists(home_path.join(".profile").as_path(), &params)
750                        .await?;
751                } else {
752                    if !self
753                        .source_if_exists(home_path.join(".bash_profile").as_path(), &params)
754                        .await?
755                    {
756                        if !self
757                            .source_if_exists(home_path.join(".bash_login").as_path(), &params)
758                            .await?
759                        {
760                            self.source_if_exists(home_path.join(".profile").as_path(), &params)
761                                .await?;
762                        }
763                    }
764                }
765            }
766        } else {
767            if options.interactive {
768                // --norc means skip this. Also skip in sh mode.
769                if options.no_rc || options.sh_mode {
770                    return Ok(());
771                }
772
773                // If an rc file was specified, then source it.
774                if let Some(rc_file) = &options.rc_file {
775                    // If an explicit rc file is provided, source it.
776                    self.source_if_exists(rc_file, &params).await?;
777                } else {
778                    //
779                    // Otherwise, for non-login interactive shells, load in this order:
780                    //
781                    //     /etc/bash.bashrc
782                    //     ~/.bashrc
783                    //
784                    self.source_if_exists(Path::new("/etc/bash.bashrc"), &params)
785                        .await?;
786                    if let Some(home_path) = self.get_home_dir() {
787                        self.source_if_exists(home_path.join(".bashrc").as_path(), &params)
788                            .await?;
789                        self.source_if_exists(home_path.join(".brushrc").as_path(), &params)
790                            .await?;
791                    }
792                }
793            } else {
794                let env_var_name = if options.sh_mode { "ENV" } else { "BASH_ENV" };
795
796                if self.env.is_set(env_var_name) {
797                    //
798                    // TODO: look at $ENV/BASH_ENV; source its expansion if that file exists
799                    //
800                    return error::unimp(
801                        "load config from $ENV/BASH_ENV for non-interactive, non-login shell",
802                    );
803                }
804            }
805        }
806
807        Ok(())
808    }
809
810    async fn source_if_exists(
811        &mut self,
812        path: impl AsRef<Path>,
813        params: &ExecutionParameters,
814    ) -> Result<bool, error::Error> {
815        let path = path.as_ref();
816        if path.exists() {
817            self.source_script(path, std::iter::empty::<String>(), params)
818                .await?;
819            Ok(true)
820        } else {
821            tracing::debug!("skipping non-existent file: {}", path.display());
822            Ok(false)
823        }
824    }
825
826    /// Source the given file as a shell script, returning the execution result.
827    ///
828    /// # Arguments
829    ///
830    /// * `path` - The path to the file to source.
831    /// * `args` - The arguments to pass to the script as positional parameters.
832    /// * `params` - Execution parameters.
833    pub async fn source_script<S: AsRef<str>, P: AsRef<Path>, I: Iterator<Item = S>>(
834        &mut self,
835        path: P,
836        args: I,
837        params: &ExecutionParameters,
838    ) -> Result<ExecutionResult, error::Error> {
839        self.parse_and_execute_script_file(path.as_ref(), args, params, ScriptCallType::Sourced)
840            .await
841    }
842
843    /// Parse and execute the given file as a shell script, returning the execution result.
844    ///
845    /// # Arguments
846    ///
847    /// * `path` - The path to the file to source.
848    /// * `args` - The arguments to pass to the script as positional parameters.
849    /// * `params` - Execution parameters.
850    /// * `call_type` - The type of script call being made.
851    async fn parse_and_execute_script_file<S: AsRef<str>, P: AsRef<Path>, I: Iterator<Item = S>>(
852        &mut self,
853        path: P,
854        args: I,
855        params: &ExecutionParameters,
856        call_type: ScriptCallType,
857    ) -> Result<ExecutionResult, error::Error> {
858        let path = path.as_ref();
859        tracing::debug!("sourcing: {}", path.display());
860        let opened_file: openfiles::OpenFile = self
861            .open_file(path, params)
862            .map_err(|e| error::Error::FailedSourcingFile(path.to_owned(), e.into()))?;
863
864        if opened_file.is_dir() {
865            return Err(error::Error::FailedSourcingFile(
866                path.to_owned(),
867                error::Error::IsADirectory.into(),
868            ));
869        }
870
871        let source_info = brush_parser::SourceInfo {
872            source: path.to_string_lossy().to_string(),
873        };
874
875        self.source_file(opened_file, &source_info, args, params, call_type)
876            .await
877    }
878
879    /// Source the given file as a shell script, returning the execution result.
880    ///
881    /// # Arguments
882    ///
883    /// * `file` - The file to source.
884    /// * `source_info` - Information about the source of the script.
885    /// * `args` - The arguments to pass to the script as positional parameters.
886    /// * `params` - Execution parameters.
887    /// * `call_type` - The type of script call being made.
888    async fn source_file<F: Read, S: AsRef<str>, I: Iterator<Item = S>>(
889        &mut self,
890        file: F,
891        source_info: &brush_parser::SourceInfo,
892        args: I,
893        params: &ExecutionParameters,
894        call_type: ScriptCallType,
895    ) -> Result<ExecutionResult, error::Error> {
896        let mut reader = std::io::BufReader::new(file);
897        let mut parser =
898            brush_parser::Parser::new(&mut reader, &self.parser_options(), source_info);
899
900        tracing::debug!(target: trace_categories::PARSE, "Parsing sourced file: {}", source_info.source);
901        let parse_result = parser.parse_program();
902
903        let mut other_positional_parameters = args.map(|s| s.as_ref().to_owned()).collect();
904        let mut other_shell_name = Some(source_info.source.clone());
905
906        // TODO: Find a cleaner way to change args.
907        std::mem::swap(&mut self.shell_name, &mut other_shell_name);
908        std::mem::swap(
909            &mut self.positional_parameters,
910            &mut other_positional_parameters,
911        );
912
913        self.script_call_stack
914            .push_front((call_type.clone(), source_info.source.clone()));
915
916        let result = self
917            .run_parsed_result(parse_result, source_info, params)
918            .await;
919
920        self.script_call_stack.pop_front();
921
922        // Restore.
923        std::mem::swap(&mut self.shell_name, &mut other_shell_name);
924        std::mem::swap(
925            &mut self.positional_parameters,
926            &mut other_positional_parameters,
927        );
928
929        result
930    }
931
932    /// Invokes a function defined in this shell, returning the resulting exit status.
933    ///
934    /// # Arguments
935    ///
936    /// * `name` - The name of the function to invoke.
937    /// * `args` - The arguments to pass to the function.
938    pub async fn invoke_function(&mut self, name: &str, args: &[&str]) -> Result<u8, error::Error> {
939        // TODO: Figure out if *all* callers have the same process group policy.
940        let params = self.default_exec_params();
941
942        let command_name = String::from(name);
943
944        let func_registration = self
945            .funcs
946            .get(name)
947            .ok_or_else(|| error::Error::FunctionNotFound(name.to_owned()))?;
948
949        let func = func_registration.definition.clone();
950
951        let context = commands::ExecutionContext {
952            shell: self,
953            command_name,
954            params,
955        };
956
957        let command_args = args
958            .iter()
959            .map(|s| commands::CommandArg::String(String::from(*s)))
960            .collect::<Vec<_>>();
961
962        match commands::invoke_shell_function(func, context, &command_args).await? {
963            commands::CommandSpawnResult::SpawnedProcess(_) => {
964                error::unimp("child spawned from function invocation")
965            }
966            commands::CommandSpawnResult::ImmediateExit(code) => Ok(code),
967            commands::CommandSpawnResult::ExitShell(code) => Ok(code),
968            commands::CommandSpawnResult::ReturnFromFunctionOrScript(code) => Ok(code),
969            commands::CommandSpawnResult::BreakLoop(_)
970            | commands::CommandSpawnResult::ContinueLoop(_) => {
971                error::unimp("break or continue returned from function invocation")
972            }
973        }
974    }
975
976    /// Executes the given string as a shell program, returning the resulting exit status.
977    ///
978    /// # Arguments
979    ///
980    /// * `command` - The command to execute.
981    /// * `params` - Execution parameters.
982    pub async fn run_string<S: Into<String>>(
983        &mut self,
984        command: S,
985        params: &ExecutionParameters,
986    ) -> Result<ExecutionResult, error::Error> {
987        // TODO: Actually track line numbers; this is something of a hack, assuming each time
988        // this function is invoked we are on the next line of the input. For one thing,
989        // each string we run could be multiple lines.
990        self.current_line_number += 1;
991
992        let parse_result = self.parse_string(command.into());
993        let source_info = brush_parser::SourceInfo {
994            source: String::from("main"),
995        };
996        self.run_parsed_result(parse_result, &source_info, params)
997            .await
998    }
999
1000    /// Parses the given reader as a shell program, returning the resulting Abstract Syntax Tree
1001    /// for the program.
1002    pub fn parse<R: Read>(
1003        &self,
1004        reader: R,
1005    ) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
1006        let mut parser = create_parser(reader, &self.parser_options());
1007
1008        tracing::debug!(target: trace_categories::PARSE, "Parsing reader as program...");
1009        parser.parse_program()
1010    }
1011
1012    /// Parses the given string as a shell program, returning the resulting Abstract Syntax Tree
1013    /// for the program.
1014    ///
1015    /// # Arguments
1016    ///
1017    /// * `s` - The string to parse as a program.
1018    pub fn parse_string<S: Into<String>>(
1019        &self,
1020        s: S,
1021    ) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
1022        parse_string_impl(s.into(), self.parser_options())
1023    }
1024
1025    /// Applies basic shell expansion to the provided string.
1026    ///
1027    /// # Arguments
1028    ///
1029    /// * `s` - The string to expand.
1030    pub async fn basic_expand_string<S: AsRef<str>>(
1031        &mut self,
1032        params: &ExecutionParameters,
1033        s: S,
1034    ) -> Result<String, error::Error> {
1035        let result = expansion::basic_expand_str(self, params, s.as_ref()).await?;
1036        Ok(result)
1037    }
1038
1039    /// Applies full shell expansion and field splitting to the provided string; returns
1040    /// a sequence of fields.
1041    ///
1042    /// # Arguments
1043    ///
1044    /// * `s` - The string to expand and split.
1045    pub async fn full_expand_and_split_string<S: AsRef<str>>(
1046        &mut self,
1047        params: &ExecutionParameters,
1048        s: S,
1049    ) -> Result<Vec<String>, error::Error> {
1050        let result = expansion::full_expand_and_split_str(self, params, s.as_ref()).await?;
1051        Ok(result)
1052    }
1053
1054    /// Returns the default execution parameters for this shell.
1055    pub fn default_exec_params(&self) -> ExecutionParameters {
1056        ExecutionParameters {
1057            open_files: self.open_files.clone(),
1058            ..Default::default()
1059        }
1060    }
1061
1062    /// Executes the given script file, returning the resulting exit status.
1063    ///
1064    /// # Arguments
1065    ///
1066    /// * `script_path` - The path to the script file to execute.
1067    /// * `args` - The arguments to pass to the script as positional parameters.
1068    pub async fn run_script<S: AsRef<str>, P: AsRef<Path>, I: Iterator<Item = S>>(
1069        &mut self,
1070        script_path: P,
1071        args: I,
1072    ) -> Result<ExecutionResult, error::Error> {
1073        let params = self.default_exec_params();
1074        self.parse_and_execute_script_file(
1075            script_path.as_ref(),
1076            args,
1077            &params,
1078            ScriptCallType::Executed,
1079        )
1080        .await
1081    }
1082
1083    async fn run_parsed_result(
1084        &mut self,
1085        parse_result: Result<brush_parser::ast::Program, brush_parser::ParseError>,
1086        source_info: &brush_parser::SourceInfo,
1087        params: &ExecutionParameters,
1088    ) -> Result<ExecutionResult, error::Error> {
1089        let error_prefix = if !source_info.source.is_empty() {
1090            format!("{}: ", source_info.source)
1091        } else {
1092            String::new()
1093        };
1094
1095        let result = match parse_result {
1096            Ok(prog) => match self.run_program(prog, params).await {
1097                Ok(result) => result,
1098                Err(e) => {
1099                    tracing::error!("error: {:#}", e);
1100                    self.last_exit_status = 1;
1101                    ExecutionResult::new(1)
1102                }
1103            },
1104            Err(brush_parser::ParseError::ParsingNearToken(token_near_error)) => {
1105                let error_loc = &token_near_error.location().start;
1106
1107                tracing::error!(
1108                    "{}syntax error near token `{}' (line {} col {})",
1109                    error_prefix,
1110                    token_near_error.to_str(),
1111                    error_loc.line,
1112                    error_loc.column,
1113                );
1114                self.last_exit_status = 2;
1115                ExecutionResult::new(2)
1116            }
1117            Err(brush_parser::ParseError::ParsingAtEndOfInput) => {
1118                tracing::error!("{}syntax error at end of input", error_prefix);
1119
1120                self.last_exit_status = 2;
1121                ExecutionResult::new(2)
1122            }
1123            Err(brush_parser::ParseError::Tokenizing { inner, position }) => {
1124                let mut error_message = error_prefix.clone();
1125                error_message.push_str(inner.to_string().as_str());
1126
1127                if let Some(position) = position {
1128                    write!(
1129                        error_message,
1130                        " (detected near line {} column {})",
1131                        position.line, position.column
1132                    )?;
1133                }
1134
1135                tracing::error!("{}", error_message);
1136
1137                self.last_exit_status = 2;
1138                ExecutionResult::new(2)
1139            }
1140        };
1141
1142        Ok(result)
1143    }
1144
1145    /// Executes the given parsed shell program, returning the resulting exit status.
1146    ///
1147    /// # Arguments
1148    ///
1149    /// * `program` - The program to execute.
1150    /// * `params` - Execution parameters.
1151    pub async fn run_program(
1152        &mut self,
1153        program: brush_parser::ast::Program,
1154        params: &ExecutionParameters,
1155    ) -> Result<ExecutionResult, error::Error> {
1156        program.execute(self, params).await
1157    }
1158
1159    const fn default_prompt(&self) -> &'static str {
1160        if self.options.sh_mode {
1161            "$ "
1162        } else {
1163            "brush$ "
1164        }
1165    }
1166
1167    /// Composes the shell's post-input, pre-command prompt, applying all appropriate expansions.
1168    pub async fn compose_precmd_prompt(&mut self) -> Result<String, error::Error> {
1169        self.expand_prompt_var("PS0", "").await
1170    }
1171
1172    /// Composes the shell's prompt, applying all appropriate expansions.
1173    pub async fn compose_prompt(&mut self) -> Result<String, error::Error> {
1174        self.expand_prompt_var("PS1", self.default_prompt()).await
1175    }
1176
1177    /// Compose's the shell's alternate-side prompt, applying all appropriate expansions.
1178    #[allow(clippy::unused_async)]
1179    pub async fn compose_alt_side_prompt(&mut self) -> Result<String, error::Error> {
1180        // This is a brush extension.
1181        self.expand_prompt_var("BRUSH_PS_ALT", "").await
1182    }
1183
1184    /// Composes the shell's continuation prompt.
1185    pub async fn compose_continuation_prompt(&mut self) -> Result<String, error::Error> {
1186        self.expand_prompt_var("PS2", "> ").await
1187    }
1188
1189    async fn expand_prompt_var(
1190        &mut self,
1191        var_name: &str,
1192        default: &str,
1193    ) -> Result<String, error::Error> {
1194        //
1195        // TODO(prompt): bash appears to do this in a subshell; we need to investigate
1196        // if that's required.
1197        //
1198
1199        // Retrieve the spec.
1200        let prompt_spec = self.parameter_or_default(var_name, default);
1201        if prompt_spec.is_empty() {
1202            return Ok(String::new());
1203        }
1204
1205        // Expand it.
1206        let formatted_prompt = prompt::expand_prompt(self, prompt_spec.into_owned())?;
1207
1208        // Now expand.
1209        let params = self.default_exec_params();
1210        expansion::basic_expand_str(self, &params, &formatted_prompt).await
1211    }
1212
1213    /// Returns the exit status of the last command executed in this shell.
1214    pub const fn last_result(&self) -> u8 {
1215        self.last_exit_status
1216    }
1217
1218    fn parameter_or_default<'a>(&'a self, name: &str, default: &'a str) -> Cow<'a, str> {
1219        self.get_env_str(name).unwrap_or_else(|| default.into())
1220    }
1221
1222    /// Returns the options that should be used for parsing shell programs; reflects
1223    /// the current configuration state of the shell and may change over time.
1224    pub const fn parser_options(&self) -> brush_parser::ParserOptions {
1225        brush_parser::ParserOptions {
1226            enable_extended_globbing: self.options.extended_globbing,
1227            posix_mode: self.options.posix_mode,
1228            sh_mode: self.options.sh_mode,
1229            tilde_expansion: true,
1230        }
1231    }
1232
1233    /// Returns whether or not the shell is actively executing in a sourced script.
1234    pub(crate) fn in_sourced_script(&self) -> bool {
1235        self.script_call_stack
1236            .front()
1237            .is_some_and(|(ty, _)| matches!(ty, ScriptCallType::Sourced))
1238    }
1239
1240    /// Returns whether or not the shell is actively executing in a shell function.
1241    pub(crate) fn in_function(&self) -> bool {
1242        !self.function_call_stack.is_empty()
1243    }
1244
1245    /// Updates the shell's internal tracking state to reflect that a new shell
1246    /// function is being entered.
1247    ///
1248    /// # Arguments
1249    ///
1250    /// * `name` - The name of the function being entered.
1251    /// * `function_def` - The definition of the function being entered.
1252    pub(crate) fn enter_function(
1253        &mut self,
1254        name: &str,
1255        function_def: &Arc<brush_parser::ast::FunctionDefinition>,
1256    ) -> Result<(), error::Error> {
1257        if let Some(max_call_depth) = self.options.max_function_call_depth {
1258            if self.function_call_stack.len() >= max_call_depth {
1259                return Err(error::Error::MaxFunctionCallDepthExceeded);
1260            }
1261        }
1262
1263        if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
1264            let depth = self.function_call_stack.len();
1265            let prefix = repeated_char_str(' ', depth);
1266            tracing::debug!(target: trace_categories::FUNCTIONS, "Entering func [depth={depth}]: {prefix}{name}");
1267        }
1268
1269        self.function_call_stack.push_front(FunctionCall {
1270            function_name: name.to_owned(),
1271            function_definition: function_def.clone(),
1272        });
1273        self.env.push_scope(env::EnvironmentScope::Local);
1274        Ok(())
1275    }
1276
1277    /// Updates the shell's internal tracking state to reflect that the shell
1278    /// has exited the top-most function on its call stack.
1279    pub(crate) fn leave_function(&mut self) -> Result<(), error::Error> {
1280        self.env.pop_scope(env::EnvironmentScope::Local)?;
1281
1282        if let Some(exited_call) = self.function_call_stack.pop_front() {
1283            if tracing::enabled!(target: trace_categories::FUNCTIONS, tracing::Level::DEBUG) {
1284                let depth = self.function_call_stack.len();
1285                let prefix = repeated_char_str(' ', depth);
1286                tracing::debug!(target: trace_categories::FUNCTIONS, "Exiting func  [depth={depth}]: {prefix}{}", exited_call.function_name);
1287            }
1288        }
1289
1290        Ok(())
1291    }
1292
1293    fn get_funcname_value(&self) -> variables::ShellValue {
1294        if self.function_call_stack.is_empty() {
1295            ShellValue::Unset(variables::ShellValueUnsetType::IndexedArray)
1296        } else {
1297            self.function_call_stack
1298                .iter()
1299                .map(|s| s.function_name.as_str())
1300                .collect::<Vec<_>>()
1301                .into()
1302        }
1303    }
1304
1305    fn get_bash_source_value(&self) -> variables::ShellValue {
1306        if self.function_call_stack.is_empty() {
1307            self.script_call_stack
1308                .front()
1309                .map_or_else(Vec::new, |(_call_type, s)| vec![s.as_ref()])
1310                .into()
1311        } else {
1312            self.function_call_stack
1313                .iter()
1314                .map(|s| s.function_definition.source.as_ref())
1315                .collect::<Vec<_>>()
1316                .into()
1317        }
1318    }
1319
1320    /// Returns the path to the history file used by the shell, if one is set.
1321    pub fn get_history_file_path(&self) -> Option<PathBuf> {
1322        self.get_env_str("HISTFILE")
1323            .map(|s| PathBuf::from(s.into_owned()))
1324    }
1325
1326    /// Returns the number of the line being executed in the currently executing program.
1327    pub(crate) const fn get_current_input_line_number(&self) -> u32 {
1328        self.current_line_number
1329    }
1330
1331    /// Tries to retrieve a variable from the shell's environment, converting it into its
1332    /// string form.
1333    ///
1334    /// # Arguments
1335    ///
1336    /// * `name` - The name of the variable to retrieve.
1337    pub fn get_env_str(&self, name: &str) -> Option<Cow<'_, str>> {
1338        self.env.get_str(name, self)
1339    }
1340
1341    /// Tries to retrieve a variable from the shell's environment.
1342    ///
1343    /// # Arguments
1344    ///
1345    /// * `name` - The name of the variable to retrieve.
1346    pub fn get_env_var(&self, name: &str) -> Option<&ShellVariable> {
1347        self.env.get(name).map(|(_, var)| var)
1348    }
1349
1350    /// Tries to set a global variable in the shell's environment.
1351    ///
1352    /// # Arguments
1353    ///
1354    /// * `name` - The name of the variable to add.
1355    /// * `var` - The variable contents to add.
1356    pub fn set_env_global(&mut self, name: &str, var: ShellVariable) -> Result<(), error::Error> {
1357        self.env.set_global(name, var)
1358    }
1359
1360    /// Register a builtin to the shell's environment.
1361    ///
1362    /// # Arguments
1363    ///
1364    /// * `name` - The in-shell name of the builtin.
1365    /// * `registration` - The registration handle for the builtin.
1366    pub fn register_builtin<S: Into<String>>(
1367        &mut self,
1368        name: S,
1369        registration: builtins::Registration,
1370    ) {
1371        self.builtins.insert(name.into(), registration);
1372    }
1373
1374    /// Returns the current value of the IFS variable, or the default value if it is not set.
1375    pub(crate) fn get_ifs(&self) -> Cow<'_, str> {
1376        self.get_env_str("IFS").unwrap_or_else(|| " \t\n".into())
1377    }
1378
1379    /// Returns the first character of the IFS variable, or a space if it is not set.
1380    pub(crate) fn get_ifs_first_char(&self) -> char {
1381        self.get_ifs().chars().next().unwrap_or(' ')
1382    }
1383
1384    /// Generates command completions for the shell.
1385    ///
1386    /// # Arguments
1387    ///
1388    /// * `input` - The input string to generate completions for.
1389    /// * `position` - The position in the input string to generate completions at.
1390    pub async fn get_completions(
1391        &mut self,
1392        input: &str,
1393        position: usize,
1394    ) -> Result<completion::Completions, error::Error> {
1395        let completion_config = self.completion_config.clone();
1396        completion_config
1397            .get_completions(self, input, position)
1398            .await
1399    }
1400
1401    /// Finds executables in the shell's current default PATH, matching the given glob pattern.
1402    ///
1403    /// # Arguments
1404    ///
1405    /// * `required_glob_pattern` - The glob pattern to match against.
1406    pub fn find_executables_in_path<'a>(
1407        &'a self,
1408        filename: &'a str,
1409    ) -> impl Iterator<Item = PathBuf> + 'a {
1410        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
1411        let paths = path_var.split(':').map(|s| s.to_owned());
1412
1413        pathsearch::search_for_executable(paths.into_iter(), filename)
1414    }
1415
1416    /// Finds executables in the shell's current default PATH, with filenames matching the
1417    /// given prefix.
1418    ///
1419    /// # Arguments
1420    ///
1421    /// * `filename_prefix` - The prefix to match against executable filenames.
1422    pub fn find_executables_in_path_with_prefix(
1423        &self,
1424        filename_prefix: &str,
1425        case_insensitive: bool,
1426    ) -> impl Iterator<Item = PathBuf> {
1427        let path_var = self.env.get_str("PATH", self).unwrap_or_default();
1428        let paths = path_var.split(':').map(|s| s.to_owned());
1429
1430        pathsearch::search_for_executable_with_prefix(
1431            paths.into_iter(),
1432            filename_prefix,
1433            case_insensitive,
1434        )
1435    }
1436
1437    /// Determines whether the given filename is the name of an executable in one of the
1438    /// directories in the shell's current PATH. If found, returns the path.
1439    ///
1440    /// # Arguments
1441    ///
1442    /// * `candidate_name` - The name of the file to look for.
1443    pub fn find_first_executable_in_path<S: AsRef<str>>(
1444        &self,
1445        candidate_name: S,
1446    ) -> Option<PathBuf> {
1447        for dir_str in self.get_env_str("PATH").unwrap_or_default().split(':') {
1448            let candidate_path = Path::new(dir_str).join(candidate_name.as_ref());
1449            if candidate_path.executable() {
1450                return Some(candidate_path);
1451            }
1452        }
1453        None
1454    }
1455
1456    /// Uses the shell's hash-based path cache to check whether the given filename is the name
1457    /// of an executable in one of the directories in the shell's current PATH. If found,
1458    /// ensures the path is in the cache and returns it.
1459    ///
1460    /// # Arguments
1461    ///
1462    /// * `candidate_name` - The name of the file to look for.
1463    pub fn find_first_executable_in_path_using_cache<S: AsRef<str>>(
1464        &mut self,
1465        candidate_name: S,
1466    ) -> Option<PathBuf> {
1467        if let Some(cached_path) = self.program_location_cache.get(&candidate_name) {
1468            Some(cached_path)
1469        } else if let Some(found_path) = self.find_first_executable_in_path(&candidate_name) {
1470            self.program_location_cache
1471                .set(&candidate_name, found_path.clone());
1472            Some(found_path)
1473        } else {
1474            None
1475        }
1476    }
1477
1478    /// Gets the absolute form of the given path.
1479    ///
1480    /// # Arguments
1481    ///
1482    /// * `path` - The path to get the absolute form of.
1483    pub fn get_absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
1484        let path = path.as_ref();
1485        if path.as_os_str().is_empty() || path.is_absolute() {
1486            path.to_owned()
1487        } else {
1488            self.working_dir.join(path)
1489        }
1490    }
1491
1492    /// Opens the given file.
1493    ///
1494    /// # Arguments
1495    ///
1496    /// * `path` - The path to the file to open; may be relative to the shell's working directory.
1497    /// * `params` - Execution parameters.
1498    pub(crate) fn open_file(
1499        &self,
1500        path: impl AsRef<Path>,
1501        params: &ExecutionParameters,
1502    ) -> Result<openfiles::OpenFile, error::Error> {
1503        let path_to_open = self.get_absolute_path(path.as_ref());
1504
1505        // See if this is a reference to a file descriptor, in which case the actual
1506        // /dev/fd* file path for this process may not match with what's in the execution
1507        // parameters.
1508        if let Some(parent) = path_to_open.parent() {
1509            if parent == Path::new("/dev/fd") {
1510                if let Some(filename) = path_to_open.file_name() {
1511                    if let Ok(fd_num) = filename.to_string_lossy().to_string().parse::<u32>() {
1512                        if let Some(open_file) = params.open_files.files.get(&fd_num) {
1513                            return open_file.try_dup();
1514                        }
1515                    }
1516                }
1517            }
1518        }
1519
1520        Ok(std::fs::File::open(path_to_open)?.into())
1521    }
1522
1523    /// Replaces the shell's file descriptor table with the given one.
1524    ///
1525    /// # Arguments
1526    ///
1527    /// * `open_files` - The new file descriptor table to use.
1528    #[allow(dead_code)]
1529    pub(crate) fn replace_open_files(&mut self, open_files: openfiles::OpenFiles) {
1530        self.open_files = open_files;
1531    }
1532
1533    /// Sets the shell's current working directory to the given path.
1534    ///
1535    /// # Arguments
1536    ///
1537    /// * `target_dir` - The path to set as the working directory.
1538    pub fn set_working_dir(&mut self, target_dir: impl AsRef<Path>) -> Result<(), error::Error> {
1539        let abs_path = self.get_absolute_path(target_dir.as_ref());
1540
1541        match std::fs::metadata(&abs_path) {
1542            Ok(m) => {
1543                if !m.is_dir() {
1544                    return Err(error::Error::NotADirectory(abs_path));
1545                }
1546            }
1547            Err(e) => {
1548                return Err(e.into());
1549            }
1550        }
1551
1552        // Normalize the path (but don't canonicalize it).
1553        let cleaned_path = abs_path.normalize();
1554
1555        let pwd = cleaned_path.to_string_lossy().to_string();
1556
1557        self.env.update_or_add(
1558            "PWD",
1559            variables::ShellValueLiteral::Scalar(pwd),
1560            |var| {
1561                var.export();
1562                Ok(())
1563            },
1564            EnvironmentLookup::Anywhere,
1565            EnvironmentScope::Global,
1566        )?;
1567        let oldpwd = std::mem::replace(&mut self.working_dir, cleaned_path);
1568
1569        self.env.update_or_add(
1570            "OLDPWD",
1571            variables::ShellValueLiteral::Scalar(oldpwd.to_string_lossy().to_string()),
1572            |var| {
1573                var.export();
1574                Ok(())
1575            },
1576            EnvironmentLookup::Anywhere,
1577            EnvironmentScope::Global,
1578        )?;
1579
1580        Ok(())
1581    }
1582
1583    /// Tilde-shortens the given string, replacing the user's home directory with a tilde.
1584    ///
1585    /// # Arguments
1586    ///
1587    /// * `s` - The string to shorten.
1588    pub(crate) fn tilde_shorten(&self, s: String) -> String {
1589        if let Some(home_dir) = self.get_home_dir() {
1590            if let Some(stripped) = s.strip_prefix(home_dir.to_string_lossy().as_ref()) {
1591                return format!("~{stripped}");
1592            }
1593        }
1594        s
1595    }
1596
1597    /// Returns the shell's current home directory, if available.
1598    pub(crate) fn get_home_dir(&self) -> Option<PathBuf> {
1599        Self::get_home_dir_with_env(&self.env, self)
1600    }
1601
1602    fn get_home_dir_with_env(env: &ShellEnvironment, shell: &Self) -> Option<PathBuf> {
1603        if let Some(home) = env.get_str("HOME", shell) {
1604            Some(PathBuf::from(home.to_string()))
1605        } else {
1606            // HOME isn't set, so let's sort it out ourselves.
1607            users::get_current_user_home_dir()
1608        }
1609    }
1610
1611    /// Returns a value that can be used to write to the shell's currently configured
1612    /// standard output stream using `write!` at al.
1613    pub fn stdout(&self) -> impl std::io::Write {
1614        self.open_files.files.get(&1).unwrap().try_dup().unwrap()
1615    }
1616
1617    /// Returns a value that can be used to write to the shell's currently configured
1618    /// standard error stream using `write!` et al.
1619    pub fn stderr(&self) -> impl std::io::Write {
1620        self.open_files.files.get(&2).unwrap().try_dup().unwrap()
1621    }
1622
1623    /// Outputs `set -x` style trace output for a command.
1624    ///
1625    /// # Arguments
1626    ///
1627    /// * `command` - The command to trace.
1628    pub(crate) async fn trace_command<S: AsRef<str>>(
1629        &mut self,
1630        command: S,
1631    ) -> Result<(), error::Error> {
1632        let ps4 = self.as_mut().expand_prompt_var("PS4", "").await?;
1633
1634        let mut prefix = ps4;
1635
1636        let additional_depth = self.script_call_stack.len() + self.depth;
1637        if let Some(c) = prefix.chars().next() {
1638            for _ in 0..additional_depth {
1639                prefix.insert(0, c);
1640            }
1641        }
1642
1643        writeln!(self.stderr(), "{prefix}{}", command.as_ref())?;
1644        Ok(())
1645    }
1646
1647    /// Returns the keywords that are reserved by the shell.
1648    pub(crate) fn get_keywords(&self) -> Vec<String> {
1649        if self.options.sh_mode {
1650            keywords::SH_MODE_KEYWORDS.iter().cloned().collect()
1651        } else {
1652            keywords::KEYWORDS.iter().cloned().collect()
1653        }
1654    }
1655
1656    /// Checks if the given string is a keyword reserved in this shell.
1657    ///
1658    /// # Arguments
1659    ///
1660    /// * `s` - The string to check.
1661    pub fn is_keyword(&self, s: &str) -> bool {
1662        if self.options.sh_mode {
1663            keywords::SH_MODE_KEYWORDS.contains(s)
1664        } else {
1665            keywords::KEYWORDS.contains(s)
1666        }
1667    }
1668
1669    /// Checks for completed jobs in the shell, reporting any changes found.
1670    pub fn check_for_completed_jobs(&mut self) -> Result<(), error::Error> {
1671        let results = self.jobs.poll()?;
1672
1673        if self.options.enable_job_control {
1674            for (job, _result) in results {
1675                writeln!(self.stderr(), "{job}")?;
1676            }
1677        }
1678
1679        Ok(())
1680    }
1681
1682    /// Evaluate the given arithmetic expression, returning the result.
1683    pub fn eval_arithmetic(
1684        &mut self,
1685        expr: &brush_parser::ast::ArithmeticExpr,
1686    ) -> Result<i64, error::Error> {
1687        Ok(expr.eval(self)?)
1688    }
1689
1690    /// Updates the shell state to reflect the given edit buffer contents.
1691    ///
1692    /// # Arguments
1693    ///
1694    /// * `contents` - The contents of the edit buffer.
1695    /// * `cursor` - The cursor position in the edit buffer.
1696    pub fn set_edit_buffer(&mut self, contents: String, cursor: usize) -> Result<(), error::Error> {
1697        self.env
1698            .set_global("READLINE_LINE", ShellVariable::new(contents.into()))?;
1699
1700        self.env.set_global(
1701            "READLINE_POINT",
1702            ShellVariable::new(cursor.to_string().into()),
1703        )?;
1704
1705        Ok(())
1706    }
1707
1708    /// Returns the contents of the shell's edit buffer, if any. The buffer
1709    /// state is cleared from the shell.
1710    pub fn pop_edit_buffer(&mut self) -> Result<Option<(String, usize)>, error::Error> {
1711        let line = self
1712            .env
1713            .unset("READLINE_LINE")?
1714            .map(|line| line.value().to_cow_str(self).to_string());
1715
1716        let point = self
1717            .env
1718            .unset("READLINE_POINT")?
1719            .and_then(|point| point.value().to_cow_str(self).parse::<usize>().ok())
1720            .unwrap_or(0);
1721
1722        if let Some(line) = line {
1723            Ok(Some((line, point)))
1724        } else {
1725            Ok(None)
1726        }
1727    }
1728}
1729
1730#[cached::proc_macro::cached(size = 64, result = true)]
1731fn parse_string_impl(
1732    s: String,
1733    parser_options: brush_parser::ParserOptions,
1734) -> Result<brush_parser::ast::Program, brush_parser::ParseError> {
1735    let mut parser = create_parser(s.as_bytes(), &parser_options);
1736
1737    tracing::debug!(target: trace_categories::PARSE, "Parsing string as program...");
1738    parser.parse_program()
1739}
1740
1741fn create_parser<R: Read>(
1742    r: R,
1743    parser_options: &brush_parser::ParserOptions,
1744) -> brush_parser::Parser<std::io::BufReader<R>> {
1745    let reader = std::io::BufReader::new(r);
1746    let source_info = brush_parser::SourceInfo {
1747        source: String::from("main"),
1748    };
1749
1750    brush_parser::Parser::new(reader, parser_options, &source_info)
1751}
1752
1753fn repeated_char_str(c: char, count: usize) -> String {
1754    (0..count).map(|_| c).collect()
1755}
1756
1757fn get_random_value(_shell: &Shell) -> ShellValue {
1758    let mut rng = rand::rng();
1759    let num = rng.random_range(0..32768);
1760    let str = num.to_string();
1761    str.into()
1762}
1763
1764fn get_srandom_value(_shell: &Shell) -> ShellValue {
1765    let mut rng = rand::rng();
1766    let num: u32 = rng.random();
1767    let str = num.to_string();
1768    str.into()
1769}
1770
1771/// Returns a list of the current user's group IDs, with the effective GID at the front.
1772fn get_current_user_gids() -> Vec<u32> {
1773    let mut groups = sys::users::get_user_group_ids().unwrap_or_default();
1774
1775    // If the effective GID is present but not in the first position in the list, then move
1776    // it there.
1777    if let Ok(gid) = sys::users::get_effective_gid() {
1778        if let Some(index) = groups.iter().position(|&g| g == gid) {
1779            if index > 0 {
1780                // Move it to the front.
1781                groups.remove(index);
1782                groups.insert(0, gid);
1783            }
1784        }
1785    }
1786
1787    groups
1788}