1use std::path::Path;
  2
  3use anyhow::{Context as _, Result};
  4use collections::HashMap;
  5
  6use crate::shell::ShellKind;
  7
  8pub fn print_env() {
  9    let env_vars: HashMap<String, String> = std::env::vars().collect();
 10    let json = serde_json::to_string_pretty(&env_vars).unwrap_or_else(|err| {
 11        eprintln!("Error serializing environment variables: {}", err);
 12        std::process::exit(1);
 13    });
 14    println!("{}", json);
 15}
 16
 17/// Capture all environment variables from the login shell in the given directory.
 18pub async fn capture(
 19    shell_path: impl AsRef<Path>,
 20    args: &[String],
 21    directory: impl AsRef<Path>,
 22) -> Result<collections::HashMap<String, String>> {
 23    #[cfg(windows)]
 24    return capture_windows(shell_path.as_ref(), args, directory.as_ref()).await;
 25    #[cfg(unix)]
 26    return capture_unix(shell_path.as_ref(), args, directory.as_ref()).await;
 27}
 28
 29#[cfg(unix)]
 30async fn capture_unix(
 31    shell_path: &Path,
 32    args: &[String],
 33    directory: &Path,
 34) -> Result<collections::HashMap<String, String>> {
 35    use std::os::unix::process::CommandExt;
 36    use std::process::Stdio;
 37
 38    let shell_kind = ShellKind::new(shell_path, false);
 39    let zed_path = super::get_shell_safe_zed_path(shell_kind)?;
 40
 41    let mut command_string = String::new();
 42    let mut command = std::process::Command::new(shell_path);
 43    command.args(args);
 44    // In some shells, file descriptors greater than 2 cannot be used in interactive mode,
 45    // so file descriptor 0 (stdin) is used instead. This impacts zsh, old bash; perhaps others.
 46    // See: https://github.com/zed-industries/zed/pull/32136#issuecomment-2999645482
 47    const FD_STDIN: std::os::fd::RawFd = 0;
 48    const FD_STDOUT: std::os::fd::RawFd = 1;
 49    const FD_STDERR: std::os::fd::RawFd = 2;
 50
 51    let (fd_num, redir) = match shell_kind {
 52        ShellKind::Rc => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]`
 53        ShellKind::Nushell | ShellKind::Tcsh => (FD_STDOUT, "".to_string()),
 54        // xonsh doesn't support redirecting to stdin, and control sequences are printed to
 55        // stdout on startup
 56        ShellKind::Xonsh => (FD_STDERR, "o>e".to_string()),
 57        _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0`
 58    };
 59    command.stdin(Stdio::null());
 60    command.stdout(Stdio::piped());
 61    command.stderr(Stdio::piped());
 62
 63    match shell_kind {
 64        ShellKind::Csh | ShellKind::Tcsh => {
 65            // For csh/tcsh, login shell requires passing `-` as 0th argument (instead of `-l`)
 66            command.arg0("-");
 67        }
 68        ShellKind::Fish => {
 69            // in fish, asdf, direnv attach to the `fish_prompt` event
 70            command_string.push_str("emit fish_prompt;");
 71            command.arg("-l");
 72        }
 73        _ => {
 74            command.arg("-l");
 75        }
 76    }
 77    // cd into the directory, triggering directory specific side-effects (asdf, direnv, etc)
 78    command_string.push_str(&format!("cd '{}';", directory.display()));
 79    if let Some(prefix) = shell_kind.command_prefix() {
 80        command_string.push(prefix);
 81    }
 82    command_string.push_str(&format!("{} --printenv {}", zed_path, redir));
 83    command.args(["-i", "-c", &command_string]);
 84
 85    super::set_pre_exec_to_start_new_session(&mut command);
 86
 87    let (env_output, process_output) = spawn_and_read_fd(command, fd_num).await?;
 88    let env_output = String::from_utf8_lossy(&env_output);
 89
 90    anyhow::ensure!(
 91        process_output.status.success(),
 92        "login shell exited with {}. stdout: {:?}, stderr: {:?}",
 93        process_output.status,
 94        String::from_utf8_lossy(&process_output.stdout),
 95        String::from_utf8_lossy(&process_output.stderr),
 96    );
 97
 98    // Parse the JSON output from zed --printenv
 99    let env_map: collections::HashMap<String, String> = serde_json::from_str(&env_output)
