Skip to content

Commit 031cd0b

Browse files
committed
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. Discussed in: - #149070 - rust-lang/libs-team#194
1 parent 3d461af commit 031cd0b

File tree

8 files changed

+171
-2
lines changed

8 files changed

+171
-2
lines changed

library/std/src/process.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
mod tests;
162162

163163
use crate::convert::Infallible;
164-
use crate::ffi::OsStr;
164+
use crate::ffi::{OsStr, OsString};
165165
use crate::io::prelude::*;
166166
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut};
167167
use crate::num::NonZero;
@@ -1157,7 +1157,8 @@ impl Command {
11571157
/// [`Command::env_remove`] can be retrieved with this method.
11581158
///
11591159
/// Note that this output does not include environment variables inherited from the parent
1160-
/// process.
1160+
/// process. To see the full list of environment variables, including those inherited from the
1161+
/// parent process, use [`Command::get_resolved_envs`].
11611162
///
11621163
/// Each element is a tuple key/value pair `(&OsStr, Option<&OsStr>)`. A [`None`] value
11631164
/// indicates its key was explicitly removed via [`Command::env_remove`]. The associated key for
@@ -1186,6 +1187,42 @@ impl Command {
11861187
CommandEnvs { iter: self.inner.get_envs() }
11871188
}
11881189

1190+
/// Returns an iterator of the environment variables that will be set when the process is spawned.
1191+
///
1192+
/// This returns the environment as it would be if the command were executed at the time of calling
1193+
/// this method. The returned environment includes:
1194+
/// - All inherited environment variables from the parent process (unless [`Command::env_clear`] was called)
1195+
/// - All environment variables explicitly set via [`Command::env`] or [`Command::envs`]
1196+
/// - Excluding any environment variables removed via [`Command::env_remove`]
1197+
///
1198+
/// Note that the returned environment is a snapshot at the time this method is called and will not
1199+
/// reflect any subsequent changes to the `Command` or the parent process's environment. Additionally,
1200+
/// it will not reflect changes made in a `pre_exec` hook (on Unix platforms).
1201+
///
1202+
/// Each element is a tuple `(OsString, OsString)` representing an environment variable key and value.
1203+
///
1204+
/// # Examples
1205+
///
1206+
/// ```
1207+
/// #![feature(command_get_resolved_envs)]
1208+
/// use std::process::Command;
1209+
/// use std::ffi::{OsString, OsStr};
1210+
/// use std::env;
1211+
/// use std::collections::HashMap;
1212+
///
1213+
/// let mut cmd = Command::new("ls");
1214+
/// cmd.env("TZ", "UTC");
1215+
/// unsafe { env::set_var("EDITOR", "vim"); }
1216+
///
1217+
/// let resolved: HashMap<OsString, OsString> = cmd.get_resolved_envs().collect();
1218+
/// assert_eq!(resolved.get(OsStr::new("TZ")), Some(&OsString::from("UTC")));
1219+
/// assert_eq!(resolved.get(OsStr::new("EDITOR")), Some(&OsString::from("vim")));
1220+
/// ```
1221+
#[unstable(feature = "command_get_resolved_envs", issue = "149070")]
1222+
pub fn get_resolved_envs(&self) -> impl Iterator<Item = (OsString, OsString)> {
1223+
self.inner.get_resolved_envs()
1224+
}
1225+
11891226
/// Returns the working directory for the child process.
11901227
///
11911228
/// This returns [`None`] if the working directory will not be changed.

library/std/src/process/tests.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -667,3 +667,83 @@ fn terminate_exited_process() {
667667
assert!(p.kill().is_ok());
668668
assert!(p.kill().is_ok());
669669
}
670+
671+
#[test]
672+
fn test_get_resolved_envs() {
673+
use crate::env;
674+
use crate::ffi::OsString;
675+
676+
// Test 1: Basic environment with inheritance
677+
let mut cmd = Command::new("echo");
678+
cmd.env("TEST_VAR", "test_value");
679+
680+
let resolved: Vec<(OsString, OsString)> = cmd.get_resolved_envs().collect();
681+
682+
// Should contain our test var
683+
assert!(
684+
resolved.iter().any(|(k, v)| k == "TEST_VAR" && v == "test_value"),
685+
"TEST_VAR should be present in resolved environment"
686+
);
687+
688+
// Should also contain inherited vars (like PATH on most platforms)
689+
let current_env_count = env::vars_os().count();
690+
// We added one more, so should have at least as many
691+
assert!(
692+
resolved.len() >= current_env_count,
693+
"Resolved environment should include inherited variables"
694+
);
695+
696+
// Test 2: env_clear should only include explicitly set vars
697+
let mut cmd2 = Command::new("echo");
698+
cmd2.env_clear();
699+
cmd2.env("ONLY_VAR", "only_value");
700+
701+
let resolved2: Vec<(OsString, OsString)> = cmd2.get_resolved_envs().collect();
702+
703+
assert_eq!(resolved2.len(), 1, "After env_clear, only explicitly set vars should be present");
704+
assert_eq!(resolved2[0], (OsString::from("ONLY_VAR"), OsString::from("only_value")));
705+
706+
// Test 3: env_remove should exclude a variable
707+
let mut cmd3 = Command::new("echo");
708+
// Set a known variable and then remove it
709+
cmd3.env("TO_REMOVE", "value");
710+
cmd3.env_remove("TO_REMOVE");
711+
712+
let resolved3: Vec<(OsString, OsString)> = cmd3.get_resolved_envs().collect();
713+
714+
// Should not contain TO_REMOVE
715+
assert!(
716+
!resolved3.iter().any(|(k, _)| k == "TO_REMOVE"),
717+
"Removed variables should not appear in resolved environment"
718+
);
719+
720+
// Test 4: Overriding inherited variables
721+
let original_value = env::var_os("PATH");
722+
if original_value.is_some() {
723+
let mut cmd4 = Command::new("echo");
724+
cmd4.env("PATH", "custom_path");
725+
726+
let resolved4: Vec<(OsString, OsString)> = cmd4.get_resolved_envs().collect();
727+
728+
let path_entry = resolved4.iter().find(|(k, _)| k == "PATH");
729+
assert!(path_entry.is_some(), "PATH should be in resolved environment");
730+
assert_eq!(
731+
path_entry.unwrap().1,
732+
OsString::from("custom_path"),
733+
"PATH should have the overridden value"
734+
);
735+
}
736+
737+
// Test 5: Multiple operations combined
738+
let mut cmd5 = Command::new("echo");
739+
cmd5.env("VAR1", "value1");
740+
cmd5.env("VAR2", "value2");
741+
cmd5.env("VAR3", "value3");
742+
cmd5.env_remove("VAR2");
743+
744+
let resolved5: Vec<(OsString, OsString)> = cmd5.get_resolved_envs().collect();
745+
746+
assert!(resolved5.iter().any(|(k, v)| k == "VAR1" && v == "value1"));
747+
assert!(!resolved5.iter().any(|(k, _)| k == "VAR2"));
748+
assert!(resolved5.iter().any(|(k, v)| k == "VAR3" && v == "value3"));
749+
}

library/std/src/sys/process/env.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,35 @@ impl<'a> ExactSizeIterator for CommandEnvs<'a> {
113113
self.iter.is_empty()
114114
}
115115
}
116+
117+
/// An iterator over the fully resolved environment variables.
118+
///
119+
/// This struct is returned by `Command::get_resolved_envs`.
120+
#[derive(Debug)]
121+
pub struct ResolvedEnvs {
122+
inner: crate::collections::btree_map::IntoIter<EnvKey, OsString>,
123+
}
124+
125+
impl ResolvedEnvs {
126+
pub(crate) fn new(map: BTreeMap<EnvKey, OsString>) -> Self {
127+
Self { inner: map.into_iter() }
128+
}
129+
}
130+
131+
impl Iterator for ResolvedEnvs {
132+
type Item = (OsString, OsString);
133+
134+
fn next(&mut self) -> Option<Self::Item> {
135+
self.inner.next().map(|(key, value)| (key.into(), value))
136+
}
137+
138+
fn size_hint(&self) -> (usize, Option<usize>) {
139+
self.inner.size_hint()
140+
}
141+
}
142+
143+
impl ExactSizeIterator for ResolvedEnvs {
144+
fn len(&self) -> usize {
145+
self.inner.len()
146+
}
147+
}

