Skip to main content

shrub_core/shell/
initscripts.rs

1//! Init script support for shells.
2
3use std::path::PathBuf;
4
5use crate::{Shell, error, extensions, interp};
6
7/// Behavior for loading profile files.
8#[derive(Default)]
9pub enum ProfileLoadBehavior {
10    /// Load the default profile files.
11    #[default]
12    LoadDefault,
13    /// Skip loading profile files.
14    Skip,
15}
16
17impl ProfileLoadBehavior {
18    /// Returns whether profile loading should be skipped.
19    pub const fn skip(&self) -> bool {
20        matches!(self, Self::Skip)
21    }
22}
23
24/// Behavior for loading rc files.
25#[derive(Default)]
26pub enum RcLoadBehavior {
27    /// Load the default rc files.
28    #[default]
29    LoadDefault,
30    /// Load a custom rc file; do not load defaults.
31    LoadCustom(PathBuf),
32    /// Skip loading rc files.
33    Skip,
34}
35
36impl RcLoadBehavior {
37    /// Returns whether rc loading should be skipped.
38    pub const fn skip(&self) -> bool {
39        matches!(self, Self::Skip)
40    }
41}
42
43impl<SE: extensions::ShellExtensions> Shell<SE> {
44    /// Loads and executes standard shell configuration files (i.e., rc and
45    /// profile), and then loads the history file (if command history is
46    /// enabled). History is loaded last since the configuration files may
47    /// change `HISTFILE`.
48    ///
49    /// # Arguments
50    ///
51    /// * `profile_behavior` - Behavior for loading profile files.
52    /// * `rc_behavior` - Behavior for loading rc files.
53    ///
54    /// # Errors
55    ///
56    /// - [`crate::error::ErrorKind::FailedSourcingFile`] if a profile or rc
57    ///   file exists but cannot be read, or is a directory.
58    /// - [`crate::error::ErrorKind::Unimplemented`] for a non-interactive,
59    ///   non-login shell with `BASH_ENV` (or `ENV` in `sh` mode) set.
60    /// - Failure to load the history file is not an error.
61    pub async fn load_config(
62        &mut self,
63        profile_behavior: &ProfileLoadBehavior,
64        rc_behavior: &RcLoadBehavior,
65    ) -> Result<(), error::Error> {
66        self.load_config_files(profile_behavior, rc_behavior)
67            .await?;
68
69        // As bash does, skip the file if startup files already added entries
70        // (e.g. via `history -s`). Do NOT fail if we can't load
71        // history.
72        if self.options.enable_command_history
73            && self
74                .history
75                .as_ref()
76                .is_none_or(crate::history::History::is_empty)
77            && let Ok(Some(history)) = self.load_history()
78        {
79            self.history = Some(history);
80        }
81
82        Ok(())
83    }
84
85    async fn load_config_files(
86        &mut self,
87        profile_behavior: &ProfileLoadBehavior,
88        rc_behavior: &RcLoadBehavior,
89    ) -> Result<(), error::Error> {
90        let mut params = self.default_exec_params();
91        params.process_group_policy = interp::ProcessGroupPolicy::SameProcessGroup;
92
93        if self.options.login_shell {
94            // --noprofile means skip this.
95            if matches!(profile_behavior, ProfileLoadBehavior::Skip) {
96                return Ok(());
97            }
98
99            //
100            // Source the system profile if it exists.
101            //
102            // Next source the first of these that exists and is readable (if
103            // any):
104            //     * ~/.bash_profile
105            //     * ~/.bash_login
106            //     * ~/.profile
107            //
108            if let Some(system_profile) = crate::sys::fs::get_system_profile_path() {
109                self.source_if_exists(system_profile, &params).await?;
110            }
111            if let Some(home_path) = self.home_dir() {
112                if self.options.sh_mode {
113                    self.source_if_exists(home_path.join(".profile").as_path(), &params)
114                        .await?;
115                } else {
116                    if !self
117                        .source_if_exists(home_path.join(".bash_profile").as_path(), &params)
118                        .await?
119                    {
120                        if !self
121                            .source_if_exists(home_path.join(".bash_login").as_path(), &params)
122                            .await?
123                        {
124                            self.source_if_exists(home_path.join(".profile").as_path(), &params)
125                                .await?;
126                        }
127                    }
128                }
129            }
130        } else {
131            if self.options.interactive {
132                match rc_behavior {
133                    _ if self.options.sh_mode => (),
134                    RcLoadBehavior::Skip => (),
135                    RcLoadBehavior::LoadCustom(rc_file) => {
136                        // If an explicit rc file is provided, source it.
137                        self.source_if_exists(rc_file, &params).await?;
138                    }
139                    RcLoadBehavior::LoadDefault => {
140                        //
141                        // Otherwise, for non-login interactive shells, load in
142                        // this order:
143                        //
144                        //     system rc file (e.g. /etc/bash.bashrc on Unix)
145                        //     ~/.bashrc
146                        //
147                        if let Some(system_rc) = crate::sys::fs::get_system_rc_path() {
148                            self.source_if_exists(system_rc, &params).await?;
149                        }
150                        if let Some(home_path) = self.home_dir() {
151                            self.source_if_exists(home_path.join(".bashrc").as_path(), &params)
152                                .await?;
153                            self.source_if_exists(home_path.join(".shrubrc").as_path(), &params)
154                                .await?;
155                        }
156                    }
157                }
158            } else {
159                let env_var_name = if self.options.sh_mode {
160                    "ENV"
161                } else {
162                    "BASH_ENV"
163                };
164
165                if self.env.is_set(env_var_name) {
166                    //
167                    // TODO(well-known-vars): look at $ENV/BASH_ENV; source its
168                    // expansion if that file exists
169                    //
170                    return error::unimp(
171                        "load config from $ENV/BASH_ENV for non-interactive, non-login shell",
172                    );
173                }
174            }
175        }
176
177        Ok(())
178    }
179}