use std::{
collections::HashMap,
env, io,
process::{Command, ExitStatus}, path::PathBuf, ffi::OsString,
};
use clap::Parser;
#[derive(Debug, Parser)]
#[clap(author, version, about, long_about = None)]
pub struct CommandArgs {
#[clap(short('e'), value_parser(parse_key_val::<String, String>))]
env_vars: Vec<(String, String)>,
#[clap(short('p'))]
path_additions: Vec<String>,
#[clap(required(true))]
program: String,
#[clap(last(true))]
args: Vec<String>,
}
pub(crate) mod helpers {
use std::ffi::OsString;
pub fn expand_argfile_path(arg: String) -> OsString {
if arg.starts_with(argfile::PREFIX) {
let path = arg.strip_prefix(argfile::PREFIX).expect("Already checked for this prefix");
if let Ok(expanded) = shellexpand::full(&path) {
return OsString::from(format!("{}{}", argfile::PREFIX, expanded))
}
}
arg.into()
}
pub fn is_argfile_comment(arg: &OsString) -> bool {
arg.to_string_lossy().starts_with('#')
}
}
impl CommandArgs {
pub fn get_all_args() -> Result<Self, io::Error> {
let args_plain_iter = env::args()
.map(helpers::expand_argfile_path);
let mut args_with_argfile = argfile::expand_args_from(
args_plain_iter,
argfile::parse_fromfile,
argfile::PREFIX,
)?;
args_with_argfile = args_with_argfile
.into_iter()
.filter(|arg| !helpers::is_argfile_comment(arg))
.collect();
let mut command_args: Self = Self::parse_from(args_with_argfile);
command_args.env_vars.iter_mut().for_each(|(k, _v)| {
*k = k.trim().to_string();
});
command_args.path_additions.iter_mut().for_each(|path| {
*path = path.trim().to_string();
});
Ok(command_args)
}
fn get_prepended_path(&self, env_vars: &Option<HashMap<String, String>>) -> Option<OsString> {
if self.path_additions.is_empty() {
return None;
}
let mut path_additions: Vec<String> = self.path_additions
.iter()
.map(|s| {
let abso_path = dunce::simplified(&PathBuf::from(s))
.to_string_lossy()
.trim()
.to_string();
let expanded = shellexpand::full_with_context_no_errors::<String, _, _, PathBuf, _>(
&abso_path,
|| {None},
|var_name| {
if let Some(env_map) = &env_vars {
env_map.get(var_name)
}
else {
None
}
}
);
expanded.to_string()
})
.collect();
let original_path: Vec<String> = env::split_paths(&env::var("PATH").unwrap_or_default())
.map(|path| path.to_string_lossy().to_string())
.collect();
path_additions.extend(original_path);
let new_path = env::join_paths(path_additions).expect("could not join paths");
Some(new_path)
}
fn get_env_vars(&self) -> HashMap<String, String> {
let mut env_vars_hashmap = HashMap::new();
for (var_name, value) in self.env_vars.clone() {
let expanded = shellexpand::full_with_context_no_errors::<String, _, _, PathBuf, _>(
&value,
dirs::home_dir,
|key| {
env_vars_hashmap.get(key)
}
);
env_vars_hashmap.insert(var_name, expanded.to_string());
}
env_vars_hashmap
}
fn get_arg_list(&self) -> Vec<String> {
let (_, flag) = get_shell_and_flag();
let mut args = vec![flag.to_string(), self.program.clone()];
args.extend(self.args.clone());
args
}
}
fn parse_key_val<T, U>(
s: &str,
) -> Result<(T, U), Box<dyn std::error::Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: std::error::Error + Send + Sync + 'static,
{
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
pub fn init_ctrlc_handler() -> Result<(), ctrlc::Error> {
ctrlc::set_handler(move || {
})
}
pub fn run(command_args: &CommandArgs) -> Result<ExitStatus, io::Error> {
let hash_map_vars: HashMap<String, String> = command_args.get_env_vars();
let (shell, _) = get_shell_and_flag();
let final_args = command_args.get_arg_list();
let mut command = Command::new(shell);
command
.args(&final_args)
.envs(&hash_map_vars);
if let Some(new_path) = command_args.get_prepended_path(&Some(hash_map_vars)) {
command.env("PATH", new_path);
}
command.status()
}
const fn get_shell_and_flag<'a>() -> (&'a str, &'a str) {
if cfg!(windows) {
("powershell", "-Command")
} else {
("bash", "-c")
}
}