library/std/src/sys/process/motor.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ impl Command {
9898
self.env.iter()
9999
}
100100

101+
pub fn get_resolved_envs(&self) -> super::env::ResolvedEnvs {
102+
super::env::ResolvedEnvs::new(self.env.capture())
103+
}
104+
101105
pub fn get_current_dir(&self) -> Option<&Path> {
102106
self.cwd.as_ref().map(Path::new)
103107
}

library/std/src/sys/process/uefi.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ impl Command {
8383
self.env.iter()
8484
}
8585

86+
pub fn get_resolved_envs(&self) -> super::env::ResolvedEnvs {
87+
super::env::ResolvedEnvs::new(self.env.capture())
88+
}
89+
8690
pub fn get_current_dir(&self) -> Option<&Path> {
8791
None
8892
}

library/std/src/sys/process/unix/common.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ impl Command {
263263
self.env.iter()
264264
}
265265

266+
pub fn get_resolved_envs(&self) -> crate::sys::process::env::ResolvedEnvs {
267+
crate::sys::process::env::ResolvedEnvs::new(self.env.capture())
268+
}
269+
266270
pub fn get_current_dir(&self) -> Option<&Path> {
267271
self.cwd.as_ref().map(|cs| Path::new(OsStr::from_bytes(cs.as_bytes())))
268272
}

library/std/src/sys/process/unsupported.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ impl Command {
8686
self.env.iter()
8787
}
8888

89+
pub fn get_resolved_envs(&self) -> super::env::ResolvedEnvs {
90+
super::env::ResolvedEnvs::new(self.env.capture())
91+
}
92+
8993
pub fn get_current_dir(&self) -> Option<&Path> {
9094
self.cwd.as_ref().map(|cs| Path::new(cs))
9195
}

library/std/src/sys/process/windows.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ impl Command {
250250
self.env.iter()
251251
}
252252

253+
pub fn get_resolved_envs(&self) -> super::env::ResolvedEnvs {
254+
super::env::ResolvedEnvs::new(self.env.capture())
255+
}
256+
253257
pub fn get_current_dir(&self) -> Option<&Path> {
254258
self.cwd.as_ref().map(Path::new)
255259
}

0 commit comments

Comments
 (0)