Skip to content

Commit

Permalink
feat: better shell words escaping (#347)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden authored Oct 19, 2024
1 parent 5ba3eda commit 32e5c28
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 37 deletions.
188 changes: 165 additions & 23 deletions src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -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<T: Runtime>(&self, runtime: T, last_arg: &str) -> Vec<char> {
if last_arg.starts_with(is_quote_char) {
return vec![];
Expand Down Expand Up @@ -399,12 +520,14 @@ impl Shell {
Some(prefix)
}

fn escape_chars(value: &str, need_escape: &str, for_escape: &str) -> String {
let chars: Vec<char> = 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<char> = 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()
Expand All @@ -413,9 +536,28 @@ impl Shell {
.collect()
}

fn contains_chars(value: &str, chars: &str) -> bool {
let value_chars: Vec<char> = 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<char> = 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 {
Expand Down
10 changes: 4 additions & 6 deletions tests/snapshots/integration__compgen__escape.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
14 changes: 6 additions & 8 deletions tests/snapshots/integration__compgen__value.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -59,11 +59,9 @@ abc\\>def abc>def abc>def 39
abc\\>xyz abc>xyz abc>xyz 39

************ COMPGEN Tcsh `prog --oa abc` ************
abcdef
abcxyz
abc\def
abc\xyz
abc:def
abc:xyz
abc>def
abc>xyz


abc\>def
abc\>xyz

0 comments on commit 32e5c28

Please sign in to comment.