From 32e5c2861a357c293908209cc05517a0e91dd945 Mon Sep 17 00:00:00 2001 From: sigoden Date: Sat, 19 Oct 2024 13:20:53 +0800 Subject: [PATCH] feat: better shell words escaping (#347) --- src/shell.rs | 188 +++++++++++++++--- .../integration__compgen__escape.snap | 10 +- .../integration__compgen__value.snap | 14 +- 3 files changed, 175 insertions(+), 37 deletions(-) diff --git a/src/shell.rs b/src/shell.rs index 3b1cf1ad..777f2de7 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -260,18 +260,150 @@ impl Shell { } } + pub(crate) fn need_escape_chars(&self) -> &[(char, u8)] { + // Flags: + // 1: escape-first-char + // 2: escape-middle-char + // 4: escape-last-char + match self { + Shell::Bash => &[ + (' ', 7), + ('!', 3), + ('"', 7), + ('#', 1), + ('$', 3), + ('&', 7), + ('\'', 7), + ('(', 7), + (')', 7), + (';', 7), + ('<', 7), + ('>', 7), + ('\\', 7), + ('`', 7), + ('|', 7), + ], + Shell::Elvish => &[], + Shell::Fish => &[], + Shell::Generic => &[], + Shell::Nushell => &[ + (' ', 7), + ('!', 1), + ('"', 7), + ('#', 1), + ('$', 1), + ('\'', 7), + ('(', 7), + (')', 7), + (';', 7), + ('[', 7), + ('`', 7), + ('{', 7), + ('|', 7), + ('}', 7), + ], + Shell::Powershell => &[ + (' ', 7), + ('"', 7), + ('#', 1), + ('$', 3), + ('&', 7), + ('\'', 7), + ('(', 7), + (')', 7), + (',', 7), + (';', 7), + ('<', 1), + ('>', 1), + ('@', 1), + (']', 1), + ('`', 7), + ('{', 7), + ('|', 7), + ('}', 7), + ], + Shell::Xonsh => &[ + (' ', 7), + ('!', 7), + ('"', 7), + ('#', 7), + ('$', 4), + ('&', 7), + ('\'', 7), + ('(', 7), + (')', 7), + ('*', 7), + (':', 1), + (';', 7), + ('<', 7), + ('=', 1), + ('>', 7), + ('[', 7), + ('\\', 4), + (']', 7), + ('^', 1), + ('`', 7), + ('{', 7), + ('|', 7), + ('}', 7), + ], + Shell::Zsh => &[ + (' ', 7), + ('!', 3), + ('"', 7), + ('#', 1), + ('$', 3), + ('&', 7), + ('\'', 7), + ('(', 7), + (')', 7), + ('*', 7), + (';', 7), + ('<', 7), + ('=', 1), + ('>', 7), + ('?', 7), + ('[', 7), + ('\\', 7), + ('`', 7), + ('|', 7), + ('~', 1), + ], + Shell::Tcsh => &[ + (' ', 7), + ('!', 3), + ('"', 7), + ('$', 3), + ('&', 7), + ('\'', 7), + ('(', 7), + (')', 7), + ('*', 7), + (';', 7), + ('<', 7), + ('>', 7), + ('?', 7), + ('\\', 7), + ('`', 7), + ('{', 7), + ('|', 7), + ('~', 1), + ], + } + } + pub(crate) fn escape(&self, value: &str) -> String { match self { - Shell::Bash => Self::escape_chars(value, self.need_escape_chars(), "\\"), + Shell::Bash | Shell::Tcsh => Self::escape_chars(value, self.need_escape_chars(), "\\"), + Shell::Elvish | Shell::Fish | Shell::Generic => value.into(), Shell::Nushell | Shell::Powershell | Shell::Xonsh => { - if Self::contains_chars(value, self.need_escape_chars()) { + if Self::contains_escape_chars(value, self.need_escape_chars()) { format!("'{value}'") } else { value.into() } } Shell::Zsh => Self::escape_chars(value, self.need_escape_chars(), "\\\\"), - _ => value.into(), } } @@ -308,17 +440,6 @@ impl Shell { } } - pub(crate) fn need_escape_chars(&self) -> &str { - match self { - Shell::Bash => r#"()<>"'` !#$&;\|"#, - Shell::Nushell => r#"()[]{}"'` #$;|"#, - Shell::Powershell => r#"()<>[]{}"'` #$&;@|"#, - Shell::Xonsh => r#"()<>[]{}!"'` #&;|"#, - Shell::Zsh => r#"()<>[]"'` !#$&*;?\|"#, - _ => "", - } - } - pub(crate) fn need_break_chars(&self, runtime: T, last_arg: &str) -> Vec { if last_arg.starts_with(is_quote_char) { return vec![]; @@ -399,12 +520,14 @@ impl Shell { Some(prefix) } - fn escape_chars(value: &str, need_escape: &str, for_escape: &str) -> String { - let chars: Vec = need_escape.chars().collect(); - value - .chars() - .map(|c| { - if chars.contains(&c) { + fn escape_chars(value: &str, need_escape_chars: &[(char, u8)], for_escape: &str) -> String { + let chars: Vec = value.chars().collect(); + let len = chars.len(); + chars + .into_iter() + .enumerate() + .map(|(i, c)| { + if Self::match_escape_chars(need_escape_chars, c, i, len) { format!("{for_escape}{c}") } else { c.to_string() @@ -413,9 +536,28 @@ impl Shell { .collect() } - fn contains_chars(value: &str, chars: &str) -> bool { - let value_chars: Vec = value.chars().collect(); - chars.chars().any(|v| value_chars.contains(&v)) + fn contains_escape_chars(value: &str, need_escape_chars: &[(char, u8)]) -> bool { + let chars: Vec = value.chars().collect(); + chars + .iter() + .enumerate() + .any(|(i, c)| Self::match_escape_chars(need_escape_chars, *c, i, chars.len())) + } + + fn match_escape_chars(need_escape_chars: &[(char, u8)], c: char, i: usize, len: usize) -> bool { + need_escape_chars.iter().any(|(ch, flag)| { + if *ch == c { + if i == 0 { + (*flag & 1) != 0 + } else if i == len - 1 { + (*flag & 4) != 0 + } else { + (*flag & 2) != 0 + } + } else { + false + } + }) } fn sanitize_tcsh_value(value: &str) -> String { diff --git a/tests/snapshots/integration__compgen__escape.snap b/tests/snapshots/integration__compgen__escape.snap index 4751911e..c1a1cd8c 100644 --- a/tests/snapshots/integration__compgen__escape.snap +++ b/tests/snapshots/integration__compgen__escape.snap @@ -19,8 +19,8 @@ a:b>c d:e>f ************ COMPGEN Powershell `prog --oa ` ************ -'a:b>c' 1 a:b>c 39 -'d:e>f' 1 d:e>f 39 +a:b>c 1 a:b>c 39 +d:e>f 1 d:e>f 39 ************ COMPGEN Xonsh `prog --oa ` ************ 'a:b>c' 1 a:b>c @@ -31,7 +31,5 @@ a\:b\\>c a\:b>c a:b>c 39 d\:e\\>f d\:e>f d:e>f 39 ************ COMPGEN Tcsh `prog --oa ` ************ -a:b>c -d:e>f - - +a:b\>c +d:e\>f diff --git a/tests/snapshots/integration__compgen__value.snap b/tests/snapshots/integration__compgen__value.snap index 5a45c6f0..6a737047 100644 --- a/tests/snapshots/integration__compgen__value.snap +++ b/tests/snapshots/integration__compgen__value.snap @@ -39,8 +39,8 @@ abc>xyz 'abc xyz' 1 abc xyz 39 abc:def 1 abc:def 39 abc:xyz 1 abc:xyz 39 -'abc>def' 1 abc>def 39 -'abc>xyz' 1 abc>xyz 39 +abc>def 1 abc>def 39 +abc>xyz 1 abc>xyz 39 ************ COMPGEN Xonsh `prog --oa abc` ************ 'abc def' 1 abc def @@ -59,11 +59,9 @@ abc\\>def abc>def abc>def 39 abc\\>xyz abc>xyz abc>xyz 39 ************ COMPGEN Tcsh `prog --oa abc` ************ -abc⠀def -abc⠀xyz +abc\⠀def +abc\⠀xyz abc:def abc:xyz -abc>def -abc>xyz - - +abc\>def +abc\>xyz