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, ¶ms).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(), ¶ms)
114 .await?;
115 } else {
116 if !self
117 .source_if_exists(home_path.join(".bash_profile").as_path(), ¶ms)
118 .await?
119 {
120 if !self
121 .source_if_exists(home_path.join(".bash_login").as_path(), ¶ms)
122 .await?
123 {
124 self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
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, ¶ms).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, ¶ms).await?;
149 }
150 if let Some(home_path) = self.home_dir() {
151 self.source_if_exists(home_path.join(".bashrc").as_path(), ¶ms)
152 .await?;
153 self.source_if_exists(home_path.join(".shrubrc").as_path(), ¶ms)
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}