100        .with_context(|| "Failed to deserialize environment variables from json")?;
101    Ok(env_map)
102}
103
104#[cfg(unix)]
105async fn spawn_and_read_fd(
106    mut command: std::process::Command,
107    child_fd: std::os::fd::RawFd,
108) -> anyhow::Result<(Vec<u8>, std::process::Output)> {
109    use command_fds::{CommandFdExt, FdMapping};
110    use std::io::Read;
111
112    let (mut reader, writer) = std::io::pipe()?;
113
114    command.fd_mappings(vec![FdMapping {
115        parent_fd: writer.into(),
116        child_fd,
117    }])?;
118
119    let process = smol::process::Command::from(command).spawn()?;
120
121    let mut buffer = Vec::new();
122    reader.read_to_end(&mut buffer)?;
123
124    Ok((buffer, process.output().await?))
125}
126
127#[cfg(windows)]
128async fn capture_windows(
129    shell_path: &Path,
130    _args: &[String],
131    directory: &Path,
132) -> Result<collections::HashMap<String, String>> {
133    use std::process::Stdio;
134
135    let zed_path =
136        std::env::current_exe().context("Failed to determine current zed executable path.")?;
137
138    let shell_kind = ShellKind::new(shell_path, true);
139    let env_output = match shell_kind {
140        ShellKind::Posix
141        | ShellKind::Csh
142        | ShellKind::Tcsh
143        | ShellKind::Rc
144        | ShellKind::Fish
145        | ShellKind::Xonsh => {
146            return Err(anyhow::anyhow!("unsupported shell kind"));
147        }
148        ShellKind::PowerShell => {
149            let output = crate::command::new_smol_command(shell_path)
150                .args([
151                    "-NonInteractive",
152                    "-NoProfile",
153                    "-Command",
154                    &format!(
155                        "Set-Location '{}'; & '{}' --printenv",
156                        directory.display(),
157                        zed_path.display()
158                    ),
159                ])
160                .stdin(Stdio::null())
161                .stdout(Stdio::piped())
162                .stderr(Stdio::piped())
163                .output()
164                .await?;
165
166            anyhow::ensure!(
167                output.status.success(),
168                "PowerShell command failed with {}. stdout: {:?}, stderr: {:?}",
169                output.status,
170                String::from_utf8_lossy(&output.stdout),
171                String::from_utf8_lossy(&output.stderr),
172            );
173            output
174        }
175        ShellKind::Nushell => {
176            let output = crate::command::new_smol_command(shell_path)
177                .args([
178                    "-c",
179                    &format!(
180                        "cd '{}'; {}{} --printenv",
181                        directory.display(),
182                        shell_kind
183                            .command_prefix()
184                            .map(|prefix| prefix.to_string())
185                            .unwrap_or_default(),
186                        zed_path.display()
187                    ),
188                ])
189                .stdin(Stdio::null())
190                .stdout(Stdio::piped())
191                .stderr(Stdio::piped())
192                .output()
193                .await?;
194
195            anyhow::ensure!(
196                output.status.success(),
197                "Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
198                output.status,
199                String::from_utf8_lossy(&output.stdout),
200                String::from_utf8_lossy(&output.stderr),
201            );
202            output
203        }
204        ShellKind::Cmd => {
205            let output = crate::command::new_smol_command(shell_path)
206                .args([
207                    "/c",
208                    &format!(
209                        "cd '{}'; {} --printenv",
210                        directory.display(),
211                        zed_path.display()
212                    ),
213                ])
214                .stdin(Stdio::null())
215                .stdout(Stdio::piped())
216                .stderr(Stdio::piped())
217                .output()
218                .await?;
219
220            anyhow::ensure!(
221                output.status.success(),
222                "Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
223                output.status,
224                String::from_utf8_lossy(&output.stdout),
225                String::from_utf8_lossy(&output.stderr),
226            );
227            output
228        }
229    };
230
231    let env_output = String::from_utf8_lossy(&env_output.stdout);
232
233    // Parse the JSON output from zed --printenv
234    serde_json::from_str(&env_output)
235        .with_context(|| "Failed to deserialize environment variables from json")
236}