1use crate::result_streams::{CapturedInput, CapturedOutput};
17use std::collections::HashMap;
18use std::env;
19use std::path::{Path, PathBuf};
20
21pub use crate::ansi::{AnsiConfig, AnsiUtils};
23pub use crate::quote::quote;
24pub use crate::trace::{is_trace_enabled, trace, trace_lazy};
25
26#[cfg(unix)]
32pub(crate) fn with_exported_process_context(
33 command: &str,
34 env: Option<&HashMap<String, String>>,
35) -> String {
36 let Some(env) = env else {
37 return command.to_string();
38 };
39 let assignments = ["PWD", "OLDPWD"]
40 .into_iter()
41 .filter_map(|name| {
42 env.get(name).map(|value| {
43 let value = value.replace('\'', "'\\''");
44 format!("{name}='{value}'")
45 })
46 })
47 .collect::<Vec<_>>();
48
49 if assignments.is_empty() {
50 command.to_string()
51 } else {
52 format!("export {}; {command}", assignments.join(" "))
53 }
54}
55
56#[cfg(not(unix))]
57pub(crate) fn with_exported_process_context(
58 command: &str,
59 _env: Option<&HashMap<String, String>>,
60) -> String {
61 command.to_string()
62}
63
64#[derive(Debug, Clone)]
65struct ShellConfig {
66 cmd: String,
67 args: Vec<String>,
68 raw_command_arg: bool,
69}
70
71fn find_available_shell() -> ShellConfig {
72 #[cfg(windows)]
73 let shells: &[(&str, &[&str], bool)] = &[
74 (r"C:\Program Files\Git\bin\bash.exe", &["-c"], false),
75 (r"C:\Program Files\Git\usr\bin\bash.exe", &["-c"], false),
76 (r"C:\Program Files (x86)\Git\bin\bash.exe", &["-c"], false),
77 ("bash.exe", &["-c"], false),
78 ("wsl.exe", &["bash", "-c"], false),
79 ("powershell.exe", &["-Command"], false),
80 ("pwsh.exe", &["-Command"], false),
81 ("cmd.exe", &["/c"], true),
82 ];
83
84 #[cfg(not(windows))]
85 let shells: &[(&str, &[&str], bool)] = &[
86 ("/bin/sh", &["-c"], false),
87 ("/usr/bin/sh", &["-c"], false),
88 ("/bin/bash", &["-c"], false),
89 ("sh", &["-c"], false),
90 ];
91
92 for (cmd, args, raw_command_arg) in shells {
93 if Path::new(cmd).exists() || which::which(cmd).is_ok() {
94 return ShellConfig {
95 cmd: (*cmd).to_string(),
96 args: args.iter().map(|arg| (*arg).to_string()).collect(),
97 raw_command_arg: *raw_command_arg,
98 };
99 }
100 }
101
102 #[cfg(windows)]
103 return ShellConfig {
104 cmd: "cmd.exe".to_string(),
105 args: vec!["/c".to_string()],
106 raw_command_arg: true,
107 };
108
109 #[cfg(not(windows))]
110 ShellConfig {
111 cmd: "/bin/sh".to_string(),
112 args: vec!["-c".to_string()],
113 raw_command_arg: false,
114 }
115}
116
117#[cfg(windows)]
118fn append_command_arg(process: &mut tokio::process::Command, command: &str, raw_command_arg: bool) {
119 if raw_command_arg {
120 use std::os::windows::process::CommandExt;
125 process.as_std_mut().raw_arg(format!("\"{command}\""));
126 } else {
127 process.arg(command);
128 }
129}
130
131#[cfg(not(windows))]
132fn append_command_arg(
133 process: &mut tokio::process::Command,
134 command: &str,
135 _raw_command_arg: bool,
136) {
137 process.arg(command);
138}
139
140pub(crate) fn shell_command(
142 command: &str,
143 env: Option<&HashMap<String, String>>,
144) -> tokio::process::Command {
145 let shell = find_available_shell();
146 let mut process = tokio::process::Command::new(&shell.cmd);
147 process.args(&shell.args);
148 let command = with_exported_process_context(command, env);
149 append_command_arg(&mut process, &command, shell.raw_command_arg);
150 process
151}
152
153#[derive(Debug, Clone)]
155pub struct CommandResult {
156 pub stdout: CapturedOutput,
157 pub stderr: CapturedOutput,
158 pub stdin: CapturedInput,
159 pub code: i32,
160}
161
162impl CommandResult {
163 pub fn new(
165 stdout: impl Into<CapturedOutput>,
166 stderr: impl Into<CapturedOutput>,
167 code: i32,
168 ) -> Self {
169 Self {
170 stdout: stdout.into(),
171 stderr: stderr.into(),
172 stdin: CapturedInput::default(),
173 code,
174 }
175 }
176
177 pub fn success(stdout: impl Into<String>) -> Self {
179 Self::new(stdout.into(), "", 0)
180 }
181
182 pub fn success_empty() -> Self {
184 Self::new("", "", 0)
185 }
186
187 pub fn error(stderr: impl Into<String>) -> Self {
189 Self::new("", stderr.into(), 1)
190 }
191
192 pub fn error_with_code(stderr: impl Into<String>, code: i32) -> Self {
194 Self::new("", stderr.into(), code)
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}