1use std::{
2 collections::HashMap,
3 env, io,
4 process::{Command, ExitStatus}, path::PathBuf, ffi::OsString,
5};
6
7use clap::Parser;
8
9#[derive(Debug, Parser)]
10#[clap(author, version, about, long_about = None)]
11pub struct CommandArgs {
12 #[clap(short('e'), value_parser(parse_key_val::<String, String>))]
14 env_vars: Vec<(String, String)>,
15 #[clap(short('p'))]
17 path_additions: Vec<String>,
18 #[clap(required(true))]
20 program: String,
21 #[clap(last(true))]
23 args: Vec<String>,
24}
25
26pub(crate) mod helpers {
27 use std::ffi::OsString;
28
29 pub fn expand_argfile_path(arg: String) -> OsString {
31 if arg.starts_with(argfile::PREFIX) {
33 let path = arg.strip_prefix(argfile::PREFIX).expect("Already checked for this prefix");
35 if let Ok(expanded) = shellexpand::full(&path) {
37 return OsString::from(format!("{}{}", argfile::PREFIX, expanded))
39 }
40 }
41 arg.into()
43 }
44 pub fn is_argfile_comment(arg: &OsString) -> bool {
46 arg.to_string_lossy().starts_with('#')
47 }
48}
49
50impl CommandArgs {
51 pub fn get_all_args() -> Result<Self, io::Error> {
53 let args_plain_iter = env::args()
55 .map(helpers::expand_argfile_path);
56
57 let mut args_with_argfile = argfile::expand_args_from(
59 args_plain_iter,
60 argfile::parse_fromfile,
61 argfile::PREFIX,
62 )?;
63
64 args_with_argfile = args_with_argfile
66 .into_iter()
67 .filter(|arg| !helpers::is_argfile_comment(arg))
68 .collect();
69
70 let mut command_args: Self = Self::parse_from(args_with_argfile);
72 command_args.env_vars.iter_mut().for_each(|(k, _v)| {
74 *k = k.trim().to_string();
75 });
76 command_args.path_additions.iter_mut().for_each(|path| {
78 *path = path.trim().to_string();
79 });
80 Ok(command_args)
81 }
82 fn get_prepended_path(&self, env_vars: &Option<HashMap<String, String>>) -> Option<OsString> {
84 if self.path_additions.is_empty() {
85 return None;
86 }
87 let mut path_additions: Vec<String> = self.path_additions
89 .iter()
90 .map(|s| {
91 let abso_path = dunce::simplified(&PathBuf::from(s))
92 .to_string_lossy()
93 .trim()
95 .to_string();
96
97 let expanded = shellexpand::full_with_context_no_errors::<String, _, _, PathBuf, _>(
99 &abso_path,
100 || {None},
101 |var_name| {
102 if let Some(env_map) = &env_vars {
103 env_map.get(var_name)
105 }
106 else {
107 None
108 }
109 }
110 );
111 expanded.to_string()
112 })
113 .collect();
114 let original_path: Vec<String> = env::split_paths(&env::var("PATH").unwrap_or_default())
116 .map(|path| path.to_string_lossy().to_string())
118 .collect();
119
120 path_additions.extend(original_path);
122
123 let new_path = env::join_paths(path_additions).expect("could not join paths");
125 Some(new_path)
126 }
127
128 fn get_env_vars(&self) -> HashMap<String, String> {
129 let mut env_vars_hashmap = HashMap::new();
130 for (var_name, value) in self.env_vars.clone() {
131 let expanded = shellexpand::full_with_context_no_errors::<String, _, _, PathBuf, _>(
132 &value,
133 dirs::home_dir,
134 |key| {
135 env_vars_hashmap.get(key)
136 }
137 );
138 env_vars_hashmap.insert(var_name, expanded.to_string());
139 }
140 env_vars_hashmap
141 }
142
143 fn get_arg_list(&self) -> Vec<String> {
144 let (_, flag) = get_shell_and_flag();
145 let mut args = vec![flag.to_string(), self.program.clone()];
146 args.extend(self.args.clone());
147 args
148 }
149}
150
151fn parse_key_val<T, U>(
154 s: &str,
155) -> Result<(T, U), Box<dyn std::error::Error + Send + Sync + 'static>>
156where
157 T: std::str::FromStr,
158 T::Err: std::error::Error + Send + Sync + 'static,
159 U: std::str::FromStr,
160 U::Err: std::error::Error + Send + Sync + 'static,
161{
162 let pos = s
163 .find('=')
164 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
165 Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
166}
167
168pub fn init_ctrlc_handler() -> Result<(), ctrlc::Error> {
172 ctrlc::set_handler(move || {
174 })
176}
177
178pub fn run(command_args: &CommandArgs) -> Result<ExitStatus, io::Error> {
180 let hash_map_vars: HashMap<String, String> = command_args.get_env_vars();
182
183 let (shell, _) = get_shell_and_flag();
185 let final_args = command_args.get_arg_list();
187
188 let mut command = Command::new(shell);
190 command
192 .args(&final_args)
193 .envs(&hash_map_vars);
195
196 if let Some(new_path) = command_args.get_prepended_path(&Some(hash_map_vars)) {
198 command.env("PATH", new_path);
200 }
201
202 command.status()
204}
205
206
207
208const fn get_shell_and_flag<'a>() -> (&'a str, &'a str) {
210 if cfg!(windows) {
211 ("powershell", "-Command")
212 } else {
213 ("bash", "-c")
214 }
215}