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