use std::ffi::OsStr;
pub const INHERITED_VARIABLE: &str = "PATH";
pub trait DeclaredCommandWorld {
fn close_standard_input(&mut self);
fn clear_environment(&mut self);
fn set_variable(&mut self, name: &OsStr, value: &OsStr);
}
impl DeclaredCommandWorld for std::process::Command {
fn close_standard_input(&mut self) {
self.stdin(std::process::Stdio::null());
}
fn clear_environment(&mut self) {
self.env_clear();
}
fn set_variable(&mut self, name: &OsStr, value: &OsStr) {
self.env(name, value);
}
}
impl DeclaredCommandWorld for tokio::process::Command {
fn close_standard_input(&mut self) {
self.stdin(std::process::Stdio::null());
}
fn clear_environment(&mut self) {
self.env_clear();
}
fn set_variable(&mut self, name: &OsStr, value: &OsStr) {
self.env(name, value);
}
}
pub fn place_in_declared_world<'binding, C, E>(command: &mut C, exports: E)
where
C: DeclaredCommandWorld + ?Sized,
E: IntoIterator<Item = (&'binding str, &'binding str)>,
{
command.close_standard_input();
command.clear_environment();
if let Some(path) = std::env::var_os(INHERITED_VARIABLE) {
command.set_variable(OsStr::new(INHERITED_VARIABLE), &path);
}
for (name, value) in exports {
command.set_variable(OsStr::new(name), OsStr::new(value));
}
}
#[cfg(test)]
mod tests {
use std::ffi::OsStr;
use super::{DeclaredCommandWorld, INHERITED_VARIABLE, place_in_declared_world};
#[derive(Default)]
struct Recorder {
steps: Vec<String>,
}
impl DeclaredCommandWorld for Recorder {
fn close_standard_input(&mut self) {
self.steps.push("stdin-closed".to_owned());
}
fn clear_environment(&mut self) {
self.steps.push("environment-cleared".to_owned());
}
fn set_variable(&mut self, name: &OsStr, value: &OsStr) {
self.steps.push(format!(
"{}={}",
name.to_string_lossy(),
value.to_string_lossy()
));
}
}
#[test]
fn the_world_is_stdin_closed_then_a_cleared_environment_then_path_then_the_exports() {
let mut recorder = Recorder::default();
place_in_declared_world(&mut recorder, [("GREETING", "hello"), ("SEAT", "calliope")]);
let mut expected = vec!["stdin-closed".to_owned(), "environment-cleared".to_owned()];
if let Some(path) = std::env::var_os(INHERITED_VARIABLE) {
expected.push(format!("PATH={}", path.to_string_lossy()));
}
expected.push("GREETING=hello".to_owned());
expected.push("SEAT=calliope".to_owned());
assert_eq!(recorder.steps, expected);
}
#[test]
fn an_exported_path_is_applied_after_the_inherited_one() {
let mut recorder = Recorder::default();
place_in_declared_world(&mut recorder, [("PATH", "/usr/bin:/bin")]);
let last = recorder.steps.last().map(String::as_str);
assert_eq!(last, Some("PATH=/usr/bin:/bin"));
assert_eq!(
recorder
.steps
.iter()
.filter(|step| step.starts_with("PATH="))
.count(),
if std::env::var_os(INHERITED_VARIABLE).is_some() {
2
} else {
1
},
"the inherited PATH is set first and the export after it: {:?}",
recorder.steps
);
}
#[test]
fn a_name_bound_twice_ends_on_the_later_binding() {
let mut recorder = Recorder::default();
place_in_declared_world(&mut recorder, [("SEAT", "first"), ("SEAT", "second")]);
let bindings: Vec<&str> = recorder
.steps
.iter()
.filter(|step| step.starts_with("SEAT="))
.map(String::as_str)
.collect();
assert_eq!(bindings, vec!["SEAT=first", "SEAT=second"]);
}
}