1pub mod shells;
2pub mod state;
3
4use std::{
5 collections::HashSet,
6 env, fs, num,
7 ops::{Deref, DerefMut},
8 path::PathBuf,
9 process::ExitStatus,
10};
11
12use bstr::{B, BString, ByteSlice};
13use duct::cmd;
14use indexmap::{IndexMap, IndexSet, map::IntoIter};
15use once_cell::sync::Lazy;
16use serde::{Deserialize, Serialize};
17use shell_quote::Bash;
18
19type EnvVarsInner = IndexMap<String, String>;
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
22pub struct EnvVars(EnvVarsInner);
23
24impl EnvVars {
25 pub fn new() -> Self {
26 Self::default()
27 }
28}
29
30impl Deref for EnvVars {
31 type Target = EnvVarsInner;
32 fn deref(&self) -> &Self::Target {
33 &self.0
34 }
35}
36
37impl DerefMut for EnvVars {
38 fn deref_mut(&mut self) -> &mut Self::Target {
39 &mut self.0
40 }
41}
42
43impl IntoIterator for EnvVars {
44 type Item = (String, String);
45 type IntoIter = EnvVarIntoIter<String, String>;
46
47 fn into_iter(self) -> Self::IntoIter {
48 EnvVarIntoIter(self.0.into_iter())
49 }
50}
51
52impl FromIterator<(String, String)> for EnvVars {
53 fn from_iter<I: IntoIterator<Item = (String, String)>>(iter: I) -> Self {
54 EnvVars(EnvVarsInner::from_iter(iter))
55 }
56}
57
58impl From<EnvVars> for EnvVarsState {
59 fn from(value: EnvVars) -> Self {
60 EnvVarsState(value.0.into_iter().map(|(k, v)| (k, Some(v))).collect())
61 }
62}
63
64type EnvVarsStateInner = IndexMap<String, Option<String>>;
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
67pub struct EnvVarsState(EnvVarsStateInner);
68
69impl EnvVarsState {
70 pub fn new() -> Self {
71 Self::default()
72 }
73}
74
75impl Deref for EnvVarsState {
76 type Target = EnvVarsStateInner;
77 fn deref(&self) -> &Self::Target {
78 &self.0
79 }
80}
81
82impl DerefMut for EnvVarsState {
83 fn deref_mut(&mut self) -> &mut Self::Target {
84 &mut self.0
85 }
86}
87
88impl IntoIterator for EnvVarsState {
89 type Item = (String, Option<String>);
90 type IntoIter = EnvVarIntoIter<String, Option<String>>;
91
92 fn into_iter(self) -> Self::IntoIter {
93 EnvVarIntoIter(self.0.into_iter())
94 }
95}
96
97impl FromIterator<(String, Option<String>)> for EnvVarsState {
98 fn from_iter<I: IntoIterator<Item = (String, Option<String>)>>(iter: I) -> Self {
99 EnvVarsState(EnvVarsStateInner::from_iter(iter))
100 }
101}
102
103type EnvVarInner<K, V> = IntoIter<K, V>;
104
105pub struct EnvVarIntoIter<K, V>(EnvVarInner<K, V>);
106
107impl<K, V> Deref for EnvVarIntoIter<K, V> {
108 type Target = EnvVarInner<K, V>;
109 fn deref(&self) -> &Self::Target {
110 &self.0
111 }
112}
113
114impl<K, V> DerefMut for EnvVarIntoIter<K, V> {
115 fn deref_mut(&mut self) -> &mut Self::Target {
116 &mut self.0
117 }
118}
119
120impl<K, V> Iterator for EnvVarIntoIter<K, V> {
121 type Item = (K, V);
122 fn next(&mut self) -> Option<Self::Item> {
123 self.0.next()
124 }
125}
126
127pub fn get_old_env_vars_to_be_updated(old_env_vars: EnvVars, new_env_vars: &EnvVars) -> EnvVars {
128 old_env_vars
129 .into_iter()
130 .fold(EnvVars::new(), |mut acc, (key, value)| {
131 if new_env_vars.contains_key(&key) && new_env_vars.get(&key) != Some(&value) {
132 acc.insert(key, value);
133 }
134 acc
135 })
136}
137
138pub fn get_env_vars_reset(
139 mut old_env_vars_that_were_updated: EnvVars,
140 new_env_vars: HashSet<String>,
141 env_state_var_key: String,
142) -> EnvVarsState {
143 let mut env_vars_state = new_env_vars
144 .into_iter()
145 .fold(EnvVarsState::new(), |mut acc, key| {
146 let value = old_env_vars_that_were_updated.shift_remove(&key);
147 acc.insert(key, value);
148 acc
149 });
150 env_vars_state.insert(env_state_var_key, None);
151 env_vars_state
152}
153
154pub fn get_env_vars_from_current_process() -> EnvVars {
155 EnvVars(env::vars().collect::<EnvVarsInner>())
156}
157
158pub enum BashSource {
159 File(PathBuf),
160 Script(BString),
161}
162
163impl AsRef<BashSource> for BashSource {
164 fn as_ref(&self) -> &BashSource {
165 self
166 }
167}
168
169impl BashSource {
170 fn to_command_string(&self) -> BString {
171 match &self {
172 Self::File(path) => bstr::join(" ", [B("source"), &Bash::quote_vec(path)]).into(),
173 Self::Script(script) => bstr::join(" ", [B("eval"), &Bash::quote_vec(script)]).into(),
174 }
175 }
176}
177
178pub(crate) trait SimplifiedExitOk {
179 fn simplified_exit_ok(&self) -> anyhow::Result<()>;
180}
181
182impl SimplifiedExitOk for ExitStatus {
183 fn simplified_exit_ok(&self) -> anyhow::Result<()> {
186 match num::NonZero::try_from(self.code().unwrap_or(-1)) {
187 Ok(_) => Err(anyhow::format_err!(
188 "process exited unsuccessfully: {}",
189 &self
190 )),
191 Err(_) => Ok(()),
192 }
193 }
194}
195
196pub fn get_env_vars_from_bash(
197 source: impl AsRef<BashSource>,
198 env_vars: Option<EnvVars>,
199) -> anyhow::Result<EnvVars> {
200 let bash_env_vars_file = tempfile::NamedTempFile::new()?;
201
202 let command_string = bstr::join(
203 " ",
204 [
205 &source.as_ref().to_command_string(),
206 B("&& env -0 >"),
207 &Bash::quote_vec(bash_env_vars_file.path()),
208 ],
209 );
210 let handle = cmd!("bash", "-c", command_string.to_os_str()?)
211 .full_env(env_vars.unwrap_or_default())
212 .stdout_to_stderr()
213 .start()?;
214 let output = handle.wait()?;
215 output
216 .status
217 .simplified_exit_ok()
218 .map_err(|e| anyhow::format_err!("Bash command to retrieve env vars failed:\n{e}"))?;
219
220 let bash_env_vars_string = fs::read_to_string(bash_env_vars_file.path())?;
221
222 let bash_env_vars = EnvVars(
223 bash_env_vars_string
224 .split('\0')
225 .filter_map(|env_var| env_var.split_once('='))
226 .map(|(key, value)| (String::from(key), String::from(value)))
227 .collect::<EnvVarsInner>(),
228 );
229
230 Ok(bash_env_vars)
231}
232
233pub fn merge_delimited_env_var(
234 env_var: &str,
235 split_delimiter: char,
236 join_delimiter: char,
237 old_env_vars: &EnvVars,
238 new_env_vars: &mut EnvVars,
239) {
240 if let (Some(old_value), Some(new_value)) =
241 (old_env_vars.get(env_var), new_env_vars.get_mut(env_var))
242 {
243 *new_value = merge_delimited_values(split_delimiter, join_delimiter, old_value, new_value);
244 }
245}
246
247pub fn merge_delimited_values(
248 split_delimiter: char,
249 join_delimiter: char,
250 old_value: &str,
251 new_value: &str,
252) -> String {
253 new_value
254 .split(split_delimiter)
255 .chain(old_value.split(split_delimiter))
256 .collect::<IndexSet<_>>()
257 .into_iter()
258 .collect::<Vec<_>>()
259 .join(&join_delimiter.to_string())
260}
261
262const IGNORED_ENV_VAR_PREFIXES: &[&str] = &["__fish", "BASH_FUNC_"];
263
264static IGNORED_ENV_VAR_KEYS: Lazy<HashSet<&str>> = Lazy::new(|| {
265 HashSet::from([
266 "DIRENV_CONFIG",
268 "DIRENV_BASH",
269 "DIRENV_IN_ENVRC",
271 "COMP_WORDBREAKS", "PS1", "OLDPWD",
275 "PWD",
276 "SHELL",
277 "SHELLOPTS",
278 "SHLVL",
279 "_",
280 "__CF_USER_TEXT_ENCODING",
281 ])
282});
283
284pub fn ignored_env_var_key(env_var_key: &str) -> bool {
285 for ignored_env_var_prefix in IGNORED_ENV_VAR_PREFIXES {
286 if env_var_key.starts_with(ignored_env_var_prefix) {
287 return true;
288 }
289 }
290 IGNORED_ENV_VAR_KEYS.contains(env_var_key)
291}
292
293pub fn remove_ignored_env_vars(env_vars: &mut EnvVars) {
294 let env_var_keys = env_vars.keys().cloned().collect::<Vec<_>>();
295 env_var_keys.into_iter().for_each(|env_var_key| {
296 if ignored_env_var_key(&env_var_key) {
297 env_vars.shift_remove(&env_var_key);
298 }
299 });
300}