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
144pub struct VirtualUtils;
146
147impl VirtualUtils {
148 pub fn missing_operand_error(command_name: &str) -> CommandResult {
150 CommandResult::error(format!("{}: missing operand", command_name))
151 }
152
153 pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
155 CommandResult::error(format!("{}: {}", command_name, message))
156 }
157
158 pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
160 CommandResult::error(format!("{}: {}", command_name, message))
161 }
162
163 pub fn success(stdout: impl Into<String>) -> CommandResult {
165 CommandResult::success(stdout)
166 }
167
168 pub fn error(stderr: impl Into<String>) -> CommandResult {
170 CommandResult::error(stderr)
171 }
172
173 pub fn validate_args(
175 args: &[String],
176 min_count: usize,
177 command_name: &str,
178 ) -> Option<CommandResult> {
179 if args.len() < min_count {
180 if min_count == 1 {
181 return Some(Self::missing_operand_error(command_name));
182 } else {
183 return Some(Self::invalid_argument_error(
184 command_name,
185 &format!("requires at least {} arguments", min_count),
186 ));
187 }
188 }
189 None }
191
192 pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf {
194 let path = Path::new(file_path);
195 if path.is_absolute() {
196 path.to_path_buf()
197 } else {
198 let base_path = cwd
199 .map(|p| p.to_path_buf())
200 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
201 base_path.join(path)
202 }
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_command_result_success() {
212 let result = CommandResult::success("hello");
213 assert!(result.is_success());
214 assert_eq!(result.stdout, "hello");
215 assert_eq!(result.stderr, "");
216 assert_eq!(result.code, 0);
217 }
218
219 #[test]
220 fn test_command_result_error() {
221 let result = CommandResult::error("something went wrong");
222 assert!(!result.is_success());
223 assert_eq!(result.stdout, "");
224 assert_eq!(result.stderr, "something went wrong");
225 assert_eq!(result.code, 1);
226 }
227
228 #[test]
229 fn test_command_result_error_with_code() {
230 let result = CommandResult::error_with_code("permission denied", 126);
231 assert!(!result.is_success());
232 assert_eq!(result.code, 126);
233 }
234
235 #[test]
236 fn test_resolve_path_absolute() {
237 let absolute_path = if cfg!(windows) {
238 PathBuf::from(r"C:\absolute\path")
239 } else {
240 PathBuf::from("/absolute/path")
241 };
242 let path = VirtualUtils::resolve_path(absolute_path.to_str().unwrap(), None);
243 assert_eq!(path, absolute_path);
244 }
245
246 #[test]
247 fn test_resolve_path_relative() {
248 let cwd = PathBuf::from("/home/user");
249 let path = VirtualUtils::resolve_path("relative/path", Some(&cwd));
250 assert_eq!(path, PathBuf::from("/home/user/relative/path"));
251 }
252
253 #[test]
254 fn test_validate_args_success() {
255 let args = vec!["arg1".to_string()];
256 assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
257 }
258
259 #[test]
260 fn test_validate_args_missing() {
261 let args = vec!["arg1".to_string()];
262 let result = VirtualUtils::validate_args(&args, 2, "cmd");
263 assert!(result.is_some());
264 }
265
266 #[test]
267 fn test_missing_operand_error() {
268 let result = VirtualUtils::missing_operand_error("cat");
269 assert!(!result.is_success());
270 assert!(result.stderr.contains("missing operand"));
271 }
272
273 #[test]
274 fn test_invalid_argument_error() {
275 let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
276 assert!(!result.is_success());
277 assert!(result.stderr.contains("invalid option"));
278 }
279
280 #[test]
284 fn test_reexported_quote() {
285 assert_eq!(quote("hello"), "hello");
286 assert_eq!(quote("hello world"), "'hello world'");
287 }
288
289 #[test]
290 fn test_reexported_ansi_utils() {
291 let text = "\x1b[31mRed text\x1b[0m";
292 assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
293 }
294
295 #[test]
296 fn test_reexported_ansi_config() {
297 let config = AnsiConfig::default();
298 assert!(config.preserve_ansi);
299 assert!(config.preserve_control_chars);
300 }
301
302 #[cfg(unix)]
303 #[test]
304 fn safely_exports_invocation_directory_variables() {
305 let env = HashMap::from([
306 ("PWD".to_string(), "/tmp/new dir".to_string()),
307 (
308 "OLDPWD".to_string(),
309 "/tmp/old' dir\n$() `cmd`; end".to_string(),
310 ),
311 ]);
312
313 assert_eq!(
314 with_exported_process_context("printf done", Some(&env)),
315 "export PWD='/tmp/new dir' OLDPWD='/tmp/old'\\'' dir\n$() `cmd`; end'; printf done"
316 );
317 }
318}