portable_pty/
cmdbuilder.rs

1#[cfg(unix)]
2use anyhow::Context;
3#[cfg(feature = "serde_support")]
4use serde_derive::*;
5use std::collections::BTreeMap;
6use std::ffi::{OsStr, OsString};
7#[cfg(windows)]
8use std::os::windows::ffi::OsStrExt;
9#[cfg(unix)]
10use std::path::Component;
11use std::path::Path;
12
13/// Used to deal with Windows having case-insensitive environment variables.
14#[derive(Clone, Debug, PartialEq, PartialOrd)]
15#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
16struct EnvEntry {
17    /// Whether or not this environment variable came from the base environment,
18    /// as opposed to having been explicitly set by the caller.
19    is_from_base_env: bool,
20
21    /// For case-insensitive platforms, the environment variable key in its preferred casing.
22    preferred_key: OsString,
23
24    /// The environment variable value.
25    value: OsString,
26}
27
28impl EnvEntry {
29    fn map_key(k: OsString) -> OsString {
30        if cfg!(windows) {
31            // Best-effort lowercase transformation of an os string
32            match k.to_str() {
33                Some(s) => s.to_lowercase().into(),
34                None => k,
35            }
36        } else {
37            k
38        }
39    }
40}
41
42#[cfg(unix)]
43fn get_shell() -> String {
44    use nix::unistd::{access, AccessFlags};
45    use std::ffi::CStr;
46    use std::str;
47
48    let ent = unsafe { libc::getpwuid(libc::getuid()) };
49    if !ent.is_null() {
50        let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
51        match shell.to_str().map(str::to_owned) {
52            Err(err) => {
53                log::warn!(
54                    "passwd database shell could not be \
55                     represented as utf-8: {err:#}, \
56                     falling back to /bin/sh"
57                );
58            }
59            Ok(shell) => {
60                if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) {
61                    log::warn!(
62                        "passwd database shell={shell:?} which is \
63                         not executable ({err:#}), falling back to /bin/sh"
64                    );
65                } else {
66                    return shell;
67                }
68            }
69        }
70    }
71    "/bin/sh".into()
72}
73
74fn get_base_env() -> BTreeMap<OsString, EnvEntry> {
75    let mut env: BTreeMap<OsString, EnvEntry> = std::env::vars_os()
76        .map(|(key, value)| {
77            (
78                EnvEntry::map_key(key.clone()),
79                EnvEntry {
80                    is_from_base_env: true,
81                    preferred_key: key,
82                    value,
83                },
84            )
85        })
86        .collect();
87
88    #[cfg(unix)]
89    {
90        let key = EnvEntry::map_key("SHELL".into());
91        // Only set the value of SHELL if it isn't already set
92        if !env.contains_key(&key) {
93            env.insert(
94                EnvEntry::map_key("SHELL".into()),
95                EnvEntry {
96                    is_from_base_env: true,
97                    preferred_key: "SHELL".into(),
98                    value: get_shell().into(),
99                },
100            );
101        }
102    }
103
104    #[cfg(windows)]
105    {
106        use std::os::windows::ffi::OsStringExt;
107        use winapi::um::processenv::ExpandEnvironmentStringsW;
108        use winreg::enums::{RegType, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
109        use winreg::types::FromRegValue;
110        use winreg::{RegKey, RegValue};
111
112        fn reg_value_to_string(value: &RegValue) -> anyhow::Result<OsString> {
113            match value.vtype {
114                RegType::REG_EXPAND_SZ => {
115                    let src = unsafe {
116                        std::slice::from_raw_parts(
117                            value.bytes.as_ptr() as *const u16,
118                            value.bytes.len() / 2,
119                        )
120                    };
121                    let size =
122                        unsafe { ExpandEnvironmentStringsW(src.as_ptr(), std::ptr::null_mut(), 0) };
123                    let mut buf = vec![0u16; size as usize + 1];
124                    unsafe {
125                        ExpandEnvironmentStringsW(src.as_ptr(), buf.as_mut_ptr(), buf.len() as u32)
126                    };
127
128                    let mut buf = buf.as_slice();
129                    while let Some(0) = buf.last() {
130                        buf = &buf[0..buf.len() - 1];
131                    }
132                    Ok(OsString::from_wide(buf))
133                }
134                _ => Ok(OsString::from_reg_value(value)?),
135            }
136        }
137
138        if let Ok(sys_env) = RegKey::predef(HKEY_LOCAL_MACHINE)
139            .open_subkey("System\\CurrentControlSet\\Control\\Session Manager\\Environment")
140        {
141            for res in sys_env.enum_values() {
142                if let Ok((name, value)) = res {
143                    if name.to_ascii_lowercase() == "username" {
144                        continue;
145                    }
146                    if let Ok(value) = reg_value_to_string(&value) {
147                        log::trace!("adding SYS env: {:?} {:?}", name, value);
148                        env.insert(
149                            EnvEntry::map_key(name.clone().into()),
150                            EnvEntry {
151                                is_from_base_env: true,
152                                preferred_key: name.into(),
153                                value,
154                            },
155                        );
156                    }
157                }
158            }
159        }
160
161        if let Ok(sys_env) = RegKey::predef(HKEY_CURRENT_USER).open_subkey("Environment") {
162            for res in sys_env.enum_values() {
163                if let Ok((name, value)) = res {
164                    if let Ok(value) = reg_value_to_string(&value) {
165                        // Merge the system and user paths together
166                        let value = if name.to_ascii_lowercase() == "path" {
167                            match env.get(&EnvEntry::map_key(name.clone().into())) {
168                                Some(entry) => {
169                                    let mut result = OsString::new();
170                                    result.push(&entry.value);
171                                    result.push(";");
172                                    result.push(&value);
173                                    result
174                                }
175                                None => value,
176                            }
177                        } else {
178                            value
179                        };
180
181                        log::trace!("adding USER env: {:?} {:?}", name, value);
182                        env.insert(
183                            EnvEntry::map_key(name.clone().into()),
184                            EnvEntry {
185                                is_from_base_env: true,
186                                preferred_key: name.into(),
187                                value,
188                            },
189                        );
190                    }
191                }
192            }
193        }
194    }
195
196    env
197}
198
199/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
200/// The interface is intentionally similar to that of `std::process::Command`.
201#[derive(Clone, Debug, PartialEq)]
202#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
203pub struct CommandBuilder {
204    args: Vec<OsString>,
205    envs: BTreeMap<OsString, EnvEntry>,
206    cwd: Option<OsString>,
207    #[cfg(unix)]
208    pub(crate) umask: Option<libc::mode_t>,
209    controlling_tty: bool,
210}
211
212impl CommandBuilder {
213    /// Create a new builder instance with argv\[0\] set to the specified
214    /// program.
215    pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
216        Self {
217            args: vec![program.as_ref().to_owned()],
218            envs: get_base_env(),
219            cwd: None,
220            #[cfg(unix)]
221            umask: None,
222            controlling_tty: true,
223        }
224    }
225
226    /// Create a new builder instance from a pre-built argument vector
227    pub fn from_argv(args: Vec<OsString>) -> Self {
228        Self {
229            args,
230            envs: get_base_env(),
231            cwd: None,
232            #[cfg(unix)]
233            umask: None,
234            controlling_tty: true,
235        }
236    }
237
238    /// Set whether we should set the pty as the controlling terminal.
239    /// The default is true, which is usually what you want, but you
240    /// may need to set this to false if you are crossing container
241    /// boundaries (eg: flatpak) to workaround issues like:
242    /// <https://github.com/flatpak/flatpak/issues/3697>
243    /// <https://github.com/flatpak/flatpak/issues/3285>
244    pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
245        self.controlling_tty = controlling_tty;
246    }
247
248    pub fn get_controlling_tty(&self) -> bool {
249        self.controlling_tty
250    }
251
252    /// Create a new builder instance that will run some idea of a default
253    /// program.  Such a builder will panic if `arg` is called on it.
254    pub fn new_default_prog() -> Self {
255        Self {
256            args: vec![],
257            envs: get_base_env(),
258            cwd: None,
259            #[cfg(unix)]
260            umask: None,
261            controlling_tty: true,
262        }
263    }
264
265    /// Returns true if this builder was created via `new_default_prog`
266    pub fn is_default_prog(&self) -> bool {
267        self.args.is_empty()
268    }
269
270    /// Append an argument to the current command line.
271    /// Will panic if called on a builder created via `new_default_prog`.
272    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
273        if self.is_default_prog() {
274            panic!("attempted to add args to a default_prog builder");
275        }
276        self.args.push(arg.as_ref().to_owned());
277    }
278
279    /// Append a sequence of arguments to the current command line
280    pub fn args<I, S>(&mut self, args: I)
281    where
282        I: IntoIterator<Item = S>,
283        S: AsRef<OsStr>,
284    {
285        for arg in args {
286            self.arg(arg);
287        }
288    }
289
290    pub fn get_argv(&self) -> &Vec<OsString> {
291        &self.args
292    }
293
294    pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
295        &mut self.args
296    }
297
298    /// Override the value of an environmental variable
299    pub fn env<K, V>(&mut self, key: K, value: V)
300    where
301        K: AsRef<OsStr>,
302        V: AsRef<OsStr>,
303    {
304        let key: OsString = key.as_ref().into();
305        let value: OsString = value.as_ref().into();
306        self.envs.insert(
307            EnvEntry::map_key(key.clone()),
308            EnvEntry {
309                is_from_base_env: false,
310                preferred_key: key,
311                value: value,
312            },
313        );
314    }
315
316    pub fn env_remove<K>(&mut self, key: K)
317    where
318        K: AsRef<OsStr>,
319    {
320        let key = key.as_ref().into();
321        self.envs.remove(&EnvEntry::map_key(key));
322    }
323
324    pub fn env_clear(&mut self) {
325        self.envs.clear();
326    }
327
328    pub fn get_env<K>(&self, key: K) -> Option<&OsStr>
329    where
330        K: AsRef<OsStr>,
331    {
332        let key = key.as_ref().into();
333        self.envs.get(&EnvEntry::map_key(key)).map(
334            |EnvEntry {
335                 is_from_base_env: _,
336                 preferred_key: _,
337                 value,
338             }| value.as_os_str(),
339        )
340    }
341
342    pub fn cwd<D>(&mut self, dir: D)
343    where
344        D: AsRef<OsStr>,
345    {
346        self.cwd = Some(dir.as_ref().to_owned());
347    }
348
349    pub fn clear_cwd(&mut self) {
350        self.cwd.take();
351    }
352
353    pub fn get_cwd(&self) -> Option<&OsString> {
354        self.cwd.as_ref()
355    }
356
357    /// Iterate over the configured environment. Only includes environment
358    /// variables set by the caller via `env`, not variables set in the base
359    /// environment.
360    pub fn iter_extra_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
361        self.envs.values().filter_map(
362            |EnvEntry {
363                 is_from_base_env,
364                 preferred_key,
365                 value,
366             }| {
367                if *is_from_base_env {
368                    None
369                } else {
370                    let key = preferred_key.to_str()?;
371                    let value = value.to_str()?;
372                    Some((key, value))
373                }
374            },
375        )
376    }
377
378    pub fn iter_full_env_as_str(&self) -> impl Iterator<Item = (&str, &str)> {
379        self.envs.values().filter_map(
380            |EnvEntry {
381                 preferred_key,
382                 value,
383                 ..
384             }| {
385                let key = preferred_key.to_str()?;
386                let value = value.to_str()?;
387                Some((key, value))
388            },
389        )
390    }
391
392    /// Return the configured command and arguments as a single string,
393    /// quoted per the unix shell conventions.
394    pub fn as_unix_command_line(&self) -> anyhow::Result<String> {
395        let mut strs = vec![];
396        for arg in &self.args {
397            let s = arg
398                .to_str()
399                .ok_or_else(|| anyhow::anyhow!("argument cannot be represented as utf8"))?;
400            strs.push(s);
401        }
402        Ok(shell_words::join(strs))
403    }
404}
405
406#[cfg(unix)]
407impl CommandBuilder {
408    pub fn umask(&mut self, mask: Option<libc::mode_t>) {
409        self.umask = mask;
410    }
411
412    fn resolve_path(&self) -> Option<&OsStr> {
413        self.get_env("PATH")
414    }
415
416    fn search_path(&self, exe: &OsStr, cwd: &OsStr) -> anyhow::Result<OsString> {
417        use nix::unistd::{access, AccessFlags};
418
419        let exe_path: &Path = exe.as_ref();
420        if exe_path.is_relative() {
421            let cwd: &Path = cwd.as_ref();
422            let mut errors = vec![];
423
424            // If the requested executable is explicitly relative to cwd,
425            // then check only there.
426            if is_cwd_relative_path(exe_path) {
427                let abs_path = cwd.join(exe_path);
428
429                if abs_path.is_dir() {
430                    anyhow::bail!(
431                        "Unable to spawn {} because it is a directory",
432                        abs_path.display()
433                    );
434                } else if access(&abs_path, AccessFlags::X_OK).is_ok() {
435                    return Ok(abs_path.into_os_string());
436                } else if access(&abs_path, AccessFlags::F_OK).is_ok() {
437                    anyhow::bail!(
438                        "Unable to spawn {} because it is not executable",
439                        abs_path.display()
440                    );
441                }
442
443                anyhow::bail!(
444                    "Unable to spawn {} because it does not exist",
445                    abs_path.display()
446                );
447            }
448
449            if let Some(path) = self.resolve_path() {
450                for path in std::env::split_paths(&path) {
451                    let candidate = cwd.join(&path).join(&exe);
452
453                    if candidate.is_dir() {
454                        errors.push(format!("{} exists but is a directory", candidate.display()));
455                    } else if access(&candidate, AccessFlags::X_OK).is_ok() {
456                        return Ok(candidate.into_os_string());
457                    } else if access(&candidate, AccessFlags::F_OK).is_ok() {
458                        errors.push(format!(
459                            "{} exists but is not executable",
460                            candidate.display()
461                        ));
462                    }
463                }
464                errors.push(format!("No viable candidates found in PATH {path:?}"));
465            } else {
466                errors.push("Unable to resolve the PATH".to_string());
467            }
468            anyhow::bail!(
469                "Unable to spawn {} because:\n{}",
470                exe_path.display(),
471                errors.join(".\n")
472            );
473        } else if exe_path.is_dir() {
474            anyhow::bail!(
475                "Unable to spawn {} because it is a directory",
476                exe_path.display()
477            );
478        } else {
479            if let Err(err) = access(exe_path, AccessFlags::X_OK) {
480                if access(exe_path, AccessFlags::F_OK).is_ok() {
481                    anyhow::bail!(
482                        "Unable to spawn {} because it is not executable ({err:#})",
483                        exe_path.display()
484                    );
485                } else {
486                    anyhow::bail!(
487                        "Unable to spawn {} because it doesn't exist on the filesystem ({err:#})",
488                        exe_path.display()
489                    );
490                }
491            }
492
493            Ok(exe.to_owned())
494        }
495    }
496
497    /// Convert the CommandBuilder to a `std::process::Command` instance.
498    pub(crate) fn as_command(&self) -> anyhow::Result<std::process::Command> {
499        use std::os::unix::process::CommandExt;
500
501        let home = self.get_home_dir()?;
502        let dir: &OsStr = self
503            .cwd
504            .as_ref()
505            .map(|dir| dir.as_os_str())
506            .filter(|dir| std::path::Path::new(dir).is_dir())
507            .unwrap_or(home.as_ref());
508        let shell = self.get_shell();
509
510        let mut cmd = if self.is_default_prog() {
511            let mut cmd = std::process::Command::new(&shell);
512
513            // Run the shell as a login shell by prefixing the shell's
514            // basename with `-` and setting that as argv0
515            let basename = shell.rsplit('/').next().unwrap_or(&shell);
516            cmd.arg0(&format!("-{}", basename));
517            cmd
518        } else {
519            let resolved = self.search_path(&self.args[0], dir)?;
520            let mut cmd = std::process::Command::new(&resolved);
521            cmd.arg0(&self.args[0]);
522            cmd.args(&self.args[1..]);
523            cmd
524        };
525
526        cmd.current_dir(dir);
527
528        cmd.env_clear();
529        cmd.env("SHELL", shell);
530        cmd.envs(self.envs.values().map(
531            |EnvEntry {
532                 is_from_base_env: _,
533                 preferred_key,
534                 value,
535             }| (preferred_key.as_os_str(), value.as_os_str()),
536        ));
537
538        Ok(cmd)
539    }
540
541    /// Determine which shell to run.
542    /// We take the contents of the $SHELL env var first, then
543    /// fall back to looking it up from the password database.
544    pub fn get_shell(&self) -> String {
545        use nix::unistd::{access, AccessFlags};
546
547        if let Some(shell) = self.get_env("SHELL").and_then(OsStr::to_str) {
548            match access(shell, AccessFlags::X_OK) {
549                Ok(()) => return shell.into(),
550                Err(err) => log::warn!(
551                    "$SHELL -> {shell:?} which is \
552                     not executable ({err:#}), falling back to password db lookup"
553                ),
554            }
555        }
556
557        get_shell().into()
558    }
559
560    fn get_home_dir(&self) -> anyhow::Result<String> {
561        if let Some(home_dir) = self.get_env("HOME").and_then(OsStr::to_str) {
562            return Ok(home_dir.into());
563        }
564
565        let ent = unsafe { libc::getpwuid(libc::getuid()) };
566        if ent.is_null() {
567            Ok("/".into())
568        } else {
569            use std::ffi::CStr;
570            use std::str;
571            let home = unsafe { CStr::from_ptr((*ent).pw_dir) };
572            home.to_str()
573                .map(str::to_owned)
574                .context("failed to resolve home dir")
575        }
576    }
577}
578
579#[cfg(windows)]
580impl CommandBuilder {
581    fn search_path(&self, exe: &OsStr) -> OsString {
582        if let Some(path) = self.get_env("PATH") {
583            let extensions = self.get_env("PATHEXT").unwrap_or(OsStr::new(".EXE"));
584            for path in std::env::split_paths(&path) {
585                // Check for exactly the user's string in this path dir
586                let candidate = path.join(&exe);
587                if candidate.exists() {
588                    return candidate.into_os_string();
589                }
590
591                // otherwise try tacking on some extensions.
592                // Note that this really replaces the extension in the
593                // user specified path, so this is potentially wrong.
594                for ext in std::env::split_paths(&extensions) {
595                    // PATHEXT includes the leading `.`, but `with_extension`
596                    // doesn't want that
597                    let ext = ext.to_str().expect("PATHEXT entries must be utf8");
598                    let path = path.join(&exe).with_extension(&ext[1..]);
599                    if path.exists() {
600                        return path.into_os_string();
601                    }
602                }
603            }
604        }
605
606        exe.to_owned()
607    }
608
609    pub(crate) fn current_directory(&self) -> Option<Vec<u16>> {
610        let home: Option<&OsStr> = self
611            .get_env("USERPROFILE")
612            .filter(|path| Path::new(path).is_dir());
613        let cwd: Option<&OsStr> = self.cwd.as_deref().filter(|path| Path::new(path).is_dir());
614        let dir: Option<&OsStr> = cwd.or(home);
615
616        dir.map(|dir| {
617            let mut wide = vec![];
618
619            if Path::new(dir).is_relative() {
620                if let Ok(ccwd) = std::env::current_dir() {
621                    wide.extend(ccwd.join(dir).as_os_str().encode_wide());
622                } else {
623                    wide.extend(dir.encode_wide());
624                }
625            } else {
626                wide.extend(dir.encode_wide());
627            }
628
629            wide.push(0);
630            wide
631        })
632    }
633
634    /// Constructs an environment block for this spawn attempt.
635    /// Uses the current process environment as the base and then
636    /// adds/replaces the environment that was specified via the
637    /// `env` methods.
638    pub(crate) fn environment_block(&self) -> Vec<u16> {
639        // encode the environment as wide characters
640        let mut block = vec![];
641
642        for EnvEntry {
643            is_from_base_env: _,
644            preferred_key,
645            value,
646        } in self.envs.values()
647        {
648            block.extend(preferred_key.encode_wide());
649            block.push(b'=' as u16);
650            block.extend(value.encode_wide());
651            block.push(0);
652        }
653        // and a final terminator for CreateProcessW
654        block.push(0);
655
656        block
657    }
658
659    pub fn get_shell(&self) -> String {
660        let exe: OsString = self
661            .get_env("ComSpec")
662            .unwrap_or(OsStr::new("cmd.exe"))
663            .into();
664        exe.into_string()
665            .unwrap_or_else(|_| "%CompSpec%".to_string())
666    }
667
668    pub(crate) fn cmdline(&self) -> anyhow::Result<(Vec<u16>, Vec<u16>)> {
669        let mut cmdline = Vec::<u16>::new();
670
671        let exe: OsString = if self.is_default_prog() {
672            self.get_env("ComSpec")
673                .unwrap_or(OsStr::new("cmd.exe"))
674                .into()
675        } else {
676            self.search_path(&self.args[0])
677        };
678
679        Self::append_quoted(&exe, &mut cmdline);
680
681        // Ensure that we nul terminate the module name, otherwise we'll
682        // ask CreateProcessW to start something random!
683        let mut exe: Vec<u16> = exe.encode_wide().collect();
684        exe.push(0);
685
686        for arg in self.args.iter().skip(1) {
687            cmdline.push(' ' as u16);
688            anyhow::ensure!(
689                !arg.encode_wide().any(|c| c == 0),
690                "invalid encoding for command line argument {:?}",
691                arg
692            );
693            Self::append_quoted(arg, &mut cmdline);
694        }
695        // Ensure that the command line is nul terminated too!
696        cmdline.push(0);
697        Ok((exe, cmdline))
698    }
699
700    // Borrowed from https://github.com/hniksic/rust-subprocess/blob/873dfed165173e52907beb87118b2c0c05d8b8a1/src/popen.rs#L1117
701    // which in turn was translated from ArgvQuote at http://tinyurl.com/zmgtnls
702    fn append_quoted(arg: &OsStr, cmdline: &mut Vec<u16>) {
703        if !arg.is_empty()
704            && !arg.encode_wide().any(|c| {
705                c == ' ' as u16
706                    || c == '\t' as u16
707                    || c == '\n' as u16
708                    || c == '\x0b' as u16
709                    || c == '\"' as u16
710            })
711        {
712            cmdline.extend(arg.encode_wide());
713            return;
714        }
715        cmdline.push('"' as u16);
716
717        let arg: Vec<_> = arg.encode_wide().collect();
718        let mut i = 0;
719        while i < arg.len() {
720            let mut num_backslashes = 0;
721            while i < arg.len() && arg[i] == '\\' as u16 {
722                i += 1;
723                num_backslashes += 1;
724            }
725
726            if i == arg.len() {
727                for _ in 0..num_backslashes * 2 {
728                    cmdline.push('\\' as u16);
729                }
730                break;
731            } else if arg[i] == b'"' as u16 {
732                for _ in 0..num_backslashes * 2 + 1 {
733                    cmdline.push('\\' as u16);
734                }
735                cmdline.push(arg[i]);
736            } else {
737                for _ in 0..num_backslashes {
738                    cmdline.push('\\' as u16);
739                }
740                cmdline.push(arg[i]);
741            }
742            i += 1;
743        }
744        cmdline.push('"' as u16);
745    }
746}
747
748#[cfg(unix)]
749/// Returns true if the path begins with `./` or `../`
750fn is_cwd_relative_path<P: AsRef<Path>>(p: P) -> bool {
751    matches!(
752        p.as_ref().components().next(),
753        Some(Component::CurDir | Component::ParentDir)
754    )
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760
761    #[cfg(unix)]
762    #[test]
763    fn test_cwd_relative() {
764        assert!(is_cwd_relative_path("."));
765        assert!(is_cwd_relative_path("./foo"));
766        assert!(is_cwd_relative_path("../foo"));
767        assert!(!is_cwd_relative_path("foo"));
768        assert!(!is_cwd_relative_path("/foo"));
769    }
770
771    #[test]
772    fn test_env() {
773        let mut cmd = CommandBuilder::new("dummy");
774        let package_authors = cmd.get_env("CARGO_PKG_AUTHORS");
775        println!("package_authors: {:?}", package_authors);
776        assert!(package_authors == Some(OsStr::new("Wez Furlong")));
777
778        cmd.env("foo key", "foo value");
779        cmd.env("bar key", "bar value");
780
781        let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
782        println!("iterated_envs: {:?}", iterated_envs);
783        assert!(iterated_envs == vec![("bar key", "bar value"), ("foo key", "foo value")]);
784
785        {
786            let mut cmd = cmd.clone();
787            cmd.env_remove("foo key");
788
789            let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
790            println!("iterated_envs: {:?}", iterated_envs);
791            assert!(iterated_envs == vec![("bar key", "bar value")]);
792        }
793
794        {
795            let mut cmd = cmd.clone();
796            cmd.env_remove("bar key");
797
798            let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
799            println!("iterated_envs: {:?}", iterated_envs);
800            assert!(iterated_envs == vec![("foo key", "foo value")]);
801        }
802
803        {
804            let mut cmd = cmd.clone();
805            cmd.env_clear();
806
807            let iterated_envs = cmd.iter_extra_env_as_str().collect::<Vec<_>>();
808            println!("iterated_envs: {:?}", iterated_envs);
809            assert!(iterated_envs.is_empty());
810        }
811    }
812
813    #[cfg(windows)]
814    #[test]
815    fn test_env_case_insensitive_override() {
816        let mut cmd = CommandBuilder::new("dummy");
817        cmd.env("Cargo_Pkg_Authors", "Not Wez");
818        assert!(cmd.get_env("cargo_pkg_authors") == Some(OsStr::new("Not Wez")));
819
820        cmd.env_remove("cARGO_pKG_aUTHORS");
821        assert!(cmd.get_env("CARGO_PKG_AUTHORS").is_none());
822    }
823}