Skip to main content

command_stream/
utils.rs

1//! Utility functions and types for command-stream
2//!
3//! This module provides helper functions for command results, virtual command
4//! utilities, and re-exports from specialized utility modules.
5//!
6//! ## Module Organization
7//!
8//! The utilities are organized into focused modules following the same
9//! modular pattern as the JavaScript implementation:
10//!
11//! - `trace` - Logging and tracing utilities
12//! - `ansi` - ANSI escape code handling
13//! - `quote` - Shell quoting utilities
14//! - `utils` (this module) - Command results and virtual command helpers
15
16use std::collections::HashMap;
17use std::env;
18use std::path::{Path, PathBuf};
19
20// Re-export from specialized modules for backwards compatibility
21pub use crate::ansi::{AnsiConfig, AnsiUtils};
22pub use crate::quote::quote;
23pub use crate::trace::{is_trace_enabled, trace, trace_lazy};
24
25/// Re-export invocation-local directory variables inside POSIX shells.
26///
27/// Some shells, notably the macOS system shell, import `OLDPWD` as an
28/// internal variable but drop its export flag during startup. Prefixing the
29/// command keeps both variables visible to the real child process.
30#[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/// Result type for virtual command operations
64#[derive(Debug, Clone)]
65pub struct CommandResult {
66    pub stdout: String,
67    pub stderr: String,
68    pub code: i32,
69}
70
71impl CommandResult {
72    /// Create a success result with stdout output
73    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    /// Create an empty success result
82    pub fn success_empty() -> Self {
83        CommandResult {
84            stdout: String::new(),
85            stderr: String::new(),
86            code: 0,
87        }
88    }
89
90    /// Create an error result with stderr output
91    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    /// Create an error result with custom exit code
100    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    /// Check if the command was successful
109    pub fn is_success(&self) -> bool {
110        self.code == 0
111    }
112
113    /// Exit code of the command.
114    ///
115    /// This is an alias for the [`code`](Self::code) field, mirroring the
116    /// `exitCode` alias exposed by the JavaScript implementation (issue #36).
117    pub fn exit_code(&self) -> i32 {
118        self.code
119    }
120}
121
122/// Utility functions for virtual commands
123pub struct VirtualUtils;
124
125impl VirtualUtils {
126    /// Create standardized error response for missing operands
127    pub fn missing_operand_error(command_name: &str) -> CommandResult {
128        CommandResult::error(format!("{}: missing operand", command_name))
129    }
130
131    /// Create standardized error response for missing operands with custom message
132    pub fn missing_operand_error_with_message(command_name: &str, message: &str) -> CommandResult {
133        CommandResult::error(format!("{}: {}", command_name, message))
134    }
135
136    /// Create standardized error response for invalid arguments
137    pub fn invalid_argument_error(command_name: &str, message: &str) -> CommandResult {
138        CommandResult::error(format!("{}: {}", command_name, message))
139    }
140
141    /// Create standardized success response
142    pub fn success(stdout: impl Into<String>) -> CommandResult {
143        CommandResult::success(stdout)
144    }
145
146    /// Create standardized error response
147    pub fn error(stderr: impl Into<String>) -> CommandResult {
148        CommandResult::error(stderr)
149    }
150
151    /// Validate that command has required number of arguments
152    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 // No error
168    }
169
170    /// Resolve file path with optional cwd parameter
171    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    // Re-exported module tests are in their respective modules
259    // These tests verify the re-exports work correctly
260
261    #[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}