1use std::path::Path;
2
3use anyhow::{Context as _, Result};
4use collections::HashMap;
5use serde::Deserialize;
6
7use crate::shell::ShellKind;
8
9fn parse_env_map_from_noisy_output(output: &str) -> Result<collections::HashMap<String, String>> {
10 for (position, _) in output.match_indices('{') {
11 let candidate = &output[position..];
12 let mut deserializer = serde_json::Deserializer::from_str(candidate);
13 if let Ok(env_map) = HashMap::<String, String>::deserialize(&mut deserializer) {
14 return Ok(env_map);
15 }
16 }
17 anyhow::bail!("Failed to find JSON in shell output: {output}")
18}
19
20pub fn print_env() {
21 let env_vars: HashMap<String, String> = std::env::vars().collect();
22 let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
23 eprintln!("Error serializing environment variables: {}", err);
24 std::process::exit(1);
25 });
26 println!("{}", json);
27}
28
29pub async fn capture(
31 shell_path: impl AsRef<Path>,
32 args: &[String],
33 directory: impl AsRef<Path>,
34) -> Result<collections::HashMap<String, String>> {
35 #[cfg(windows)]
36 return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
37 #[cfg(unix)]
38 return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
39}
40
41fn parse_env_output(
47 env_output: &str,
48 status: &std::process::ExitStatus,
49 successful_capture_warning: impl FnOnce() -> String,
50 failed_capture_error: impl FnOnce() -> String,
51) -> Result<collections::HashMap<String, String>> {
52 match parse_env_map_from_noisy_output(env_output) {
53 Ok(env_map) => {
54 if !status.success() {
55 log::warn!("{}", successful_capture_warning());
56 }
57 Ok(env_map)
58 }
59 Err(parse_error) => {
60 if !status.success() {
61 anyhow::bail!(
62 "{}. Failed to deserialize environment variables from json: {parse_error}. output: {env_output}",
63 failed_capture_error(),
64 );
65 }
66
67 anyhow::bail!(
68 "Failed to deserialize environment variables from json: {parse_error}. output: {env_output}"
69 );
70 }
71 }
72}
73
74#[cfg(unix)]
75async fn capture_unix(
76 shell_path: &Path,
77 args: &[String],
78 directory: &Path,
79) -> Result<collections::HashMap<String, String>> {
80 use std::os::unix::process::CommandExt;
81
82 use crate::command::new_std_command;
83
84 let shell_kind = ShellKind::new(shell_path, false);
85 let quoted_app_path = super::get_shell_safe_app_path(shell_kind)?;
86
87 let mut command_string = String::new();
88 let mut command = new_std_command(shell_path);
89 command.args(args);
90 const FD_STDIN: std::os::fd::RawFd = 0;
93 const FD_STDOUT: std::os::fd::RawFd = 1;
94 const FD_STDERR: std::os::fd::RawFd = 2;
95
96 let (fd_num, redir) = match shell_kind {
97 ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
99 ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
102 ShellKind::PowerShell => (FD_STDIN, format!(">{}", FD_STDIN)),
103 _ => (FD_STDIN, format!(">&{}", FD_STDIN)), };
105
106 match shell_kind {
107 ShellKind::Csh | ShellKind::Tcsh => {
108 command.arg0("-");
110 }
111 ShellKind::Fish => {
112 command_string.push_str("emit fish_prompt;");
114 command.arg("-l");
115 }
116 _ => {
117 command.arg("-l");
118 }
119 }
120
121 match shell_kind {
122 ShellKind::Nushell => command.arg("-e"),
126 _ => command.args(["-i", "-c"]),
127 };
128
129 let dir_str = directory.to_string_lossy();
131 let dir_str = if dir_str.starts_with('-') {
132 format!("./{dir_str}").into()
133 } else {
134 dir_str
135 };
136 let quoted_dir = shell_kind
137 .try_quote(&dir_str)
138 .context("unexpected null in directory name")?;
139
140 command_string.push_str(&format!("cd {};", quoted_dir));
142 if let Some(prefix) = shell_kind.command_prefix() {
143 command_string.push(prefix);
144 }
145 command_string.push_str(&format!("{} --printenv {}", quoted_app_path, redir));
146
147 if let ShellKind::Nushell = shell_kind {
148 command_string.push_str("; exit");
149 }
150
151 command.arg(&command_string);
152
153 super::set_pre_exec_to_start_new_session(&mut command);
154
155 let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?;
156 let env_output = String::from_utf8_lossy(&env_output);
157
158 parse_env_output(
159 &env_output,
160 &process_output.status,
161 || {
162 format!(
163 "login shell exited with {} but environment was captured successfully. stderr: {:?}",
164 process_output.status,
165 String::from_utf8_lossy(&process_output.stderr),
166 )
167 },
168 || {
169 format!(
170 "login shell exited with {}. stdout: {:?}, stderr: {:?}",
171 process_output.status,
172 String::from_utf8_lossy(&process_output.stdout),
173 String::from_utf8_lossy(&process_output.stderr),
174 )
175 },
176 )
177}
178
179#[cfg(unix)]
180async fn spawn_and_read_fd(
181 mut command: std::process::Command,
182 child_fd: std::os::fd::RawFd,
183) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
184 use command_fds::{CommandFdExt, FdMapping};
185 use std::{io::Read, process::Stdio};
186
187 let (mut reader, writer) = std::io::pipe()?;
188
189 command.fd_mappings(vec![FdMapping {
190 parent_fd: writer.into(),
191 child_fd,
192 }])?;
193
194 let process = smol::process::Command::from(command)
195 .stdin(Stdio::null())
196 .stdout(Stdio::piped())
197 .stderr(Stdio::piped())
198 .spawn()?;
199
200 let mut buffer = Vec::new();
201 reader.read_to_end(&mut buffer)?;
202
203 Ok((buffer, process.output().await?))
204}
205
206#[cfg(windows)]
207async fn capture_windows(
208 shell_path: &Path,
209 args: &[String],
210 directory: &Path,
211) -> Result<collections::HashMap<String, String>> {
212 use std::process::Stdio;
213
214 let app_path =
215 std::env::current_exe().context("Failed to determine current app executable path.")?;
216
217 let shell_kind = ShellKind::new(shell_path, true);
218 let directory_string = directory.display().to_string();
220 let directory_string = if directory_string.starts_with('-') {
221 format!("./{directory_string}")
222 } else {
223 directory_string
224 };
225 let app_path_string = app_path.display().to_string();
226 let quote_for_shell = |value: &str| {
227 shell_kind
228 .try_quote(value)
229 .map(|quoted| quoted.into_owned())
230 .context("unexpected null in directory name")
231 };
232 let mut cmd = crate::command::new_command(shell_path);
233 cmd.args(args);
234 let quoted_directory = quote_for_shell(&directory_string)?;
235 let quoted_app_path = quote_for_shell(&app_path_string)?;
236 let cmd = match shell_kind {
237 ShellKind::Csh
238 | ShellKind::Tcsh
239 | ShellKind::Rc
240 | ShellKind::Fish
241 | ShellKind::Xonsh
242 | ShellKind::Posix => cmd.args([
243 "-l",
244 "-i",
245 "-c",
246 &format!("cd {}; {} --printenv", quoted_directory, quoted_app_path),
247 ]),
248 ShellKind::PowerShell | ShellKind::Pwsh => cmd.args([
249 "-NonInteractive",
250 "-NoProfile",
251 "-Command",
252 &format!(
253 "Set-Location {}; & {} --printenv",
254 quoted_directory, quoted_app_path
255 ),
256 ]),
257 ShellKind::Elvish => cmd.args([
258 "-c",
259 &format!("cd {}; {} --printenv", quoted_directory, quoted_app_path),
260 ]),
261 ShellKind::Nushell => {
262 let app_command = shell_kind
263 .prepend_command_prefix("ed_app_path)
264 .into_owned();
265 cmd.args([
266 "-c",
267 &format!("cd {}; {} --printenv", quoted_directory, app_command),
268 ])
269 }
270 ShellKind::Cmd => {
271 let dir = directory_string.trim_end_matches('\\');
272 cmd.args(["/d", "/c", "cd", dir, "&&", &app_path_string, "--printenv"])
273 }
274 }
275 .stdin(Stdio::null())
276 .stdout(Stdio::piped())
277 .stderr(Stdio::piped());
278 let output = cmd
279 .output()
280 .await
281 .with_context(|| format!("command {cmd:?}"))?;
282 let env_output = String::from_utf8_lossy(&output.stdout);
283
284 parse_env_output(
285 &env_output,
286 &output.status,
287 || {
288 format!(
289 "Command {cmd:?} exited with {} but environment was captured successfully. stderr: {:?}",
290 output.status,
291 String::from_utf8_lossy(&output.stderr),
292 )
293 },
294 || {
295 format!(
296 "Command {cmd:?} failed with {}. stdout: {:?}, stderr: {:?}",
297 output.status,
298 String::from_utf8_lossy(&output.stdout),
299 String::from_utf8_lossy(&output.stderr),
300 )
301 },
302 )
303}
304
305#[cfg(test)]
306mod tests {
307 use std::process::ExitStatus;
308
309 use super::*;
310 use crate::path;
311
312 #[cfg(unix)]
313 fn exit_status(code: i32) -> ExitStatus {
314 use std::os::unix::process::ExitStatusExt;
315
316 ExitStatus::from_raw(code << 8)
317 }
318
319 #[cfg(windows)]
320 fn exit_status(code: u32) -> ExitStatus {
321 use std::os::windows::process::ExitStatusExt;
322
323 ExitStatus::from_raw(code)
324 }
325
326 #[test]
327 fn parse_env_output_accepts_valid_env_when_shell_exits_nonzero() {
328 let env_json = serde_json::json!({
329 "PATH": path!("/usr/bin"),
330 "SHELL": path!("/bin/zsh"),
331 });
332 let env_output = format!("shell startup noise\n{env_json}\nshell shutdown noise");
333
334 let env_map = parse_env_output(
335 &env_output,
336 &exit_status(1),
337 || "shell exited with 1 but environment was captured successfully".to_string(),
338 || panic!("failed capture error should not be evaluated for valid environment output"),
339 )
340 .expect("valid environment output should be returned despite non-zero shell exit");
341 assert_eq!(
342 env_map.get("PATH").map(String::as_str),
343 Some(path!("/usr/bin"))
344 );
345 assert_eq!(
346 env_map.get("SHELL").map(String::as_str),
347 Some(path!("/bin/zsh"))
348 );
349 }
350}