1pub mod shells;
2pub mod state;
3
4use std::{
5 collections::{BTreeMap, HashSet},
6 env, fs, num,
7 path::PathBuf,
8 process::ExitStatus,
9};
10
11use bstr::{B, BString, ByteSlice};
12use duct::cmd;
13use indexmap::IndexSet;
14use once_cell::sync::Lazy;
15use shell_quote::Bash;
16
17pub type EnvVars = BTreeMap<String, String>;
18pub type EnvVarsState = BTreeMap<String, Option<String>>;
19
20pub fn get_old_env_vars_to_be_updated(old_env_vars: EnvVars, new_env_vars: &EnvVars) -> EnvVars {
21 old_env_vars
22 .into_iter()
23 .fold(BTreeMap::new(), |mut acc, (key, value)| {
24 if new_env_vars.contains_key(&key) && new_env_vars.get(&key) != Some(&value) {
25 acc.insert(key, value);
26 }
27 acc
28 })
29}
30
31pub fn env_vars_state_from_env_vars(env_vars: EnvVars) -> EnvVarsState {
41 env_vars
42 .into_iter()
43 .map(|(key, value)| (key, Some(value)))
44 .collect()
45}
46
47pub fn get_env_vars_reset(
48 mut old_env_vars_that_were_updated: EnvVars,
49 new_env_vars: HashSet<String>,
50 env_state_var_key: String,
51) -> EnvVarsState {
52 let mut env_vars_state = new_env_vars
53 .into_iter()
54 .fold(EnvVarsState::new(), |mut acc, key| {
55 let value = old_env_vars_that_were_updated.remove(&key);
56 acc.insert(key, value);
57 acc
58 });
59 env_vars_state.insert(env_state_var_key, None);
60 env_vars_state
61}
62
63pub fn get_env_vars_from_current_process() -> EnvVars {
64 env::vars().collect::<EnvVars>()
65}
66
67pub enum BashSource {
68 File(PathBuf),
69 Script(BString),
70}
71
72impl AsRef<BashSource> for BashSource {
73 fn as_ref(&self) -> &BashSource {
74 self
75 }
76}
77
78impl BashSource {
79 fn to_command_string(&self) -> BString {
80 match &self {
81 Self::File(path) => bstr::join(" ", [B("source"), &Bash::quote_vec(path)]).into(),
82 Self::Script(script) => bstr::join(" ", [B("eval"), &Bash::quote_vec(script)]).into(),
83 }
84 }
85}
86
87pub(crate) trait SimplifiedExitOk {
88 fn simplified_exit_ok(&self) -> anyhow::Result<()>;
89}
90
91impl SimplifiedExitOk for ExitStatus {
92 fn simplified_exit_ok(&self) -> anyhow::Result<()> {
95 match num::NonZero::try_from(self.code().unwrap_or(-1)) {
96 Ok(_) => Err(anyhow::format_err!(
97 "process exited unsuccessfully: {}",
98 &self
99 )),
100 Err(_) => Ok(()),
101 }
102 }
103}
104
105pub fn get_env_vars_from_bash(
106 source: impl AsRef<BashSource>,
107 env_vars: Option<EnvVars>,
108) -> anyhow::Result<EnvVars> {
109 let bash_env_vars_file = tempfile::NamedTempFile::new()?;
110
111 let command_string = bstr::join(
112 " ",
113 [
114 &source.as_ref().to_command_string(),
115 B("&& env -0 >"),
116 &Bash::quote_vec(bash_env_vars_file.path()),
117 ],
118 );
119 let handle = cmd!("bash", "-c", command_string.to_os_str()?)
120 .full_env(env_vars.unwrap_or_default())
121 .stdout_to_stderr()
122 .start()?;
123 let output = handle.wait()?;
124 output
125 .status
126 .simplified_exit_ok()
127 .map_err(|e| anyhow::format_err!("Bash command to retrieve env vars failed:\n{e}"))?;
128
129 let bash_env_vars_string = fs::read_to_string(bash_env_vars_file.path())?;
130
131 let bash_env_vars = bash_env_vars_string
132 .split('\0')
133 .filter_map(|env_var| env_var.split_once('='))
134 .map(|(key, value)| (String::from(key), String::from(value)))
135 .collect::<EnvVars>();
136
137 Ok(bash_env_vars)
138}
139
140pub fn merge_delimited_env_var(
141 env_var: &str,
142 split_delimiter: char,
143 join_delimiter: char,
144 old_env_vars: &BTreeMap<String, String>,
145 new_env_vars: &mut BTreeMap<String, String>,
146) {
147 if let (Some(old_value), Some(new_value)) =
148 (old_env_vars.get(env_var), new_env_vars.get_mut(env_var))
149 {
150 *new_value = merge_delimited_values(split_delimiter, join_delimiter, old_value, new_value);
151 }
152}
153
154pub fn merge_delimited_values(
155 split_delimiter: char,
156 join_delimiter: char,
157 old_value: &str,
158 new_value: &str,
159) -> String {
160 new_value
161 .split(split_delimiter)
162 .chain(old_value.split(split_delimiter))
163 .collect::<IndexSet<_>>()
164 .into_iter()
165 .collect::<Vec<_>>()
166 .join(&join_delimiter.to_string())
167}
168
169const IGNORED_ENV_VAR_PREFIXES: &[&str] = &["__fish", "BASH_FUNC_"];
170
171static IGNORED_ENV_VAR_KEYS: Lazy<HashSet<&str>> = Lazy::new(|| {
172 HashSet::from([
173 "DIRENV_CONFIG",
175 "DIRENV_BASH",
176 "DIRENV_IN_ENVRC",
178 "COMP_WORDBREAKS", "PS1", "OLDPWD",
182 "PWD",
183 "SHELL",
184 "SHELLOPTS",
185 "SHLVL",
186 "_",
187 ])
188});
189
190pub fn ignored_env_var_key(env_var_key: &str) -> bool {
191 for ignored_env_var_prefix in IGNORED_ENV_VAR_PREFIXES {
192 if env_var_key.starts_with(ignored_env_var_prefix) {
193 return true;
194 }
195 }
196 IGNORED_ENV_VAR_KEYS.contains(env_var_key)
197}
198
199pub fn remove_ignored_env_vars(env_vars: &mut EnvVars) {
200 let env_var_keys = env_vars.keys().cloned().collect::<Vec<_>>();
201 env_var_keys.into_iter().for_each(|env_var_key| {
202 if ignored_env_var_key(&env_var_key) {
203 env_vars.remove(&env_var_key);
204 }
205 });
206}