Skip to main content

fc_gpui_util/
shell_env.rs

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 zed_path = super::get_shell_safe_zed_path()?;
39    let shell_kind = ShellKind::new(shell_path, false);
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                        zed_path.display()
183                    ),
184                ])
185                .stdin(Stdio::null())
186                .stdout(Stdio::piped())
187                .stderr(Stdio::piped())
188                .output()
189                .await?;
190
191            anyhow::ensure!(
192                output.status.success(),
193                "Nushell command failed with {}. stdout: {:?}, stderr: {:?}",
194                output.status,
195                String::from_utf8_lossy(&output.stdout),
196                String::from_utf8_lossy(&output.stderr),
197            );
198            output
199        }
200        ShellKind::Cmd => {
201            let output = crate::command::new_smol_command(shell_path)
202                .args([
203                    "/c",
204                    &format!(
205                        "cd '{}'; {} --printenv",
206                        directory.display(),
207                        zed_path.display()
208                    ),
209                ])
210                .stdin(Stdio::null())
211                .stdout(Stdio::piped())
212                .stderr(Stdio::piped())
213                .output()
214                .await?;
215
216            anyhow::ensure!(
217                output.status.success(),
218                "Cmd command failed with {}. stdout: {:?}, stderr: {:?}",
219                output.status,
220                String::from_utf8_lossy(&output.stdout),
221                String::from_utf8_lossy(&output.stderr),
222            );
223            output
224        }
225    };
226
227    let env_output = String::from_utf8_lossy(&env_output.stdout);
228
229    // Parse the JSON output from zed --printenv
230    serde_json::from_str(&env_output)
231        .with_context(|| "Failed to deserialize environment variables from json")
232}