1use crate::helpers::{get_var_regex, get_var_regex_bytes, quotable_into_string};
2use std::collections::HashMap;
3use std::sync::Arc;
4
5pub use shell_quote::Quotable;
6
7pub fn default_escape_chars() -> HashMap<char, &'static str> {
8 HashMap::from_iter([
9 ('\0', "\x00"),
11 ('\x07', "\\a"),
13 ('\x08', "\\b"),
15 ('\x09', "\\t"),
17 ('\t', "\\t"),
18 ('\x0A', "\\n"),
20 ('\n', "\\n"),
21 ('\x0B', "\\v"),
23 ('\x0C', "\\f"),
25 ('\x0D', "\\r"),
27 ('\r', "\\r"),
28 ('\x1B', "\\e"),
30 ('"', "\\\""),
32 ('\\', "\\\\"),
34 ])
35}
36
37pub fn apply_quote(
38 value: Quotable<'_>,
39 quotes: (&str, &str),
40 replacements: &HashMap<char, &str>,
41) -> String {
42 let value = quotable_into_string(value);
43 let (open, close) = quotes;
44
45 let mut out = String::with_capacity(open.len() + value.len() + close.len());
46 out.push_str(open);
47
48 for ch in value.chars() {
49 if let Some(replacement) = replacements.get(&ch) {
50 out.push_str(replacement);
51 } else {
52 out.push(ch);
53 }
54 }
55
56 out.push_str(close);
57 out
58}
59
60pub enum Syntax {
62 Symbol(String),
63 Pair(String, String),
64}
65
66pub struct QuoterOptions<'a> {
68 pub quote_pairs: Vec<(String, String, bool)>,
71
72 pub quoted_syntax: Vec<Syntax>,
74
75 pub unquoted_syntax: Vec<Syntax>,
77
78 pub on_quote: Option<Arc<dyn Fn(Quotable<'a>) -> String>>,
80
81 pub on_quote_expansion: Option<Arc<dyn Fn(Quotable<'a>) -> String>>,
83
84 pub replacements: HashMap<char, &'a str>,
86
87 pub replacements_expansion: HashMap<char, &'a str>,
89}
90
91impl Default for QuoterOptions<'_> {
92 fn default() -> Self {
93 Self {
94 quote_pairs: vec![
95 ("'".into(), "'".into(), false),
96 ("\"".into(), "\"".into(), true),
97 ],
98 quoted_syntax: vec![
100 Syntax::Pair("${".into(), "}".into()),
102 Syntax::Pair("$(".into(), ")".into()),
104 Syntax::Pair("$((".into(), "))".into()),
106 ],
107 unquoted_syntax: vec![
108 Syntax::Pair("{".into(), "}".into()),
110 Syntax::Pair("<(".into(), ")".into()),
112 Syntax::Pair(">(".into(), ")".into()),
113 Syntax::Symbol("**".into()),
115 Syntax::Symbol("*".into()),
116 Syntax::Symbol("?".into()),
117 Syntax::Pair("[".into(), "]".into()),
118 Syntax::Pair("?(".into(), ")".into()),
119 Syntax::Pair("*(".into(), ")".into()),
120 Syntax::Pair("+(".into(), ")".into()),
121 Syntax::Pair("@(".into(), ")".into()),
122 Syntax::Pair("!(".into(), ")".into()),
123 ],
124 on_quote: None,
125 on_quote_expansion: None,
126 replacements: HashMap::default(),
127 replacements_expansion: default_escape_chars(),
128 }
129 }
130}
131
132pub struct Quoter<'a> {
134 data: Quotable<'a>,
135 options: QuoterOptions<'a>,
136}
137
138impl<'a> Quoter<'a> {
139 pub fn new(data: impl Into<Quotable<'a>>, options: QuoterOptions<'a>) -> Quoter<'a> {
141 Self {
142 data: data.into(),
143 options,
144 }
145 }
146
147 pub fn is_bareword(&self) -> bool {
149 fn is_bare(ch: u8) -> bool {
150 !ch.is_ascii_whitespace() && (ch.is_ascii_alphanumeric() || ch == b'_')
151 }
152
153 match &self.data {
154 Quotable::Bytes(bytes) => bytes.iter().all(|ch| is_bare(*ch)),
155 Quotable::Text(text) => text.chars().all(|ch| is_bare(ch as u8)),
156 }
157 }
158
159 pub fn is_empty(&self) -> bool {
161 match &self.data {
162 Quotable::Bytes(bytes) => bytes.is_empty(),
163 Quotable::Text(text) => text.is_empty(),
164 }
165 }
166
167 pub fn is_quoted(&self) -> bool {
169 for (sq, eq, _) in &self.options.quote_pairs {
170 match &self.data {
171 Quotable::Bytes(bytes) => {
172 if bytes.starts_with(sq.as_bytes()) && bytes.ends_with(eq.as_bytes()) {
173 return true;
174 }
175 }
176 Quotable::Text(text) => {
177 if text.starts_with(sq) && text.ends_with(eq) {
178 return true;
179 }
180 }
181 };
182 }
183
184 false
185 }
186
187 pub fn maybe_quote(self) -> String {
191 if self.is_empty() {
192 let pair = &self.options.quote_pairs[0];
193
194 return format!("{}{}", pair.0, pair.1);
195 }
196
197 if self.is_quoted() || self.is_bareword() {
198 return quotable_into_string(self.data);
199 }
200
201 if self.requires_expansion() {
202 return self.quote_expansion();
203 }
204
205 if self.requires_unquoted() {
206 return quotable_into_string(self.data);
207 }
208
209 self.quote()
210 }
211
212 pub fn quote_expansion(self) -> String {
215 if let Some(on_quote_expansion) = &self.options.on_quote_expansion {
216 return on_quote_expansion(self.data);
217 }
218
219 let (open, close, _) = self
220 .options
221 .quote_pairs
222 .iter()
223 .find(|(_, _, is_expansion)| *is_expansion)
224 .or(self.options.quote_pairs.last())
225 .unwrap();
226
227 apply_quote(
228 self.data,
229 (open, close),
230 &self.options.replacements_expansion,
231 )
232 }
233
234 pub fn quote(self) -> String {
237 if let Some(on_quote) = &self.options.on_quote {
238 return on_quote(self.data);
239 }
240
241 let (open, close, _) = self
242 .options
243 .quote_pairs
244 .iter()
245 .find(|(_, _, is_expansion)| !is_expansion)
246 .or(self.options.quote_pairs.first())
247 .unwrap();
248
249 apply_quote(self.data, (open, close), &self.options.replacements)
250 }
251
252 pub fn requires_expansion(&self) -> bool {
254 if quotable_contains_syntax(&self.data, &self.options.quoted_syntax) {
256 return true;
257 }
258
259 if match &self.data {
261 Quotable::Bytes(bytes) => get_var_regex_bytes().is_match(bytes),
262 Quotable::Text(text) => get_var_regex().is_match(text),
263 } {
264 return true;
265 }
266
267 for ch in self.options.replacements_expansion.keys() {
269 match &self.data {
270 Quotable::Bytes(bytes) => {
271 if bytes.contains(&(*ch as u8)) {
272 return true;
273 }
274 }
275 Quotable::Text(text) => {
276 if text.contains(*ch) {
277 return true;
278 }
279 }
280 };
281 }
282
283 false
284 }
285
286 pub fn requires_unquoted(&self) -> bool {
288 quotable_contains_syntax(&self.data, &self.options.unquoted_syntax)
289 }
290}
291
292fn quotable_contains_syntax(data: &Quotable<'_>, syntaxes: &[Syntax]) -> bool {
293 for syntax in syntaxes {
294 match data {
295 Quotable::Bytes(bytes) => {
296 match syntax {
297 Syntax::Symbol(symbol) => {
298 let sbytes = symbol.as_bytes();
299
300 if bytes.windows(sbytes.len()).any(|chunk| chunk == sbytes) {
301 return true;
302 }
303 }
304 Syntax::Pair(open, close) => {
305 let obytes = open.as_bytes();
306 let cbytes = close.as_bytes();
307
308 if let Some(o) = bytes
309 .windows(obytes.len())
310 .position(|chunk| chunk == obytes)
311 {
312 if bytes[o..]
313 .windows(cbytes.len())
314 .any(|chunk| chunk == cbytes)
315 {
316 return true;
317 }
318 }
319 }
320 };
321 }
322 Quotable::Text(text) => {
323 match syntax {
324 Syntax::Symbol(symbol) => {
325 if text.contains(symbol) {
326 return true;
327 }
328 }
329 Syntax::Pair(open, close) => {
330 if let Some(o) = text.find(open) {
331 if text[o..].contains(close) {
332 return true;
333 }
334 }
335 }
336 };
337 }
338 };
339 }
340
341 false
342}