Skip to main content

perl_dap/command_args/
mod.rs

1//! Platform-aware shell argument formatting used by `perl-dap`.
2
3/// Format command-line arguments for platform-specific shells.
4#[must_use]
5pub fn format_command_args(args: &[String]) -> Vec<String> {
6    args.iter()
7        .map(|arg| {
8            if arg.is_empty() || arg.chars().any(char::is_whitespace) {
9                #[cfg(windows)]
10                {
11                    format!("\"{}\"", arg.replace('"', "\\\""))
12                }
13                #[cfg(not(windows))]
14                {
15                    if arg.contains('\'') {
16                        format!("\"{}\"", arg.replace('"', "\\\""))
17                    } else {
18                        format!("'{}'", arg)
19                    }
20                }
21            } else {
22                arg.clone()
23            }
24        })
25        .collect()
26}
27
28#[cfg(test)]
29mod tests {
30    use super::format_command_args;
31
32    #[test]
33    fn leaves_simple_args_unmodified() {
34        let args = vec!["plain".to_string(), "--flag".to_string()];
35        let formatted = format_command_args(&args);
36        assert_eq!(formatted, args);
37    }
38
39    #[test]
40    fn quotes_args_with_spaces() {
41        let args = vec!["file with spaces.txt".to_string()];
42        let formatted = format_command_args(&args);
43        assert_eq!(formatted.len(), 1);
44        assert!(formatted[0].contains("file with spaces.txt"));
45        assert_ne!(formatted[0], "file with spaces.txt");
46    }
47
48    #[test]
49    fn empty_slice_returns_empty_vec() {
50        let args: Vec<String> = vec![];
51        let formatted = format_command_args(&args);
52        assert!(formatted.is_empty());
53    }
54
55    #[test]
56    fn empty_arg_is_preserved_via_quotes() {
57        let args = vec![String::new()];
58        let formatted = format_command_args(&args);
59        assert_eq!(formatted.len(), 1);
60        #[cfg(windows)]
61        assert_eq!(formatted[0], "\"\"");
62        #[cfg(not(windows))]
63        assert_eq!(formatted[0], "''");
64    }
65
66    #[cfg(not(windows))]
67    #[test]
68    fn double_quote_escapes_arg_with_space_and_single_quote() {
69        // An arg containing both a space and a single-quote cannot be
70        // single-quote-wrapped, so it must be double-quote-escaped instead.
71        let args = vec!["it's a file".to_string()];
72        let formatted = format_command_args(&args);
73        assert_eq!(formatted.len(), 1);
74        // Must be wrapped in double-quotes (not single-quotes).
75        assert!(
76            formatted[0].starts_with('"'),
77            "expected double-quote prefix, got: {}",
78            formatted[0]
79        );
80        assert!(formatted[0].ends_with('"'), "expected double-quote suffix, got: {}", formatted[0]);
81        // The original text must be preserved inside the wrapper.
82        assert!(
83            formatted[0].contains("it's a file"),
84            "original text not found in: {}",
85            formatted[0]
86        );
87    }
88
89    #[cfg(not(windows))]
90    #[test]
91    fn mixed_args_are_each_handled_independently() {
92        let args = vec![
93            "plain".to_string(),         // no space → unchanged
94            "has spaces".to_string(),    // space, no single-quote → single-quote wrap
95            "it's here now".to_string(), // space + single-quote → double-quote wrap
96        ];
97        let formatted = format_command_args(&args);
98        assert_eq!(formatted.len(), 3);
99
100        // Plain arg is unchanged.
101        assert_eq!(formatted[0], "plain");
102
103        // Space-only arg is single-quote-wrapped.
104        assert_eq!(formatted[1], "'has spaces'");
105
106        // Space+single-quote arg is double-quote-wrapped.
107        assert!(
108            formatted[2].starts_with('"'),
109            "expected double-quote prefix, got: {}",
110            formatted[2]
111        );
112        assert!(formatted[2].ends_with('"'), "expected double-quote suffix, got: {}", formatted[2]);
113        assert!(
114            formatted[2].contains("it's here now"),
115            "original text not found in: {}",
116            formatted[2]
117        );
118    }
119}