Skip to main content

nu_parser/
deparse.rs

1use nu_utils::escape_quote_string;
2
3fn string_should_be_quoted(input: &str) -> bool {
4    input.is_empty()
5        || input.starts_with('$')
6        || input.chars().any(|c| {
7            c.is_whitespace()
8                || c == '('
9                || c == '['
10                || c == '{'
11                || c == '}'
12                || c == '\''
13                || c == '`'
14                || c == '"'
15                || c == '\\'
16                || c == ';'
17                || c == '|'
18                // `#` starts a comment when it begins a token (or after whitespace).
19                // Without quoting, `nu script.nu #000000` becomes `main #000000` and
20                // the argument is stripped.
21                || c == '#'
22        })
23}
24
25// Escape rules:
26// input argument is not a flag, does not start with $ and doesn't contain special characters, it is passed as it is (foo -> foo)
27// input argument is not a flag and either starts with $ or contains special characters, quotes are added, " and \ are escaped (two \words -> "two \\words")
28// input argument is a flag without =, it's passed as it is (--foo -> --foo)
29// input argument is a flag with =, the first two points apply to the value (--foo=bar -> --foo=bar; --foo=bar' -> --foo="bar'")
30//
31// special characters are white space, (, [, {, }, ', `, ", \, ;, |, and #
32pub fn escape_for_script_arg(input: &str) -> String {
33    // handle for flag, maybe we need to escape the value.
34    if input.starts_with("--") {
35        if let Some((arg_name, arg_val)) = input.split_once('=') {
36            // only want to escape arg_val.
37            let arg_val = if string_should_be_quoted(arg_val) {
38                escape_quote_string(arg_val)
39            } else {
40                arg_val.into()
41            };
42
43            return format!("{arg_name}={arg_val}");
44        } else {
45            return input.into();
46        }
47    }
48    if string_should_be_quoted(input) {
49        escape_quote_string(input)
50    } else {
51        input.into()
52    }
53}
54
55#[cfg(test)]
56mod test {
57    use super::escape_for_script_arg;
58
59    #[test]
60    fn test_not_extra_quote() {
61        // check for input arg like this:
62        // nu b.nu word 8
63        assert_eq!(escape_for_script_arg("word"), "word".to_string());
64        assert_eq!(escape_for_script_arg("8"), "8".to_string());
65    }
66
67    #[test]
68    fn test_quote_special() {
69        let cases = vec![
70            ("two words", r#""two words""#),
71            ("$nake", r#""$nake""#),
72            ("`123", r#""`123""#),
73            ("this|cat", r#""this|cat""#),
74            ("this;cat", r#""this;cat""#),
75            // `#` would start a comment when re-parsed as `main <arg>`
76            ("#000000", r##""#000000""##),
77            ("#", r##""#""##),
78            ("foo#bar", r##""foo#bar""##),
79        ];
80
81        for (input, expected) in cases {
82            assert_eq!(escape_for_script_arg(input).as_str(), expected);
83        }
84    }
85
86    #[test]
87    fn test_flag_value_with_hash() {
88        assert_eq!(
89            escape_for_script_arg("--color=#000000"),
90            r##"--color="#000000""##.to_string()
91        );
92    }
93
94    #[test]
95    fn test_quote_newline() {
96        assert_eq!(escape_for_script_arg("c\nd"), format!("\"c\nd\""));
97    }
98
99    #[test]
100    fn test_arg_with_flag() {
101        // check for input arg like this:
102        // nu b.nu --linux --version=v5.2
103        assert_eq!(escape_for_script_arg("--linux"), "--linux".to_string());
104        assert_eq!(
105            escape_for_script_arg("--version=v5.2"),
106            "--version=v5.2".to_string()
107        );
108
109        // check for input arg like this:
110        // nu b.nu linux --version v5.2
111        assert_eq!(escape_for_script_arg("--version"), "--version".to_string());
112        assert_eq!(escape_for_script_arg("v5.2"), "v5.2".to_string());
113    }
114
115    #[test]
116    fn test_flag_arg_with_values_contains_special() {
117        // check for input arg like this:
118        // nu b.nu test_ver --version='xx yy' --separator="`"
119        assert_eq!(
120            escape_for_script_arg("--version='xx yy'"),
121            r#"--version="'xx yy'""#.to_string()
122        );
123        assert_eq!(
124            escape_for_script_arg("--separator=`"),
125            r#"--separator="`""#.to_string()
126        );
127    }
128
129    #[test]
130    fn test_escape() {
131        // check for input arg like this:
132        // nu b.nu \ --arg='"'
133        assert_eq!(escape_for_script_arg(r"\"), r#""\\""#.to_string());
134        assert_eq!(
135            escape_for_script_arg(r#"--arg=""#),
136            r#"--arg="\"""#.to_string()
137        );
138    }
139}