Skip to main content

perl_dap_command_args/
lib.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.contains(' ') {
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    #[cfg(not(windows))]
56    #[test]
57    fn double_quote_escapes_arg_with_space_and_single_quote() {
58        // An arg containing both a space and a single-quote cannot be
59        // single-quote-wrapped, so it must be double-quote-escaped instead.
60        let args = vec!["it's a file".to_string()];
61        let formatted = format_command_args(&args);
62        assert_eq!(formatted.len(), 1);
63        // Must be wrapped in double-quotes (not single-quotes).
64        assert!(
65            formatted[0].starts_with('"'),
66            "expected double-quote prefix, got: {}",
67            formatted[0]
68        );
69        assert!(formatted[0].ends_with('"'), "expected double-quote suffix, got: {}", formatted[0]);
70        // The original text must be preserved inside the wrapper.
71        assert!(
72            formatted[0].contains("it's a file"),
73            "original text not found in: {}",
74            formatted[0]
75        );
76    }
77
78    #[cfg(not(windows))]
79    #[test]
80    fn mixed_args_are_each_handled_independently() {
81        let args = vec![
82            "plain".to_string(),         // no space → unchanged
83            "has spaces".to_string(),    // space, no single-quote → single-quote wrap
84            "it's here now".to_string(), // space + single-quote → double-quote wrap
85        ];
86        let formatted = format_command_args(&args);
87        assert_eq!(formatted.len(), 3);
88
89        // Plain arg is unchanged.
90        assert_eq!(formatted[0], "plain");
91
92        // Space-only arg is single-quote-wrapped.
93        assert_eq!(formatted[1], "'has spaces'");
94
95        // Space+single-quote arg is double-quote-wrapped.
96        assert!(
97            formatted[2].starts_with('"'),
98            "expected double-quote prefix, got: {}",
99            formatted[2]
100        );
101        assert!(formatted[2].ends_with('"'), "expected double-quote suffix, got: {}", formatted[2]);
102        assert!(
103            formatted[2].contains("it's here now"),
104            "original text not found in: {}",
105            formatted[2]
106        );
107    }
108}