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
63pub(crate) fn append_shell_command(
65 process: &mut tokio::process::Command,
66 command: &str,
67 env: Option<&HashMap<String, String>>,
68) {
69 let command = with_exported_process_context(command, env);
70
71 #[cfg(windows)]
72 {
73 use std::os::windows::process::CommandExt;
78 process.as_std_mut().raw_arg(format!("\"{command}\""));
79 }
80
81 #[cfg(not(windows))]
82 process.arg(command);
83}
84
85#[derive(Debug, Clone)]
87pub struct CommandResult {
88 pub stdout: String,
89 pub stderr: String,
90 pub code: i32,
91}
92
93impl CommandResult {
94 pub fn success(stdout: impl Into<String>) -> Self {
96 CommandResult {
97 stdout: stdout.into(),
98 stderr: String::new(),
99 code: 0,
100 }
101 }
102
103 pub fn success_empty() -> Self {
105 CommandResult {
106 stdout: String::new(),
107 stderr: String::new(),
108 code: 0,
109 }
110 }
111
112 pub fn error(stderr: impl Into<String>) -> Self {
114 CommandResult {
115 stdout: String::new(),
116 stderr: stderr.into(),
117 code: 1,
118 }
119 }
120
121 pub fn error_with_code(stderr: impl Into<String>, code: i32) -> Self {
123 CommandResult {
124 stdout: String::new(),
125 stderr: stderr.into(),
126 code,
127 }
128 }
129
130 pub fn is_success(&self) -> bool {
132 self.code == 0
133 }
134
135 pub fn exit_code(&self) -> i32 {
140 self.code
141 }
142
143 pub fn error_for_status(self) -> crate::Result<CommandResult> {
160 if self.is_success() {
161 return Ok(self);
162 }
163
164 Err(crate::Error::command_failed(
165 self.code,
166 format!("Command failed with exit code {}", self.code),
167 ))
168 }
169}
170
171pub struct VirtualUtils;
173
174impl VirtualUtils {
175 pub fn missing_operand_error(command_name: &str) -> CommandResult {
177 CommandResult::error(format!("{}: missing operand", command_name))
178 }
179
180 pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
182 CommandResult::error(format!("{}: {}", command_name, message))
183 }
184
185 pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
187 CommandResult::error(format!("{}: {}", command_name, message))
188 }
189
190 pub fn success(stdout: impl Into<String>) -> CommandResult {
192 CommandResult::success(stdout)
193 }
194
195 pub fn error(stderr: impl Into<String>) -> CommandResult {
197 CommandResult::error(stderr)
198 }
199
200 pub fn validate_args(
202 args: &[String],
203 min_count: usize,
204 command_name: &str,
205 ) -> Option<CommandResult> {
206 if args.len() < min_count {
207 if min_count == 1 {
208 return Some(Self::missing_operand_error(command_name));
209 } else {
210 return Some(Self::invalid_argument_error(
211 command_name,
212 &format!("requires at least {} arguments", min_count),
213 ));
214 }
215 }
216 None }
218
219 pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf {
221 let path = Path::new(file_path);
222 if path.is_absolute() {
223 path.to_path_buf()
224 } else {
225 let base_path = cwd
226 .map(|p| p.to_path_buf())
227 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
228 base_path.join(path)
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_command_result_success() {
239 let result = CommandResult::success("hello");
240 assert!(result.is_success());
241 assert_eq!(result.stdout, "hello");
242 assert_eq!(result.stderr, "");
243 assert_eq!(result.code, 0);
244 }
245
246 #[test]
247 fn test_command_result_error() {
248 let result = CommandResult::error("something went wrong");
249 assert!(!result.is_success());
250 assert_eq!(result.stdout, "");
251 assert_eq!(result.stderr, "something went wrong");
252 assert_eq!(result.code, 1);
253 }
254
255 #[test]
256 fn test_command_result_error_with_code() {
257 let result = CommandResult::error_with_code("permission denied", 126);
258 assert!(!result.is_success());
259 assert_eq!(result.code, 126);
260 }
261
262 #[test]
263 fn test_resolve_path_absolute() {
264 let absolute_path = if cfg!(windows) {
265 PathBuf::from(r"C:\absolute\path")
266 } else {
267 PathBuf::from("/absolute/path")
268 };
269 let path = VirtualUtils::resolve_path(absolute_path.to_str().unwrap(), None);
270 assert_eq!(path, absolute_path);
271 }
272
273 #[test]
274 fn test_resolve_path_relative() {
275 let cwd = PathBuf::from("/home/user");
276 let path = VirtualUtils::resolve_path("relative/path", Some(&cwd));
277 assert_eq!(path, PathBuf::from("/home/user/relative/path"));
278 }
279
280 #[test]
281 fn test_validate_args_success() {
282 let args = vec!["arg1".to_string()];
283 assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
284 }
285
286 #[test]
287 fn test_validate_args_missing() {
288 let args = vec!["arg1".to_string()];
289 let result = VirtualUtils::validate_args(&args, 2, "cmd");
290 assert!(result.is_some());
291 }
292
293 #[test]
294 fn test_missing_operand_error() {
295 let result = VirtualUtils::missing_operand_error("cat");
296 assert!(!result.is_success());
297 assert!(result.stderr.contains("missing operand"));
298 }
299
300 #[test]
301 fn test_invalid_argument_error() {
302 let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
303 assert!(!result.is_success());
304 assert!(result.stderr.contains("invalid option"));
305 }
306
307 #[test]
311 fn test_reexported_quote() {
312 assert_eq!(quote("hello"), "hello");
313 assert_eq!(quote("hello world"), "'hello world'");
314 }
315
316 #[test]
317 fn test_reexported_ansi_utils() {
318 let text = "\x1b[31mRed text\x1b[0m";
319 assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
320 }
321
322 #[test]
323 fn test_reexported_ansi_config() {
324 let config = AnsiConfig::default();
325 assert!(config.preserve_ansi);
326 assert!(config.preserve_control_chars);
327 }
328
329 #[cfg(unix)]
330 #[test]
331 fn safely_exports_invocation_directory_variables() {
332 let env = HashMap::from([
333 ("PWD".to_string(), "/tmp/new dir".to_string()),
334 (
335 "OLDPWD".to_string(),
336 "/tmp/old' dir\n$() `cmd`; end".to_string(),
337 ),
338 ]);
339
340 assert_eq!(
341 with_exported_process_context("printf done", Some(&env)),
342 "export PWD='/tmp/new dir' OLDPWD='/tmp/old'\\'' dir\n$() `cmd`; end'; printf done"
343 );
344 }
345}