use std::{collections::HashMap, path::Path, process::Output};
use crate::{Error, Result};
pub struct Opts<'a> {
pub pwd: &'a Path,
pub capture: bool,
pub sh: bool,
pub reset_env: bool,
}
const ENV_OK: &[&str] = &[
"USER",
"HOME",
"PATH",
"TMPDIR",
"SHELL",
"SSH_AUTH_SOCK",
"LANG",
"LC_ALL",
"TEMPDIR",
"TERM",
"COLORTERM",
"LOGNAME",
];
pub fn cmd(cmdstr: &str, env_kvs: &[(String, String)], opts: &Opts<'_>) -> Result<Output> {
let words = if opts.sh {
shell_command_argv(cmdstr.into())
} else {
shell_words::split(cmdstr)?.iter().map(Into::into).collect()
};
cmd_slice(
words
.iter()
.map(String::as_str)
.collect::<Vec<_>>()
.as_slice(),
env_kvs,
opts,
)
}
fn cmd_slice(words: &[&str], env_kvs: &[(String, String)], opts: &Opts<'_>) -> Result<Output> {
let mut env_map: HashMap<_, _> = if opts.reset_env {
std::env::vars()
.filter(|(k, _)| ENV_OK.contains(&k.as_str()))
.collect()
} else {
std::env::vars().collect()
};
for (k, v) in env_kvs {
env_map.insert(k.clone(), v.clone());
}
let (first, rest) = words
.split_first()
.ok_or_else(|| Error::Message("command has not enough arguments".to_string()))?;
let mut expr = duct::cmd(Path::new(first), rest)
.dir(opts.pwd)
.full_env(&env_map);
if opts.capture {
expr = expr.stdout_capture();
}
Ok(expr.run()?)
}
#[cfg(unix)]
fn shell_command_argv(command: String) -> Vec<String> {
use std::env;
let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into());
vec![shell, "-c".into(), command]
}
#[cfg(windows)]
fn shell_command_argv(command: String) -> Vec<String> {
let comspec = std::env::var_os("COMSPEC")
.and_then(|s| s.into_string().ok())
.unwrap_or_else(|| "cmd.exe".into());
vec![comspec, "/C".into(), command]
}
#[cfg(test)]
mod tests {
use std::path::Path;
use insta::assert_debug_snapshot;
use teller_providers::config::ProviderInfo;
use teller_providers::config::KV;
use teller_providers::providers::ProviderKind;
use super::cmd;
use super::Opts;
#[test]
#[cfg(not(windows))]
fn run_echo() {
let out = cmd(
"echo $MY_VAR",
&std::iter::once(&KV::from_literal(
"/foo/bar",
"MY_VAR",
"shazam",
ProviderInfo {
kind: ProviderKind::Inmem,
name: "test".to_string(),
},
))
.map(|kv| (kv.key.clone(), kv.value.clone()))
.collect::<Vec<_>>(),
&Opts {
pwd: Path::new("."),
capture: true,
reset_env: true,
sh: true,
},
)
.unwrap();
let s = String::from_utf8_lossy(&out.stdout[..]);
assert_debug_snapshot!(s);
}
#[ignore]
#[test]
fn env_reset() {
let out = cmd(
"/usr/bin/env",
&std::iter::once(&KV::from_literal(
"/foo/bar",
"MY_VAR",
"shazam",
ProviderInfo {
kind: ProviderKind::Inmem,
name: "test".to_string(),
},
))
.map(|kv| (kv.key.clone(), kv.value.clone()))
.collect::<Vec<_>>(),
&Opts {
pwd: Path::new("."),
capture: true,
reset_env: false, sh: false,
},
)
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout[..]).to_string();
assert!(stdout.contains("GITHUB_TOKEN="));
let out = cmd(
"/usr/bin/env",
&std::iter::once(&KV::from_literal(
"/foo/bar",
"MY_VAR",
"shazam",
ProviderInfo {
kind: ProviderKind::Inmem,
name: "test".to_string(),
},
))
.map(|kv| (kv.key.clone(), kv.value.clone()))
.collect::<Vec<_>>(),
&Opts {
pwd: Path::new("."),
capture: true,
reset_env: true, sh: false,
},
)
.unwrap();
let stdout = String::from_utf8_lossy(&out.stdout[..]).to_string();
assert!(stdout.contains("USER="));
assert!(stdout.contains("PATH="));
assert!(!stdout.contains("GITHUB_TOKEN="));
}
}