terminals.rs

  1use crate::{Project, ProjectPath};
  2use anyhow::{Context as _, Result};
  3use collections::HashMap;
  4use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity};
  5use itertools::Itertools;
  6use language::LanguageName;
  7use remote::ssh_session::SshArgs;
  8use settings::{Settings, SettingsLocation};
  9use smol::channel::bounded;
 10use std::{
 11    borrow::Cow,
 12    env::{self},
 13    path::{Path, PathBuf},
 14    sync::Arc,
 15};
 16use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal};
 17use terminal::{
 18    TaskState, TaskStatus, Terminal, TerminalBuilder,
 19    terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings},
 20};
 21use util::{
 22    ResultExt,
 23    paths::{PathStyle, RemotePathBuf},
 24};
 25
 26/// The directory inside a Python virtual environment that contains executables
 27const PYTHON_VENV_BIN_DIR: &str = if cfg!(target_os = "windows") {
 28    "Scripts"
 29} else {
 30    "bin"
 31};
 32
 33pub struct Terminals {
 34    pub(crate) local_handles: Vec<WeakEntity<terminal::Terminal>>,
 35}
 36
 37/// Terminals are opened either for the users shell, or to run a task.
 38
 39#[derive(Debug)]
 40pub enum TerminalKind {
 41    /// Run a shell at the given path (or $HOME if None)
 42    Shell(Option<PathBuf>),
 43    /// Run a task.
 44    Task(SpawnInTerminal),
 45}
 46
 47/// SshCommand describes how to connect to a remote server
 48#[derive(Debug, Clone, PartialEq, Eq)]
 49pub struct SshCommand {
 50    pub arguments: Vec<String>,
 51}
 52
 53impl SshCommand {
 54    pub fn add_port_forwarding(&mut self, local_port: u16, host: String, remote_port: u16) {
 55        self.arguments.push("-L".to_string());
 56        self.arguments
 57            .push(format!("{}:{}:{}", local_port, host, remote_port));
 58    }
 59}
 60
 61pub struct SshDetails {
 62    pub host: String,
 63    pub ssh_command: SshCommand,
 64    pub envs: Option<HashMap<String, String>>,
 65    pub path_style: PathStyle,
 66}
 67
 68impl Project {
 69    pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
 70        let worktree = self
 71            .active_entry()
 72            .and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
 73            .into_iter()
 74            .chain(self.worktrees(cx))
 75            .find_map(|tree| tree.read(cx).root_dir());
 76        worktree
 77    }
 78
 79    pub fn first_project_directory(&self, cx: &App) -> Option<PathBuf> {
 80        let worktree = self.worktrees(cx).next()?;
 81        let worktree = worktree.read(cx);
 82        if worktree.root_entry()?.is_dir() {
 83            Some(worktree.abs_path().to_path_buf())
 84        } else {
 85            None
 86        }
 87    }
 88
 89    pub fn ssh_details(&self, cx: &App) -> Option<SshDetails> {
 90        if let Some(ssh_client) = &self.ssh_client {
 91            let ssh_client = ssh_client.read(cx);
 92            if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() {
 93                return Some(SshDetails {
 94                    host: ssh_client.connection_options().host.clone(),
 95                    ssh_command: SshCommand { arguments },
 96                    envs,
 97                    path_style,
 98                });
 99            }
100        }
101
102        None
103    }
104
105    pub fn create_terminal(
106        &mut self,
107        kind: TerminalKind,
108        cx: &mut Context<Self>,
109    ) -> Task<Result<Entity<Terminal>>> {
110        let path: Option<Arc<Path>> = match &kind {
111            TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
112            TerminalKind::Task(spawn_task) => {
113                if let Some(cwd) = &spawn_task.cwd {
114                    Some(Arc::from(cwd.as_ref()))
115                } else {
116                    self.active_project_directory(cx)
117                }
118            }
119        };
120
121        let mut settings_location = None;
122        if let Some(path) = path.as_ref()
123            && let Some((worktree, _)) = self.find_worktree(path, cx)
124        {
125            settings_location = Some(SettingsLocation {
126                worktree_id: worktree.read(cx).id(),
127                path,
128            });
129        }
130        let venv = TerminalSettings::get(settings_location, cx)
131            .detect_venv
132            .clone();
133
134        cx.spawn(async move |project, cx| {
135            let python_venv_directory = if let Some(path) = path {
136                project
137                    .update(cx, |this, cx| this.python_venv_directory(path, venv, cx))?
138                    .await
139            } else {
140                None
141            };
142            project.update(cx, |project, cx| {
143                project.create_terminal_with_venv(kind, python_venv_directory, cx)
144            })?
145        })
146    }
147
148    pub fn terminal_settings<'a>(
149        &'a self,
150        path: &'a Option<PathBuf>,
151        cx: &'a App,
152    ) -> &'a TerminalSettings {
153        let mut settings_location = None;
154        if let Some(path) = path.as_ref()
155            && let Some((worktree, _)) = self.find_worktree(path, cx)
156        {
157            settings_location = Some(SettingsLocation {
158                worktree_id: worktree.read(cx).id(),
159                path,
160            });
161        }
162        TerminalSettings::get(settings_location, cx)
163    }
164
165    pub fn exec_in_shell(&self, command: String, cx: &App) -> std::process::Command {
166        let path = self.first_project_directory(cx);
167        let ssh_details = self.ssh_details(cx);
168        let settings = self.terminal_settings(&path, cx).clone();
169
170        let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive();
171        let (command, args) = builder.build(Some(command), &Vec::new());
172
173        let mut env = self
174            .environment
175            .read(cx)
176            .get_cli_environment()
177            .unwrap_or_default();
178        env.extend(settings.env);
179
180        match self.ssh_details(cx) {
181            Some(SshDetails {
182                ssh_command,
183                envs,
184                path_style,
185                ..
186            }) => {
187                let (command, args) = wrap_for_ssh(
188                    &ssh_command,
189                    Some((&command, &args)),
190                    path.as_deref(),
191                    env,
192                    None,
193                    path_style,
194                );
195                let mut command = std::process::Command::new(command);
196                command.args(args);
197                if let Some(envs) = envs {
198                    command.envs(envs);
199                }
200                command
201            }
202            None => {
203                let mut command = std::process::Command::new(command);
204                command.args(args);
205                command.envs(env);
206                if let Some(path) = path {
207                    command.current_dir(path);
208                }
209                command
210            }
211        }
212    }
213
214    pub fn create_terminal_with_venv(
215        &mut self,
216        kind: TerminalKind,
217        python_venv_directory: Option<PathBuf>,
218        cx: &mut Context<Self>,
219    ) -> Result<Entity<Terminal>> {
220        let this = &mut *self;
221        let ssh_details = this.ssh_details(cx);
222        let path: Option<Arc<Path>> = match &kind {
223            TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())),
224            TerminalKind::Task(spawn_task) => {
225                if let Some(cwd) = &spawn_task.cwd {
226                    if ssh_details.is_some() {
227                        Some(Arc::from(cwd.as_ref()))
228                    } else {
229                        let cwd = cwd.to_string_lossy();
230                        let tilde_substituted = shellexpand::tilde(&cwd);
231                        Some(Arc::from(Path::new(tilde_substituted.as_ref())))
232                    }
233                } else {
234                    this.active_project_directory(cx)
235                }
236            }
237        };
238
239        let is_ssh_terminal = ssh_details.is_some();
240
241        let mut settings_location = None;
242        if let Some(path) = path.as_ref()
243            && let Some((worktree, _)) = this.find_worktree(path, cx)
244        {
245            settings_location = Some(SettingsLocation {
246                worktree_id: worktree.read(cx).id(),
247                path,
248            });
249        }
250        let settings = TerminalSettings::get(settings_location, cx).clone();
251
252        let (completion_tx, completion_rx) = bounded(1);
253
254        // Start with the environment that we might have inherited from the Zed CLI.
255        let mut env = this
256            .environment
257            .read(cx)
258            .get_cli_environment()
259            .unwrap_or_default();
260        // Then extend it with the explicit env variables from the settings, so they take
261        // precedence.
262        env.extend(settings.env);
263
264        let local_path = if is_ssh_terminal { None } else { path.clone() };
265
266        let mut python_venv_activate_command = Task::ready(None);
267
268        let (spawn_task, shell) = match kind {
269            TerminalKind::Shell(_) => {
270                if let Some(python_venv_directory) = &python_venv_directory {
271                    python_venv_activate_command = this.python_activate_command(
272                        python_venv_directory,
273                        &settings.detect_venv,
274                        &settings.shell,
275                        cx,
276                    );
277                }
278
279                match ssh_details {
280                    Some(SshDetails {
281                        host,
282                        ssh_command,
283                        envs,
284                        path_style,
285                    }) => {
286                        log::debug!("Connecting to a remote server: {ssh_command:?}");
287
288                        // Alacritty sets its terminfo to `alacritty`, this requiring hosts to have it installed
289                        // to properly display colors.
290                        // We do not have the luxury of assuming the host has it installed,
291                        // so we set it to a default that does not break the highlighting via ssh.
292                        env.entry("TERM".to_string())
293                            .or_insert_with(|| "xterm-256color".to_string());
294
295                        let (program, args) = wrap_for_ssh(
296                            &ssh_command,
297                            None,
298                            path.as_deref(),
299                            env,
300                            None,
301                            path_style,
302                        );
303                        env = HashMap::default();
304                        if let Some(envs) = envs {
305                            env.extend(envs);
306                        }
307                        (
308                            Option::<TaskState>::None,
309                            Shell::WithArguments {
310                                program,
311                                args,
312                                title_override: Some(format!("{} — Terminal", host).into()),
313                            },
314                        )
315                    }
316                    None => (None, settings.shell),
317                }
318            }
319            TerminalKind::Task(spawn_task) => {
320                let task_state = Some(TaskState {
321                    id: spawn_task.id,
322                    full_label: spawn_task.full_label,
323                    label: spawn_task.label,
324                    command_label: spawn_task.command_label,
325                    hide: spawn_task.hide,
326                    status: TaskStatus::Running,
327                    show_summary: spawn_task.show_summary,
328                    show_command: spawn_task.show_command,
329                    show_rerun: spawn_task.show_rerun,
330                    completion_rx,
331                });
332
333                env.extend(spawn_task.env);
334
335                if let Some(venv_path) = &python_venv_directory {
336                    env.insert(
337                        "VIRTUAL_ENV".to_string(),
338                        venv_path.to_string_lossy().to_string(),
339                    );
340                }
341
342                match ssh_details {
343                    Some(SshDetails {
344                        host,
345                        ssh_command,
346                        envs,
347                        path_style,
348                    }) => {
349                        log::debug!("Connecting to a remote server: {ssh_command:?}");
350                        env.entry("TERM".to_string())
351                            .or_insert_with(|| "xterm-256color".to_string());
352                        let (program, args) = wrap_for_ssh(
353                            &ssh_command,
354                            spawn_task
355                                .command
356                                .as_ref()
357                                .map(|command| (command, &spawn_task.args)),
358                            path.as_deref(),
359                            env,
360                            python_venv_directory.as_deref(),
361                            path_style,
362                        );
363                        env = HashMap::default();
364                        if let Some(envs) = envs {
365                            env.extend(envs);
366                        }
367                        (
368                            task_state,
369                            Shell::WithArguments {
370                                program,
371                                args,
372                                title_override: Some(format!("{} — Terminal", host).into()),
373                            },
374                        )
375                    }
376                    None => {
377                        if let Some(venv_path) = &python_venv_directory {
378                            add_environment_path(&mut env, &venv_path.join(PYTHON_VENV_BIN_DIR))
379                                .log_err();
380                        }
381
382                        let shell = if let Some(program) = spawn_task.command {
383                            Shell::WithArguments {
384                                program,
385                                args: spawn_task.args,
386                                title_override: None,
387                            }
388                        } else {
389                            Shell::System
390                        };
391                        (task_state, shell)
392                    }
393                }
394            }
395        };
396        TerminalBuilder::new(
397            local_path.map(|path| path.to_path_buf()),
398            python_venv_directory,
399            spawn_task,
400            shell,
401            env,
402            settings.cursor_shape.unwrap_or_default(),
403            settings.alternate_scroll,
404            settings.max_scroll_history_lines,
405            is_ssh_terminal,
406            cx.entity_id().as_u64(),
407            completion_tx,
408            cx,
409        )
410        .map(|builder| {
411            let terminal_handle = cx.new(|cx| builder.subscribe(cx));
412
413            this.terminals
414                .local_handles
415                .push(terminal_handle.downgrade());
416
417            let id = terminal_handle.entity_id();
418            cx.observe_release(&terminal_handle, move |project, _terminal, cx| {
419                let handles = &mut project.terminals.local_handles;
420
421                if let Some(index) = handles
422                    .iter()
423                    .position(|terminal| terminal.entity_id() == id)
424                {
425                    handles.remove(index);
426                    cx.notify();
427                }
428            })
429            .detach();
430
431            this.activate_python_virtual_environment(
432                python_venv_activate_command,
433                &terminal_handle,
434                cx,
435            );
436
437            terminal_handle
438        })
439    }
440
441    fn python_venv_directory(
442        &self,
443        abs_path: Arc<Path>,
444        venv_settings: VenvSettings,
445        cx: &Context<Project>,
446    ) -> Task<Option<PathBuf>> {
447        cx.spawn(async move |this, cx| {
448            if let Some((worktree, relative_path)) = this
449                .update(cx, |this, cx| this.find_worktree(&abs_path, cx))
450                .ok()?
451            {
452                let toolchain = this
453                    .update(cx, |this, cx| {
454                        this.active_toolchain(
455                            ProjectPath {
456                                worktree_id: worktree.read(cx).id(),
457                                path: relative_path.into(),
458                            },
459                            LanguageName::new("Python"),
460                            cx,
461                        )
462                    })
463                    .ok()?
464                    .await;
465
466                if let Some(toolchain) = toolchain {
467                    let toolchain_path = Path::new(toolchain.path.as_ref());
468                    return Some(toolchain_path.parent()?.parent()?.to_path_buf());
469                }
470            }
471            let venv_settings = venv_settings.as_option()?;
472            this.update(cx, move |this, cx| {
473                if let Some(path) = this.find_venv_in_worktree(&abs_path, &venv_settings, cx) {
474                    return Some(path);
475                }
476                this.find_venv_on_filesystem(&abs_path, &venv_settings, cx)
477            })
478            .ok()
479            .flatten()
480        })
481    }
482
483    fn find_venv_in_worktree(
484        &self,
485        abs_path: &Path,
486        venv_settings: &terminal_settings::VenvSettingsContent,
487        cx: &App,
488    ) -> Option<PathBuf> {
489        venv_settings
490            .directories
491            .iter()
492            .map(|name| abs_path.join(name))
493            .find(|venv_path| {
494                let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
495                self.find_worktree(&bin_path, cx)
496                    .and_then(|(worktree, relative_path)| {
497                        worktree.read(cx).entry_for_path(&relative_path)
498                    })
499                    .is_some_and(|entry| entry.is_dir())
500            })
501    }
502
503    fn find_venv_on_filesystem(
504        &self,
505        abs_path: &Path,
506        venv_settings: &terminal_settings::VenvSettingsContent,
507        cx: &App,
508    ) -> Option<PathBuf> {
509        let (worktree, _) = self.find_worktree(abs_path, cx)?;
510        let fs = worktree.read(cx).as_local()?.fs();
511        venv_settings
512            .directories
513            .iter()
514            .map(|name| abs_path.join(name))
515            .find(|venv_path| {
516                let bin_path = venv_path.join(PYTHON_VENV_BIN_DIR);
517                // One-time synchronous check is acceptable for terminal/task initialization
518                smol::block_on(fs.metadata(&bin_path))
519                    .ok()
520                    .flatten()
521                    .is_some_and(|meta| meta.is_dir)
522            })
523    }
524
525    fn activate_script_kind(shell: Option<&str>) -> ActivateScript {
526        let shell_env = std::env::var("SHELL").ok();
527        let shell_path = shell.or_else(|| shell_env.as_deref());
528        let shell = std::path::Path::new(shell_path.unwrap_or(""))
529            .file_name()
530            .and_then(|name| name.to_str())
531            .unwrap_or("");
532        match shell {
533            "fish" => ActivateScript::Fish,
534            "tcsh" => ActivateScript::Csh,
535            "nu" => ActivateScript::Nushell,
536            "powershell" | "pwsh" => ActivateScript::PowerShell,
537            _ => ActivateScript::Default,
538        }
539    }
540
541    fn python_activate_command(
542        &self,
543        venv_base_directory: &Path,
544        venv_settings: &VenvSettings,
545        shell: &Shell,
546        cx: &mut App,
547    ) -> Task<Option<String>> {
548        let Some(venv_settings) = venv_settings.as_option() else {
549            return Task::ready(None);
550        };
551        let activate_keyword = match venv_settings.activate_script {
552            terminal_settings::ActivateScript::Default => match std::env::consts::OS {
553                "windows" => ".",
554                _ => ".",
555            },
556            terminal_settings::ActivateScript::Nushell => "overlay use",
557            terminal_settings::ActivateScript::PowerShell => ".",
558            terminal_settings::ActivateScript::Pyenv => "pyenv",
559            _ => "source",
560        };
561        let script_kind =
562            if venv_settings.activate_script == terminal_settings::ActivateScript::Default {
563                match shell {
564                    Shell::Program(program) => Self::activate_script_kind(Some(program)),
565                    Shell::WithArguments {
566                        program,
567                        args: _,
568                        title_override: _,
569                    } => Self::activate_script_kind(Some(program)),
570                    Shell::System => Self::activate_script_kind(None),
571                }
572            } else {
573                venv_settings.activate_script
574            };
575
576        let activate_script_name = match script_kind {
577            terminal_settings::ActivateScript::Default
578            | terminal_settings::ActivateScript::Pyenv => "activate",
579            terminal_settings::ActivateScript::Csh => "activate.csh",
580            terminal_settings::ActivateScript::Fish => "activate.fish",
581            terminal_settings::ActivateScript::Nushell => "activate.nu",
582            terminal_settings::ActivateScript::PowerShell => "activate.ps1",
583        };
584
585        let line_ending = match std::env::consts::OS {
586            "windows" => "\r",
587            _ => "\n",
588        };
589
590        if venv_settings.venv_name.is_empty() {
591            let path = venv_base_directory
592                .join(PYTHON_VENV_BIN_DIR)
593                .join(activate_script_name)
594                .to_string_lossy()
595                .to_string();
596
597            let is_valid_path = self.resolve_abs_path(path.as_ref(), cx);
598            cx.background_spawn(async move {
599                let quoted = shlex::try_quote(&path).ok()?;
600                if is_valid_path.await.is_some_and(|meta| meta.is_file()) {
601                    Some(format!(
602                        "{} {} ; clear{}",
603                        activate_keyword, quoted, line_ending
604                    ))
605                } else {
606                    None
607                }
608            })
609        } else {
610            Task::ready(Some(format!(
611                "{activate_keyword} {activate_script_name} {name}; clear{line_ending}",
612                name = venv_settings.venv_name
613            )))
614        }
615    }
616
617    fn activate_python_virtual_environment(
618        &self,
619        command: Task<Option<String>>,
620        terminal_handle: &Entity<Terminal>,
621        cx: &mut App,
622    ) {
623        terminal_handle.update(cx, |_, cx| {
624            cx.spawn(async move |this, cx| {
625                if let Some(command) = command.await {
626                    this.update(cx, |this, _| {
627                        this.input(command.into_bytes());
628                    })
629                    .ok();
630                }
631            })
632            .detach()
633        });
634    }
635
636    pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {
637        &self.terminals.local_handles
638    }
639}
640
641pub fn wrap_for_ssh(
642    ssh_command: &SshCommand,
643    command: Option<(&String, &Vec<String>)>,
644    path: Option<&Path>,
645    env: HashMap<String, String>,
646    venv_directory: Option<&Path>,
647    path_style: PathStyle,
648) -> (String, Vec<String>) {
649    let to_run = if let Some((command, args)) = command {
650        // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped
651        let command: Option<Cow<str>> = if command == DEFAULT_REMOTE_SHELL {
652            Some(command.into())
653        } else {
654            shlex::try_quote(command).ok()
655        };
656        let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok());
657        command.into_iter().chain(args).join(" ")
658    } else {
659        "exec ${SHELL:-sh} -l".to_string()
660    };
661
662    let mut env_changes = String::new();
663    for (k, v) in env.iter() {
664        if let Some((k, v)) = shlex::try_quote(k).ok().zip(shlex::try_quote(v).ok()) {
665            env_changes.push_str(&format!("{}={} ", k, v));
666        }
667    }
668    if let Some(venv_directory) = venv_directory
669        && let Ok(str) = shlex::try_quote(venv_directory.to_string_lossy().as_ref())
670    {
671        let path = RemotePathBuf::new(PathBuf::from(str.to_string()), path_style).to_string();
672        env_changes.push_str(&format!("PATH={}:$PATH ", path));
673    }
674
675    let commands = if let Some(path) = path {
676        let path = RemotePathBuf::new(path.to_path_buf(), path_style).to_string();
677        // shlex will wrap the command in single quotes (''), disabling ~ expansion,
678        // replace ith with something that works
679        let tilde_prefix = "~/";
680        if path.starts_with(tilde_prefix) {
681            let trimmed_path = path
682                .trim_start_matches("/")
683                .trim_start_matches("~")
684                .trim_start_matches("/");
685
686            format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
687        } else {
688            format!("cd \"{path}\"; {env_changes} {to_run}")
689        }
690    } else {
691        format!("cd; {env_changes} {to_run}")
692    };
693    let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap());
694
695    let program = "ssh".to_string();
696    let mut args = ssh_command.arguments.clone();
697
698    args.push("-t".to_string());
699    args.push(shell_invocation);
700    (program, args)
701}
702
703fn add_environment_path(env: &mut HashMap<String, String>, new_path: &Path) -> Result<()> {
704    let mut env_paths = vec![new_path.to_path_buf()];
705    if let Some(path) = env.get("PATH").or(env::var("PATH").ok().as_ref()) {
706        let mut paths = std::env::split_paths(&path).collect::<Vec<_>>();
707        env_paths.append(&mut paths);
708    }
709
710    let paths = std::env::join_paths(env_paths).context("failed to create PATH env variable")?;
711    env.insert("PATH".to_string(), paths.to_string_lossy().to_string());
712
713    Ok(())
714}
715
716#[cfg(test)]
717mod tests {
718    use collections::HashMap;
719
720    #[test]
721    fn test_add_environment_path_with_existing_path() {
722        let tmp_path = std::path::PathBuf::from("/tmp/new");
723        let mut env = HashMap::default();
724        let old_path = if cfg!(windows) {
725            "/usr/bin;/usr/local/bin"
726        } else {
727            "/usr/bin:/usr/local/bin"
728        };
729        env.insert("PATH".to_string(), old_path.to_string());
730        env.insert("OTHER".to_string(), "aaa".to_string());
731
732        super::add_environment_path(&mut env, &tmp_path).unwrap();
733        if cfg!(windows) {
734            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", old_path));
735        } else {
736            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", old_path));
737        }
738        assert_eq!(env.get("OTHER").unwrap(), "aaa");
739    }
740
741    #[test]
742    fn test_add_environment_path_with_empty_path() {
743        let tmp_path = std::path::PathBuf::from("/tmp/new");
744        let mut env = HashMap::default();
745        env.insert("OTHER".to_string(), "aaa".to_string());
746        let os_path = std::env::var("PATH").unwrap();
747        super::add_environment_path(&mut env, &tmp_path).unwrap();
748        if cfg!(windows) {
749            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new;{}", os_path));
750        } else {
751            assert_eq!(env.get("PATH").unwrap(), &format!("/tmp/new:{}", os_path));
752        }
753        assert_eq!(env.get("OTHER").unwrap(), "aaa");
754    }
755}