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)]
65pub struct CommandResult {
66 pub stdout: String,
67 pub stderr: String,
68 pub code: i32,
69}
70
71impl CommandResult {
72 pub fn success(stdout: impl Into<String>) -> Self {
74 CommandResult {
75 stdout: stdout.into(),
76 stderr: String::new(),
77 code: 0,
78 }
79 }
80
81 pub fn success_empty() -> Self {
83 CommandResult {
84 stdout: String::new(),
85 stderr: String::new(),
86 code: 0,
87 }
88 }
89
90 pub fn error(stderr: impl Into<String>) -> Self {
92 CommandResult {
93 stdout: String::new(),
94 stderr: stderr.into(),
95 code: 1,
96 }
97 }
98
99 pub fn error_with_code(stderr: impl Into<String>, code: i32) -> Self {
101 CommandResult {
102 stdout: String::new(),
103 stderr: stderr.into(),
104 code,
105 }
106 }
107
108 pub fn is_success(&self) -> bool {
110 self.code == 0
111 }
112
113 pub fn exit_code(&self) -> i32 {
118 self.code
119 }
120}
121
122pub struct VirtualUtils;
124
125impl VirtualUtils {
126 pub fn missing_operand_error(command_name: &str) -> CommandResult {
128 CommandResult::error(format!("{}: missing operand", command_name))
129 }
130
131 pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
133 CommandResult::error(format!("{}: {}", command_name, message))
134 }
135
136 pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
138 CommandResult::error(format!("{}: {}", command_name, message))
139 }
140
141 pub fn success(stdout: impl Into<String>) -> CommandResult {
143 CommandResult::success(stdout)
144 }
145
146 pub fn error(stderr: impl Into<String>) -> CommandResult {
148 CommandResult::error(stderr)
149 }
150
151 pub fn validate_args(
153 args: &[String],
154 min_count: usize,
155 command_name: &str,
156 ) -> Option<CommandResult> {
157 if args.len() < min_count {
158 if min_count == 1 {
159 return Some(Self::missing_operand_error(command_name));
160 } else {
161 return Some(Self::invalid_argument_error(
162 command_name,
163 &format!("requires at least {} arguments", min_count),
164 ));
165 }
166 }
167 None }
169
170 pub fn resolve_path(file_path: &str, cwd: Option<&Path>) -> PathBuf {
172 let path = Path::new(file_path);
173 if path.is_absolute() {
174 path.to_path_buf()
175 } else {
176 let base_path = cwd
177 .map(|p| p.to_path_buf())
178 .unwrap_or_else(|| env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
179 base_path.join(path)
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn test_command_result_success() {
190 let result = CommandResult::success("hello");
191 assert!(result.is_success());
192 assert_eq!(result.stdout, "hello");
193 assert_eq!(result.stderr, "");
194 assert_eq!(result.code, 0);
195 }
196
197 #[test]
198 fn test_command_result_error() {
199 let result = CommandResult::error("something went wrong");
200 assert!(!result.is_success());
201 assert_eq!(result.stdout, "");
202 assert_eq!(result.stderr, "something went wrong");
203 assert_eq!(result.code, 1);
204 }
205
206 #[test]
207 fn test_command_result_error_with_code() {
208 let result = CommandResult::error_with_code("permission denied", 126);
209 assert!(!result.is_success());
210 assert_eq!(result.code, 126);
211 }
212
213 #[test]
214 fn test_resolve_path_absolute() {
215 let absolute_path = if cfg!(windows) {
216 PathBuf::from(r"C:\absolute\path")
217 } else {
218 PathBuf::from("/absolute/path")
219 };
220 let path = VirtualUtils::resolve_path(absolute_path.to_str().unwrap(), None);
221 assert_eq!(path, absolute_path);
222 }
223
224 #[test]
225 fn test_resolve_path_relative() {
226 let cwd = PathBuf::from("/home/user");
227 let path = VirtualUtils::resolve_path("relative/path", Some(&cwd));
228 assert_eq!(path, PathBuf::from("/home/user/relative/path"));
229 }
230
231 #[test]
232 fn test_validate_args_success() {
233 let args = vec!["arg1".to_string()];
234 assert!(VirtualUtils::validate_args(&args, 1, "cmd").is_none());
235 }
236
237 #[test]
238 fn test_validate_args_missing() {
239 let args = vec!["arg1".to_string()];
240 let result = VirtualUtils::validate_args(&args, 2, "cmd");
241 assert!(result.is_some());
242 }
243
244 #[test]
245 fn test_missing_operand_error() {
246 let result = VirtualUtils::missing_operand_error("cat");
247 assert!(!result.is_success());
248 assert!(result.stderr.contains("missing operand"));
249 }
250
251 #[test]
252 fn test_invalid_argument_error() {
253 let result = VirtualUtils::invalid_argument_error("ls", "invalid option");
254 assert!(!result.is_success());
255 assert!(result.stderr.contains("invalid option"));
256 }
257
258 #[test]
262 fn test_reexported_quote() {
263 assert_eq!(quote("hello"), "hello");
264 assert_eq!(quote("hello world"), "'hello world'");
265 }
266
267 #[test]
268 fn test_reexported_ansi_utils() {
269 let text = "\x1b[31mRed text\x1b[0m";
270 assert_eq!(AnsiUtils::strip_ansi(text), "Red text");
271 }
272
273 #[test]
274 fn test_reexported_ansi_config() {
275 let config = AnsiConfig::default();
276 assert!(config.preserve_ansi);
277 assert!(config.preserve_control_chars);
278 }
279
280 #[cfg(unix)]
281 #[test]
282 fn safely_exports_invocation_directory_variables() {
283 let env = HashMap::from([
284 ("PWD".to_string(), "/tmp/new dir".to_string()),
285 (
286 "OLDPWD".to_string(),
287 "/tmp/old' dir\n$() `cmd`; end".to_string(),
288 ),
289 ]);
290
291 assert_eq!(
292 with_exported_process_context("printf done", Some(&env)),
293 "export PWD='/tmp/new dir' OLDPWD='/tmp/old'\\'' dir\n$() `cmd`; end'; printf done"
294 );
295 }
296}