brush_core/
shell.rs

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