From 674da7de7fe08daef242d52f8fe8f75b68d20984 Mon Sep 17 00:00:00 2001 From: Schneems Date: Wed, 26 Nov 2025 16:28:41 -0600 Subject: [PATCH] Add Command::get_resolved_envs This addition allows an end-user to inspect the environment variables that are visible to the process when it boots. --- library/std/src/process.rs | 41 ++++++++++++++++++++-- library/std/src/sys/process/env.rs | 32 +++++++++++++++++ library/std/src/sys/process/motor.rs | 6 +++- library/std/src/sys/process/uefi.rs | 6 +++- library/std/src/sys/process/unix/common.rs | 6 +++- library/std/src/sys/process/unsupported.rs | 6 +++- library/std/src/sys/process/windows.rs | 6 +++- 7 files changed, 96 insertions(+), 7 deletions(-) diff --git a/library/std/src/process.rs b/library/std/src/process.rs index dbcf2684c6fb1..3796f6eaceed1 100644 --- a/library/std/src/process.rs +++ b/library/std/src/process.rs @@ -161,7 +161,7 @@ mod tests; use crate::convert::Infallible; -use crate::ffi::OsStr; +use crate::ffi::{OsStr, OsString}; use crate::io::prelude::*; use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut}; use crate::num::NonZero; @@ -1157,7 +1157,8 @@ impl Command { /// [`Command::env_remove`] can be retrieved with this method. /// /// Note that this output does not include environment variables inherited from the parent - /// process. + /// process. To see the full list of environment variables, including those inherited from the + /// parent process, use [`Command::get_resolved_envs`]. /// /// Each element is a tuple key/value pair `(&OsStr, Option<&OsStr>)`. A [`None`] value /// indicates its key was explicitly removed via [`Command::env_remove`]. The associated key for @@ -1186,6 +1187,42 @@ impl Command { CommandEnvs { iter: self.inner.get_envs() } } + /// Returns an iterator of the environment variables that will be set when the process is spawned. + /// + /// This returns the environment as it would be if the command were executed at the time of calling + /// this method. The returned environment includes: + /// - All inherited environment variables from the parent process (unless [`Command::env_clear`] was called) + /// - All environment variables explicitly set via [`Command::env`] or [`Command::envs`] + /// - Excluding any environment variables removed via [`Command::env_remove`] + /// + /// Note that the returned environment is a snapshot at the time this method is called and will not + /// reflect any subsequent changes to the `Command` or the parent process's environment. Additionally, + /// it will not reflect changes made in a `pre_exec` hook (on Unix platforms). + /// + /// Each element is a tuple `(OsString, OsString)` representing an environment variable key and value. + /// + /// # Examples + /// + /// ``` + /// #![feature(command_get_resolved_envs)] + /// use std::process::Command; + /// use std::ffi::{OsString, OsStr}; + /// use std::env; + /// use std::collections::HashMap; + /// + /// let mut cmd = Command::new("ls"); + /// cmd.env("TZ", "UTC"); + /// unsafe { env::set_var("EDITOR", "vim"); } + /// + /// let resolved: HashMap = cmd.get_resolved_envs().collect(); + /// assert_eq!(resolved.get(OsStr::new("TZ")), Some(&OsString::from("UTC"))); + /// assert_eq!(resolved.get(OsStr::new("EDITOR")), Some(&OsString::from("vim"))); + /// ``` + #[unstable(feature = "command_get_resolved_envs", issue = "149070")] + pub fn get_resolved_envs(&self) -> impl Iterator { + self.inner.get_resolved_envs() + } + /// Returns the working directory for the child process. /// /// This returns [`None`] if the working directory will not be changed. diff --git a/library/std/src/sys/process/env.rs b/library/std/src/sys/process/env.rs index e08b476540ef9..5fadf4fb0125b 100644 --- a/library/std/src/sys/process/env.rs +++ b/library/std/src/sys/process/env.rs @@ -113,3 +113,35 @@ impl<'a> ExactSizeIterator for CommandEnvs<'a> { self.iter.is_empty() } } + +/// An iterator over the fully resolved environment variables. +/// +/// This struct is returned by `Command::get_resolved_envs`. +#[derive(Debug)] +pub struct ResolvedEnvs { + inner: crate::collections::btree_map::IntoIter, +} + +impl ResolvedEnvs { + pub(crate) fn new(map: BTreeMap) -> Self { + Self { inner: map.into_iter() } + } +} + +impl Iterator for ResolvedEnvs { + type Item = (OsString, OsString); + + fn next(&mut self) -> Option { + self.inner.next().map(|(key, value)| (key.into(), value)) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +impl ExactSizeIterator for ResolvedEnvs { + fn len(&self) -> usize { + self.inner.len() + } +} diff --git a/library/std/src/sys/process/motor.rs b/library/std/src/sys/process/motor.rs index 949a9d4942901..58722e506153e 100644 --- a/library/std/src/sys/process/motor.rs +++ b/library/std/src/sys/process/motor.rs @@ -1,5 +1,5 @@ use super::CommandEnvs; -use super::env::CommandEnv; +use super::env::{CommandEnv, ResolvedEnvs}; use crate::ffi::OsStr; pub use crate::ffi::OsString as EnvKey; use crate::num::NonZeroI32; @@ -102,6 +102,10 @@ impl Command { self.env.does_clear() } + pub fn get_resolved_envs(&self) -> ResolvedEnvs { + ResolvedEnvs::new(self.env.capture()) + } + pub fn get_current_dir(&self) -> Option<&Path> { self.cwd.as_ref().map(Path::new) } diff --git a/library/std/src/sys/process/uefi.rs b/library/std/src/sys/process/uefi.rs index 8d44292611bcb..3eddec751e01c 100644 --- a/library/std/src/sys/process/uefi.rs +++ b/library/std/src/sys/process/uefi.rs @@ -1,6 +1,6 @@ use r_efi::protocols::{simple_text_input, simple_text_output}; -use super::env::{CommandEnv, CommandEnvs}; +use super::env::{CommandEnv, CommandEnvs, ResolvedEnvs}; use crate::collections::BTreeMap; pub use crate::ffi::OsString as EnvKey; use crate::ffi::{OsStr, OsString}; @@ -87,6 +87,10 @@ impl Command { self.env.does_clear() } + pub fn get_resolved_envs(&self) -> ResolvedEnvs { + ResolvedEnvs::new(self.env.capture()) + } + pub fn get_current_dir(&self) -> Option<&Path> { None } diff --git a/library/std/src/sys/process/unix/common.rs b/library/std/src/sys/process/unix/common.rs index 44d54aaf51512..21fe26180af5b 100644 --- a/library/std/src/sys/process/unix/common.rs +++ b/library/std/src/sys/process/unix/common.rs @@ -15,7 +15,7 @@ use crate::sys::fs::File; #[cfg(not(target_os = "fuchsia"))] use crate::sys::fs::OpenOptions; use crate::sys::pipe::{self, AnonPipe}; -use crate::sys::process::env::{CommandEnv, CommandEnvs}; +use crate::sys::process::env::{CommandEnv, CommandEnvs, ResolvedEnvs}; use crate::sys_common::{FromInner, IntoInner}; use crate::{fmt, io}; @@ -267,6 +267,10 @@ impl Command { self.env.does_clear() } + pub fn get_resolved_envs(&self) -> ResolvedEnvs { + ResolvedEnvs::new(self.env.capture()) + } + pub fn get_current_dir(&self) -> Option<&Path> { self.cwd.as_ref().map(|cs| Path::new(OsStr::from_bytes(cs.as_bytes()))) } diff --git a/library/std/src/sys/process/unsupported.rs b/library/std/src/sys/process/unsupported.rs index 2dfc676ec0059..a195767da9750 100644 --- a/library/std/src/sys/process/unsupported.rs +++ b/library/std/src/sys/process/unsupported.rs @@ -1,4 +1,4 @@ -use super::env::{CommandEnv, CommandEnvs}; +use super::env::{CommandEnv, CommandEnvs, ResolvedEnvs}; pub use crate::ffi::OsString as EnvKey; use crate::ffi::{OsStr, OsString}; use crate::num::NonZero; @@ -90,6 +90,10 @@ impl Command { self.env.does_clear() } + pub fn get_resolved_envs(&self) -> ResolvedEnvs { + ResolvedEnvs::new(self.env.capture()) + } + pub fn get_current_dir(&self) -> Option<&Path> { self.cwd.as_ref().map(|cs| Path::new(cs)) } diff --git a/library/std/src/sys/process/windows.rs b/library/std/src/sys/process/windows.rs index 6e8be21a1fa6d..af64b0d95c41d 100644 --- a/library/std/src/sys/process/windows.rs +++ b/library/std/src/sys/process/windows.rs @@ -5,7 +5,7 @@ mod tests; use core::ffi::c_void; -use super::env::{CommandEnv, CommandEnvs}; +use super::env::{CommandEnv, CommandEnvs, ResolvedEnvs}; use crate::collections::BTreeMap; use crate::env::consts::{EXE_EXTENSION, EXE_SUFFIX}; use crate::ffi::{OsStr, OsString}; @@ -254,6 +254,10 @@ impl Command { self.env.does_clear() } + pub fn get_resolved_envs(&self) -> ResolvedEnvs { + ResolvedEnvs::new(self.env.capture()) + } + pub fn get_current_dir(&self) -> Option<&Path> { self.cwd.as_ref().map(Path::new) }