1use std::collections::HashMap;
17use std::env;
18use std::path::{Path, PathBuf};
19
20pub use crate::ansi::{AnsiConfig, AnsiUtils};
22pub use crate::quote::quote;
23pub use crate::trace::{is_trace_enabled, trace, trace_lazy};
24
25#[cfg(unix)]
31pub(crate) fn with_exported_process_context(
32 command: &str,
33 env: Option<&HashMap<String, String>>,
34) -> String {
35 let Some(env) = env else {
36 return command.to_string();
37 };
38 let assignments = ["PWD", "OLDPWD"]
39 .into_iter()
40 .filter_map(|name| {
41 env.get(name).map(|value| {
42 let value = value.replace('\'', "'\\''");
43 format!("{name}='{value}'")
44 })
45 })
46 .collect::<Vec<_>>();
47
48 if assignments.is_empty() {
49 command.to_string()
50 } else {
51 format!("export {}; {command}", assignments.join(" "))
52 }
53}
54
55#[cfg(not(unix))]
56pub(crate) fn with_exported_process_context(
57 command: &str,
58 _env: Option<&HashMap<String, String>>,
59) -> String {
60 command.to_string()
61}
62
63#[derive(Debug, Clone)]
64struct ShellConfig {
65 cmd: String,
66 args: Vec<String>,
67 raw_command_arg: bool,
68}
69
70fn find_available_shell() -> ShellConfig {
71 #[cfg(windows)]
72 let shells: &[(&str, &[&str], bool)] = &[
73 (r"C:\Program Files\Git\bin\bash.exe", &["-c"], false),
74 (r"C:\Program Files\Git\usr\bin\bash.exe", &["-c"], false),
75 (r"C:\Program Files (x86)\Git\bin\bash.exe", &["-c"], false),
76 ("bash.exe", &["-c"], false),
77 ("wsl.exe", &["bash", "-c"], false),
78 ("powershell.exe", &["-Command"], false),
79 ("pwsh.exe", &["-Command"], false),
80 ("cmd.exe", &["/c"], true),
81 ];
82
83 #[cfg(not(windows))]
84 let shells: &[(&str, &[&str], bool)] = &[
85 ("/bin/sh", &["-c"], false),
86 ("/usr/bin/sh", &["-c"], false),
87 ("/bin/bash", &["-c"], false),
88 ("sh", &["-c"], false),
89 ];
90
91 for (cmd, args, raw_command_arg) in shells {
92 if Path::new(cmd).exists() || which::which(cmd).is_ok() {
93 return ShellConfig {
94 cmd: (*cmd).to_string(),
95 args: args.iter().map(|arg| (*arg).to_string()).collect(),
96 raw_command_arg: *raw_command_arg,
97 };
98 }
99 }
100
101 #[cfg(windows)]
102 return ShellConfig {
103 cmd: "cmd.exe".to_string(),
104 args: vec!["/c".to_string()],
105 raw_command_arg: true,
106 };
107
108 #[cfg(not(windows))]
109 ShellConfig {
110 cmd: "/bin/sh".to_string(),
111 args: vec!["-c".to_string()],
112 raw_command_arg: false,
113 }
114}
115
116#[cfg(windows)]
117fn append_command_arg(process: &mut tokio::process::Command, command: &str, raw_command_arg: bool) {
118 if raw_command_arg {
119 use std::os::windows::process::CommandExt;
124 process.as_std_mut().raw_arg(format!("\"{command}\""));
125 } else {
126 process.arg(command);
127 }
128}
129
130#[cfg(not(windows))]
131fn append_command_arg(
132 process: &mut tokio::process::Command,
133 command: &str,
134 _raw_command_arg: bool,
135) {
136 process.arg(command);
137}
138
139pub(crate) fn shell_command(
141 command: &str,
142 env: Option<&HashMap<String, String>>,
143) -> tokio::process::Command {
144 let shell = find_available_shell();
145 let mut process = tokio::process::Command::new(&shell.cmd);
146 process.args(&shell.args);
147 let command = with_exported_process_context(command, env);
148 append_command_arg(&mut process, &command, shell.raw_command_arg);
149 process
150}
151
152#[derive(Debug, Clone)]
154pub struct CommandResult {
155 pub stdout: String,
156 pub stderr: String,
157 pub code: i32,
158}
159
160impl CommandResult {
161 pub fn success(stdout: impl Into<String>) -> Self {
163 CommandResult {
164 stdout: stdout.into(),
165 stderr: String::new(),
166 code: 0,
167 }
168 }
169
170 pub fn success_empty() -> Self {
172 CommandResult {
173 stdout: String::new(),
174 stderr: String::new(),
175 code: 0,
176 }
177 }
178
179 pub fn error(stderr: impl Into<String>) -> Self {
181 CommandResult {
182 stdout: String::new(),
183 stderr: stderr.into(),
184 code: 1,
185 }
186 }
187
188 pub fn error_with_code(stderr: impl Into<String>, code: i32) -> Self {
190 CommandResult {
191 stdout: String::new(),
192 stderr: stderr.into(),
193 code,
194 }
195 }
196
197 pub fn is_success(&self) -> bool {
199 self.code == 0
200 }
201
202 pub fn exit_code(&self) -> i32 {
207 self.code
208 }
209
210 pub fn error_for_status(self) -> crate::Result<CommandResult> {
227 if self.is_success() {
228 return Ok(self);
229 }
230
231 Err(crate::Error::command_failed(
232 self.code,
233 format!("Command failed with exit code {}", self.code),
234 ))
235 }
236}
237
238pub struct VirtualUtils;
240
241impl VirtualUtils {
242 pub fn missing_operand_error(command_name: &str) -> CommandResult {
244 CommandResult::error(format!("{}: missing operand", command_name))
245 }
246
247 pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
249 CommandResult::error(format!("{}: {}", command_name, message))
250 }
251
252 pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
254 CommandResult::error(format!("{}: {}", command_name, message))
255 }
256
257 pub fn success(stdout: impl Into<String>) -> CommandResult {
259 CommandResult::success(stdout)
260 }
261
262 pub fn error(stderr: impl Into<String>) -> CommandResult {
264 CommandResult::error(stderr)
265 }
266
267 pub fn validate_args(
269 args: &[String],
270 min_count: usize,
271 command_name: &str,
272 ) -> Option<CommandResult> {
273 if args.len() < min_count {
274 if min_count == 1 {
275 return Some(Self::missing_operand_error(command_name));
276 } else {
277 return Some(Self::invalid_argument_error(
278 command_name,
279 &format!("requires at least {} arguments", min_count),
280 ));
281 }
282 }
283 None }
285
286 pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf {
288 let path = Path::new(file_path);
289 if path.is_absolute() {
290 path.to_path_buf()
291 } else {
292 let base_path = cwd
293 .map(|p| p.to_path_buf())
294 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
295 base_path.join(path)
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_command_result_success() {
306 let result = CommandResult::success("hello");
307 assert!(result.is_success());
308 assert_eq!(result.stdout, "hello");
309 assert_eq!(result.stderr, "");
310 assert_eq!(result.code, 0);
311 }
312
313 #[test]
314 fn test_command_result_error() {
315 let result = CommandResult::error("something went wrong");
316 assert!(!result.is_success());
317 assert_eq!(result.stdout, "");
318 assert_eq!(result.stderr, "something went wrong");
319 assert_eq!(result.code, 1);
320 }
321
322 #[test]
323 fn test_command_result_error_with_code() {
324 let result = CommandResult::error_with_code("permission denied", 126);
325 assert!(!result.is_success());
326 assert_eq!(result.code, 126);
327 }
328
329 #[test]
330 fn test_resolve_path_absolute() {
331 let absolute_path = if cfg!(windows) {
332 PathBuf::from(r"C:\absolute\path")
333 } else {
334 PathBuf::from("/absolute/path")
335 };
336 let path = VirtualUtils::resolve_path(absolute_path.to_str().unwrap(), None);
337 assert_eq!(path, absolute_path);
338 }
339
340 #[test]
341 fn test_resolve_path_relative() {
342 let cwd = PathBuf::from("/home/user");
343 let path = VirtualUtils::resolve_path("relative/path", Some(&cwd));
344 assert_eq!(path, PathBuf::from("/home/user/relative/path"));
345 }
346
347 #[test]
348 fn test_validate_args_success() {
349 let args = vec!["arg1".to_string()];
350 assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
351 }
352
353 #[test]
354 fn test_validate_args_missing() {
355 let args = vec!["arg1".to_string()];
356 let result = VirtualUtils::validate_args(&args, 2, "cmd");
357 assert!(result.is_some());
358 }
359
360 #[test]
361 fn test_missing_operand_error() {
362 let result = VirtualUtils::missing_operand_error("cat");
363 assert!(!result.is_success());
364 assert!(result.stderr.contains("missing operand"));
365 }
366
367 #[test]
368 fn test_invalid_argument_error() {
369 let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
370 assert!(!result.is_success());
371 assert!(result.stderr.contains("invalid option"));
372 }
373
374 #[test]
378 fn test_reexported_quote() {
379 assert_eq!(quote("hello"), "hello");
380 assert_eq!(quote("hello world"), "'hello world'");
381 }
382
383 #[test]
384 fn test_reexported_ansi_utils() {
385 let text = "\x1b[31mRed text\x1b[0m";
386 assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
387 }
388
389 #[test]
390 fn test_reexported_ansi_config() {
391 let config = AnsiConfig::default();
392 assert!(config.preserve_ansi);
393 assert!(config.preserve_control_chars);
394 }
395
396 #[cfg(unix)]
397 #[test]
398 fn safely_exports_invocation_directory_variables() {
399 let env = HashMap::from([
400 ("PWD".to_string(), "/tmp/new dir".to_string()),
401 (
402 "OLDPWD".to_string(),
403 "/tmp/old' dir\n$() `cmd`; end".to_string(),
404 ),
405 ]);
406
407 assert_eq!(
408 with_exported_process_context("printf done", Some(&env)),
409 "export PWD='/tmp/new dir' OLDPWD='/tmp/old'\\'' dir\n$() `cmd`; end'; printf done"
410 );
411 }
412}