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