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 pub async fn load_config(
54 &mut self,
55 profile_behavior: &ProfileLoadBehavior,
56 rc_behavior: &RcLoadBehavior,
57 ) -> Result<(), error::Error> {
58 self.load_config_files(profile_behavior, rc_behavior)
59 .await?;
60
61 // As bash does, skip the file if startup files already added entries
62 // (e.g. via `history -s`). Do NOT fail if we can't load
63 // history.
64 if self.options.enable_command_history
65 && self
66 .history
67 .as_ref()
68 .is_none_or(crate::history::History::is_empty)
69 && let Ok(Some(history)) = self.load_history()
70 {
71 self.history = Some(history);
72 }
73
74 Ok(())
75 }
76
77 async fn load_config_files(
78 &mut self,
79 profile_behavior: &ProfileLoadBehavior,
80 rc_behavior: &RcLoadBehavior,
81 ) -> Result<(), error::Error> {
82 let mut params = self.default_exec_params();
83 params.process_group_policy = interp::ProcessGroupPolicy::SameProcessGroup;
84
85 if self.options.login_shell {
86 // --noprofile means skip this.
87 if matches!(profile_behavior, ProfileLoadBehavior::Skip) {
88 return Ok(());
89 }
90
91 //
92 // Source the system profile if it exists.
93 //
94 // Next source the first of these that exists and is readable (if
95 // any):
96 // * ~/.bash_profile
97 // * ~/.bash_login
98 // * ~/.profile
99 //
100 if let Some(system_profile) = crate::sys::fs::get_system_profile_path() {
101 self.source_if_exists(system_profile, ¶ms).await?;
102 }
103 if let Some(home_path) = self.home_dir() {
104 if self.options.sh_mode {
105 self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
106 .await?;
107 } else {
108 if !self
109 .source_if_exists(home_path.join(".bash_profile").as_path(), ¶ms)
110 .await?
111 {
112 if !self
113 .source_if_exists(home_path.join(".bash_login").as_path(), ¶ms)
114 .await?
115 {
116 self.source_if_exists(home_path.join(".profile").as_path(), ¶ms)
117 .await?;
118 }
119 }
120 }
121 }
122 } else {
123 if self.options.interactive {
124 match rc_behavior {
125 _ if self.options.sh_mode => (),
126 RcLoadBehavior::Skip => (),
127 RcLoadBehavior::LoadCustom(rc_file) => {
128 // If an explicit rc file is provided, source it.
129 self.source_if_exists(rc_file, ¶ms).await?;
130 }
131 RcLoadBehavior::LoadDefault => {
132 //
133 // Otherwise, for non-login interactive shells, load in
134 // this order:
135 //
136 // system rc file (e.g. /etc/bash.bashrc on Unix)
137 // ~/.bashrc
138 //
139 if let Some(system_rc) = crate::sys::fs::get_system_rc_path() {
140 self.source_if_exists(system_rc, ¶ms).await?;
141 }
142 if let Some(home_path) = self.home_dir() {
143 self.source_if_exists(home_path.join(".bashrc").as_path(), ¶ms)
144 .await?;
145 self.source_if_exists(home_path.join(".shrubrc").as_path(), ¶ms)
146 .await?;
147 }
148 }
149 }
150 } else {
151 let env_var_name = if self.options.sh_mode {
152 "ENV"
153 } else {
154 "BASH_ENV"
155 };
156
157 if self.env.is_set(env_var_name) {
158 //
159 // TODO(well-known-vars): look at $ENV/BASH_ENV; source its
160 // expansion if that file exists
161 //
162 return error::unimp(
163 "load config from $ENV/BASH_ENV for non-interactive, non-login shell",
164 );
165 }
166 }
167 }
168
169 Ok(())
170 }
171}