garden/
syntax.rs

1/// Return true if the string contains 0-9 digits only
2#[inline]
3pub(crate) fn is_digit(string: &str) -> bool {
4    string.chars().all(|c| c.is_ascii_digit())
5}
6
7/// Return true if `string` is an `$ exec` expression.
8#[inline]
9pub(crate) fn is_exec(string: &str) -> bool {
10    string.starts_with("$ ")
11}
12
13/// Return true if `string` is a `:garden` expression.
14#[inline]
15pub(crate) fn is_garden(string: &str) -> bool {
16    string.starts_with(':')
17}
18
19/// Return true if `string` is a `%group` expression.
20#[inline]
21pub(crate) fn is_group(string: &str) -> bool {
22    string.starts_with('%')
23}
24
25/// Return true if `string` is a `@tree` expression.
26#[inline]
27pub(crate) fn is_tree(string: &str) -> bool {
28    string.starts_with('@')
29}
30
31/// Return true if `string` is a variable "replace" operation.
32#[inline]
33pub(crate) fn is_append_op(string: &str) -> bool {
34    string.ends_with('+')
35}
36
37/// Return true if `string` is a variable "append" operation.
38#[inline]
39pub(crate) fn is_replace_op(string: &str) -> bool {
40    string.ends_with('=')
41}
42
43/// Return true if `string` is a `graft::value` expression.
44#[inline]
45pub(crate) fn is_graft(string: &str) -> bool {
46    string.contains("::")
47}
48
49/// Return true if `string` is a candidate for evaluation.
50/// Returns true for strings with ${vars}  and "$ exec" expressions.
51#[inline]
52pub(crate) fn is_eval_candidate(string: &str) -> bool {
53    string.contains('$')
54}
55
56/// Return true if `string` ends in ".git". This is used to detect bare repositories.
57#[inline]
58pub(crate) fn is_git_dir(string: &str) -> bool {
59    string.len() > 4 && string.ends_with(".git") && !string.ends_with("/.git")
60}
61
62/// Return true  if `string` is a post-command.
63#[inline]
64pub(crate) fn is_post_command(string: &str) -> bool {
65    string.ends_with('>')
66}
67
68/// Return true  if `string` is a pre-command.
69#[inline]
70pub(crate) fn is_pre_command(string: &str) -> bool {
71    string.ends_with('<')
72}
73
74/// Return true  if `string` is a pre or post-command.
75#[inline]
76pub fn is_pre_or_post_command(string: &str) -> bool {
77    is_pre_command(string) || is_post_command(string)
78}
79
80/// Return true if `string` is a "#!" shebang line.
81#[inline]
82pub(crate) fn is_shebang(string: &str) -> bool {
83    string.starts_with("#!")
84}
85
86/// Trim garden, group, and tree prefixes
87#[inline]
88pub(crate) fn trim(string: &str) -> &str {
89    let needs_trim = is_group(string) || is_tree(string) || is_garden(string);
90    if !string.is_empty() && needs_trim {
91        &string[1..]
92    } else {
93        string
94    }
95}
96
97/// Trim the "$ " prefix from an exec expression
98#[inline]
99pub(crate) fn trim_exec(string: &str) -> &str {
100    let prefix = "$ ";
101    let prefix_len = prefix.len();
102    if string.len() >= prefix_len && string.starts_with(prefix) {
103        &string[prefix_len..]
104    } else {
105        string
106    }
107}
108
109/// Trim "+" and "=" suffixes in-place.
110#[inline]
111pub fn trim_op_inplace(string: &mut String) {
112    let len = string.len();
113    if len > 1 {
114        string.remove(len - 1);
115    }
116}
117
118/// Split a string into pre and post-graft namespace string refs
119#[inline]
120pub(crate) fn split_graft(string: &str) -> Option<(&str, &str)> {
121    string.split_once("::")
122}
123
124/// Remove the graft basename leaving the remainder of the graft string.
125#[inline]
126pub(crate) fn trim_graft(string: &str) -> Option<String> {
127    let (_before, after) = match split_graft(string) {
128        Some((before, after)) => (before, after),
129        None => return None,
130    };
131    let result;
132    // We split the graft prefix off which can contain garden/group/tree qualifiers.
133    // Reattach the qualifiers onto the inner graft query string.
134    if is_garden(string) {
135        result = string!(":") + after;
136    } else if is_group(string) {
137        result = string!("%") + after;
138    } else if is_tree(string) {
139        result = string!("@") + after;
140    } else {
141        result = after.to_string();
142    }
143
144    Some(result)
145}
146
147/// Return the graft basename.  "@foo::bar::baz" -> "foo"
148#[inline]
149pub(crate) fn graft_basename(string: &str) -> Option<&str> {
150    let (before, _after) = match split_graft(string) {
151        Some((before, after)) => (before, after),
152        None => return None,
153    };
154    let result = if is_garden(string) || is_group(string) || is_tree(string) {
155        trim(before)
156    } else {
157        before
158    };
159
160    Some(result)
161}
162
163/// Trim the "#!" shebang section from a string.
164#[inline]
165pub(crate) fn trim_shebang(string: &str) -> Option<&str> {
166    if is_shebang(string) {
167        Some(&string[2..])
168    } else {
169        None
170    }
171}
172
173/// Parse a custom command and extract a custom shebang interpreter command.
174/// Return an Option<(shebang, command)> when a shebang is present and None otherwise.
175pub(crate) fn split_shebang(string: &str) -> Option<(&str, &str)> {
176    if let Some(trimmed) = trim_shebang(string) {
177        trimmed.split_once('\n')
178    } else {
179        None
180    }
181}
182
183/// Escape $variable into $$variable for evaluation by shellexpand.
184#[inline]
185pub(crate) fn escape_shell_variables(string: &str) -> String {
186    let mut result = String::new();
187
188    // Did we just see '$' ? If so, we might need to escape it.
189    let mut potential_variable = false;
190    for c in string.chars() {
191        if potential_variable {
192            if c.is_alphanumeric() || c == '_' {
193                result.push('$'); // Escape $variable -> $$variable.
194                result.push(c);
195            } else if c == '$' {
196                result.push('$'); // Escape $$ -> $
197            } else {
198                result.push(c);
199            }
200            potential_variable = false;
201        } else {
202            // Push the value into the stream.
203            result.push(c);
204
205            // If the current value is '$' then the next loop may need to escape it.
206            potential_variable = c == '$';
207        }
208    }
209
210    result
211}
212
213/// Return the value of a boolean as a string.
214#[inline]
215pub(crate) fn bool_to_string(value: bool) -> String {
216    match value {
217        true => string!("true"),
218        false => string!("false"),
219    }
220}
221
222/// Return the value of a string as a boolean. Accepts "true", "false", "1" and "0".
223#[inline]
224pub(crate) fn string_to_bool(value: &str) -> Option<bool> {
225    match value {
226        "true" | "1" => Some(true),
227        "false" | "0" => Some(false),
228        _ => None,
229    }
230}
231
232/// Add a pre-command suffix to a command name.
233#[inline]
234pub(crate) fn pre_command(name: &str) -> String {
235    format!("{name}<")
236}
237
238/// Add a post-command suffix to a command name.
239#[inline]
240pub(crate) fn post_command(name: &str) -> String {
241    format!("{name}>")
242}
243
244/// Unit tests
245#[cfg(test)]
246mod tests {
247    #[test]
248    fn is_garden() {
249        assert!(super::is_garden(":garden"), ":garden is a garden");
250        assert!(!super::is_garden("garden"), "garden is not a garden");
251    }
252
253    #[test]
254    fn is_graft() {
255        assert!(super::is_graft("foo::bar"), "foo::bar is a graft");
256        assert!(!super::is_graft("foo"), "foo is not a graft");
257    }
258
259    #[test]
260    fn is_group() {
261        assert!(super::is_group("%group"), "%group is a group");
262        assert!(!super::is_group("group"), "group is not a group");
263    }
264
265    #[test]
266    fn is_tree() {
267        assert!(super::is_tree("@tree"), "@tree is a tree");
268        assert!(!super::is_tree("tree"), "tree is not a tree");
269    }
270
271    #[test]
272    fn is_git_dir() {
273        assert!(super::is_git_dir("tree.git"), "tree.git is a git dir");
274        assert!(
275            super::is_git_dir("/src/tree.git"),
276            "/src/tree.git is a git dir"
277        );
278        assert!(
279            !super::is_git_dir("src/tree/.git"),
280            "src/tree/.git is a git dir"
281        );
282        assert!(!super::is_git_dir(".git"), ".git is a git dir");
283        assert!(!super::is_git_dir("/.git"), "/.git is a git dir");
284    }
285
286    #[test]
287    fn split_graft_ok() {
288        let split = super::split_graft("foo::bar");
289        assert!(split.is_some(), "split_graft on foo::bar is ok");
290        assert_eq!(split, Some(("foo", "bar")));
291    }
292
293    #[test]
294    fn split_graft_nested_ok() {
295        let split = super::split_graft("@foo::bar::baz");
296        assert!(split.is_some(), "split_graft on @foo::bar::baz is ok");
297        assert_eq!(split, Some(("@foo", "bar::baz")));
298    }
299
300    #[test]
301    fn split_graft_empty() {
302        let split = super::split_graft("foo::");
303        assert!(split.is_some(), "split_graft on foo:: is ok");
304        assert_eq!(split, Some(("foo", "")));
305    }
306
307    #[test]
308    fn split_graft_not_found() {
309        let split = super::split_graft("foo");
310        assert!(split.is_none(), "split_graft on foo is None");
311    }
312
313    #[test]
314    fn trim_exec() {
315        assert_eq!("cmd", super::trim_exec("$ cmd"));
316        assert_eq!("$cmd", super::trim_exec("$cmd"));
317        assert_eq!("cmd", super::trim_exec("cmd"));
318        assert_eq!("", super::trim_exec("$ "));
319        assert_eq!("$", super::trim_exec("$"));
320        assert_eq!("", super::trim_exec(""));
321    }
322
323    #[test]
324    fn trim_graft() {
325        let value = super::trim_graft("foo::bar::baz");
326        assert!(value.is_some());
327        assert_eq!("bar::baz", value.unwrap());
328
329        let value = super::trim_graft("@foo::bar::baz");
330        assert!(value.is_some());
331        assert_eq!("@bar::baz", value.unwrap());
332
333        let value = super::trim_graft("%foo::bar::baz");
334        assert!(value.is_some());
335        assert_eq!("%bar::baz", value.unwrap());
336
337        let value = super::trim_graft(":foo::bar::baz");
338        assert!(value.is_some());
339        assert_eq!(":bar::baz", value.unwrap());
340
341        let value = super::trim_graft("foo::bar");
342        assert!(value.is_some());
343        assert_eq!("bar", value.unwrap());
344
345        let value = super::trim_graft("foo");
346        assert!(value.is_none());
347    }
348
349    #[test]
350    fn graft_basename() {
351        let value = super::graft_basename("foo");
352        assert!(value.is_none());
353
354        let value = super::graft_basename(":foo");
355        assert!(value.is_none());
356
357        let value = super::graft_basename("%foo");
358        assert!(value.is_none());
359
360        let value = super::graft_basename("@foo");
361        assert!(value.is_none());
362
363        let value = super::graft_basename("foo::bar");
364        assert!(value.is_some());
365        assert_eq!("foo", value.unwrap());
366
367        let value = super::graft_basename(":foo::bar");
368        assert!(value.is_some());
369        assert_eq!("foo", value.unwrap());
370
371        let value = super::graft_basename("%foo::bar");
372        assert!(value.is_some());
373        assert_eq!("foo", value.unwrap());
374
375        let value = super::graft_basename("@foo::bar");
376        assert!(value.is_some());
377        assert_eq!("foo", value.unwrap());
378
379        let value = super::graft_basename("foo::bar::baz");
380        assert!(value.is_some());
381        assert_eq!("foo", value.unwrap());
382
383        let value = super::graft_basename(":foo::bar::baz");
384        assert!(value.is_some());
385        assert_eq!("foo", value.unwrap());
386
387        let value = super::graft_basename("%foo::bar::baz");
388        assert!(value.is_some());
389        assert_eq!("foo", value.unwrap());
390
391        let value = super::graft_basename("@foo::bar::baz");
392        assert!(value.is_some());
393        assert_eq!("foo", value.unwrap());
394    }
395
396    #[test]
397    fn escape_shell_variables() {
398        let value = super::escape_shell_variables("$");
399        assert_eq!(value, "$");
400
401        let value = super::escape_shell_variables("$ ");
402        assert_eq!(value, "$ ");
403
404        let value = super::escape_shell_variables("$$");
405        assert_eq!(value, "$$");
406
407        let value = super::escape_shell_variables("$_");
408        assert_eq!(value, "$$_");
409
410        let value = super::escape_shell_variables("$a");
411        assert_eq!(value, "$$a");
412
413        let value = super::escape_shell_variables("$_a");
414        assert_eq!(value, "$$_a");
415
416        let value = super::escape_shell_variables("$ echo");
417        assert_eq!(value, "$ echo");
418
419        let value = super::escape_shell_variables("embedded $$ value");
420        assert_eq!(value, "embedded $$ value");
421
422        let value = super::escape_shell_variables("$variable");
423        assert_eq!(value, "$$variable");
424
425        let value = super::escape_shell_variables("$$variable");
426        assert_eq!(value, "$$variable");
427
428        let value = super::escape_shell_variables("${braces}${ignored}");
429        assert_eq!(value, "${braces}${ignored}");
430
431        let value = super::escape_shell_variables("$a ${b} $c $");
432        assert_eq!(value, "$$a ${b} $$c $");
433
434        // Escaped ${braced} value
435        let value = super::escape_shell_variables("echo $${value[@]:0:1}");
436        assert_eq!(value, "echo $${value[@]:0:1}");
437    }
438
439    #[test]
440    fn trim_and_split_shebang() {
441        assert!(super::is_shebang("#!test"));
442
443        let value = super::trim_shebang("#not-shebang\nvalue\n");
444        assert!(value.is_none());
445
446        let value = super::trim_shebang("#!test\nvalue\n");
447        assert!(value.is_some());
448        assert_eq!(value, Some("test\nvalue\n"));
449
450        let value = super::split_shebang("#not-shebang\nvalue\n");
451        assert!(value.is_none());
452
453        let value = super::split_shebang("#!test\nvalue\n");
454        assert_eq!(value, Some(("test", "value\n")));
455
456        let value = super::split_shebang("#comment\nvalue\n");
457        assert_eq!(value, None);
458    }
459}