diff --git a/examples/methods.out b/examples/methods.out index f60f29e72bea01e4a1403169e2f4b9aece321f08..030f77465e345b37456737878a0b00efec34cd23 100644 --- a/examples/methods.out +++ b/examples/methods.out @@ -62,7 +62,9 @@ two three one two three one\ntwo\nthree -one\ntwo\nthree +one\ +two\ +three one two three apple sauce diff --git a/src/lib/parser/shell_expand/mod.rs b/src/lib/parser/shell_expand/mod.rs index 2f607991b18c4f12dfe4584a0afc695ef883d64f..e67284e46f49b01cbcd255fe0a1b1d19ccfe7753 100644 --- a/src/lib/parser/shell_expand/mod.rs +++ b/src/lib/parser/shell_expand/mod.rs @@ -203,13 +203,16 @@ pub(crate) fn expand_string<E: Expander>( loop { match word_iterator.next() { Some(word) => { - if let WordToken::Brace(_) = word { - contains_brace = true; + match word { + WordToken::Brace(_) => { + contains_brace = true; + } + _ => (), } token_buffer.push(word); } None if original.is_empty() => { - token_buffer.push(WordToken::Normal("", true, false)); + token_buffer.push(WordToken::Normal("".into(), true, false)); break; } None => break, @@ -300,12 +303,12 @@ fn expand_braces<E: Expander>( slice(&mut output, expanded, index.clone()); } - WordToken::Normal(text, do_glob, tilde) => { + WordToken::Normal(ref text, do_glob, tilde) => { expand( &mut output, &mut expanded_words, expand_func, - text, + text.as_ref(), do_glob, tilde, ); @@ -407,12 +410,12 @@ fn expand_single_string_token<E: Expander>( match *token { WordToken::StringMethod(ref method) => method.handle(&mut output, expand_func), - WordToken::Normal(text, do_glob, tilde) => { + WordToken::Normal(ref text, do_glob, tilde) => { expand( &mut output, &mut expanded_words, expand_func, - text, + text.as_ref(), do_glob, tilde, ); @@ -552,12 +555,12 @@ pub(crate) fn expand_tokens<E: Expander>( method.handle(&mut output, expand_func); } WordToken::Brace(_) => unreachable!(), - WordToken::Normal(text, do_glob, tilde) => { + WordToken::Normal(ref text, do_glob, tilde) => { expand( &mut output, &mut expanded_words, expand_func, - text, + text.as_ref(), do_glob, tilde, ); diff --git a/src/lib/parser/shell_expand/words/mod.rs b/src/lib/parser/shell_expand/words/mod.rs index 24d686ee968a8a46bd4d3d77adca5f4468cd3bea..eb71d50d1a8627aae18cc5f6d125fdb3e1888bd0 100644 --- a/src/lib/parser/shell_expand/words/mod.rs +++ b/src/lib/parser/shell_expand/words/mod.rs @@ -12,6 +12,8 @@ pub(crate) use self::{ select::{Select, SelectWithSize}, }; use super::{super::ArgumentSplitter, expand_string, Expander}; +use shell::escape::unescape; +use std::borrow::Cow; // Bit Twiddling Guide: // var & FLAG != 0 checks if FLAG is enabled @@ -32,7 +34,7 @@ bitflags! { pub(crate) enum WordToken<'a> { /// Represents a normal string who may contain a globbing character /// (the second element) or a tilde expression (the third element) - Normal(&'a str, bool, bool), + Normal(Cow<'a, str>, bool, bool), Whitespace(&'a str), // Tilde(&'a str), Brace(Vec<&'a str>), @@ -642,7 +644,7 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { return None; } - let mut iterator = self.data.bytes().skip(self.read); + let mut iterator = self.data.bytes().skip(self.read).peekable(); let mut start = self.read; let mut glob = false; let mut tilde = false; @@ -710,7 +712,7 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { Some(b' ') | None => { self.read += 1; let output = &self.data[start..self.read]; - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(output.into(), glob, tilde)); } _ => { self.read += 1; @@ -737,7 +739,7 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { Some(b' ') | None => { self.read += 1; let output = &self.data[start..self.read]; - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(output.into(), glob, tilde)); } _ => { self.read += 1; @@ -763,48 +765,49 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { match character { _ if self.flags.contains(Flags::BACKSL) => self.flags ^= Flags::BACKSL, b'\\' if !self.flags.contains(Flags::SQUOTE) => { - self.flags ^= Flags::BACKSL; - - let include_backsl = self.flags.contains(Flags::DQUOTE) - && iterator.clone().next().map_or(false, |nextch| { - nextch != b'$' && nextch != b'\\' && nextch != b'"' - }); + pub(crate) fn maybe_unescape(input: &str, contains_escapeable: bool) -> Cow<str> { + if !contains_escapeable { + input.into() + } else { + unescape(input) + } + } - let end = if include_backsl { - self.read + 1 - } else { - self.read - }; - let output = &self.data[start..end]; + let next = iterator.next(); self.read += 1; - return Some(WordToken::Normal(output, glob, tilde)); + + if self.flags.contains(Flags::DQUOTE) { + let _ = iterator.next(); + self.read += 1; + return Some(WordToken::Normal(maybe_unescape(&self.data[start..self.read], next.map_or(true, |c| c == b'$' || c == b'@' || c == b'\\' || c == b'"')), glob, tilde)); + } } b'\'' if !self.flags.contains(Flags::DQUOTE) => { self.flags ^= Flags::SQUOTE; let output = &self.data[start..self.read]; self.read += 1; - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(output.into(), glob, tilde)); } b'"' if !self.flags.contains(Flags::SQUOTE) => { self.flags ^= Flags::DQUOTE; let output = &self.data[start..self.read]; self.read += 1; - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(output.into(), glob, tilde)); } b' ' | b'{' if !self.flags.intersects(Flags::SQUOTE | Flags::DQUOTE) => { - return Some(WordToken::Normal(&self.data[start..self.read], glob, tilde)) + return Some(WordToken::Normal(unescape(&self.data[start..self.read]), glob, tilde)) } b'$' | b'@' if !self.flags.contains(Flags::SQUOTE) => { if let Some(&character) = self.data.as_bytes().get(self.read) { if character == b' ' { self.read += 1; let output = &self.data[start..self.read]; - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(output.into(), glob, tilde)); } } let output = &self.data[start..self.read]; if output != "" { - return Some(WordToken::Normal(output, glob, tilde)); + return Some(WordToken::Normal(unescape(output), glob, tilde)); } else { return self.next(); }; @@ -813,7 +816,7 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { if self.glob_check(&mut iterator) { glob = true; } else { - return Some(WordToken::Normal(&self.data[start..self.read], glob, tilde)); + return Some(WordToken::Normal(self.data[start..self.read].into(), glob, tilde)); } } b'*' | b'?' if !self.flags.contains(Flags::SQUOTE) => { @@ -827,7 +830,7 @@ impl<'a, E: Expander + 'a> Iterator for WordIterator<'a, E> { if start == self.read { None } else { - Some(WordToken::Normal(&self.data[start..], glob, tilde)) + Some(WordToken::Normal(unescape(&self.data[start..]), glob, tilde)) } } } diff --git a/src/lib/parser/shell_expand/words/tests.rs b/src/lib/parser/shell_expand/words/tests.rs index 088e26b8b3680cae6cce810e0586d5c5cb676632..b03a965c68a8475e7de825f7c1b6a76d45814bc4 100644 --- a/src/lib/parser/shell_expand/words/tests.rs +++ b/src/lib/parser/shell_expand/words/tests.rs @@ -47,12 +47,11 @@ fn string_method() { #[test] fn escape_with_backslash() { - let input = "\\$FOO\\$BAR \\$FOO"; + let input = r#"\$FOO\$BAR \$FOO"#; let expected = vec![ - WordToken::Normal("$FOO", false, false), - WordToken::Normal("$BAR", false, false), + WordToken::Normal("$FOO$BAR".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("$FOO", false, false), + WordToken::Normal("$FOO".into(), false, false), ]; compare(input, expected); } @@ -99,7 +98,7 @@ fn array_process_within_string_process() { compare( "echo $(let free=[@(free -h)]; echo @free[6]@free[8]/@free[7])", vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Process( "let free=[@(free -h)]; echo @free[6]@free[8]/@free[7]", @@ -152,7 +151,7 @@ fn string_keys() { fn nested_processes() { let input = "echo $(echo $(echo one)) $(echo one $(echo two) three)"; let expected = vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Process("echo $(echo one)", false, Select::All), WordToken::Whitespace(" "), @@ -165,7 +164,7 @@ fn nested_processes() { fn words_process_with_quotes() { let input = "echo $(git branch | rg '[*]' | awk '{print $2}')"; let expected = vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Process( "git branch | rg '[*]' | awk '{print $2}'", @@ -177,7 +176,7 @@ fn words_process_with_quotes() { let input = "echo $(git branch | rg \"[*]\" | awk '{print $2}')"; let expected = vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Process( "git branch | rg \"[*]\" | awk '{print $2}'", @@ -192,16 +191,16 @@ fn words_process_with_quotes() { fn test_words() { let input = "echo $ABC \"${ABC}\" one{$ABC,$ABC} ~ $(echo foo) \"$(seq 1 100)\""; let expected = vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Variable("ABC", false, Select::All), WordToken::Whitespace(" "), WordToken::Variable("ABC", true, Select::All), WordToken::Whitespace(" "), - WordToken::Normal("one", false, false), + WordToken::Normal("one".into(), false, false), WordToken::Brace(vec!["$ABC", "$ABC"]), WordToken::Whitespace(" "), - WordToken::Normal("~", false, true), + WordToken::Normal("~".into(), false, true), WordToken::Whitespace(" "), WordToken::Process("echo foo", false, Select::All), WordToken::Whitespace(" "), @@ -214,13 +213,9 @@ fn test_words() { fn test_multiple_escapes() { let input = "foo\\(\\) bar\\(\\)"; let expected = vec![ - WordToken::Normal("foo", false, false), - WordToken::Normal("(", false, false), - WordToken::Normal(")", false, false), + WordToken::Normal("foo()".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("bar", false, false), - WordToken::Normal("(", false, false), - WordToken::Normal(")", false, false), + WordToken::Normal("bar()".into(), false, false), ]; compare(input, expected); } @@ -229,7 +224,7 @@ fn test_multiple_escapes() { fn test_arithmetic() { let input = "echo $((foo bar baz bing 3 * 2))"; let expected = vec![ - WordToken::Normal("echo", false, false), + WordToken::Normal("echo".into(), false, false), WordToken::Whitespace(" "), WordToken::Arithmetic("foo bar baz bing 3 * 2"), ]; @@ -240,9 +235,9 @@ fn test_arithmetic() { fn test_globbing() { let input = "barbaz* bingcrosb*"; let expected = vec![ - WordToken::Normal("barbaz*", true, false), + WordToken::Normal("barbaz*".into(), true, false), WordToken::Whitespace(" "), - WordToken::Normal("bingcrosb*", true, false), + WordToken::Normal("bingcrosb*".into(), true, false), ]; compare(input, expected); } @@ -251,15 +246,15 @@ fn test_globbing() { fn test_empty_strings() { let input = "rename '' 0 a \"\""; let expected = vec![ - WordToken::Normal("rename", false, false), + WordToken::Normal("rename".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("", false, false), + WordToken::Normal("".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("0", false, false), + WordToken::Normal("0".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("a", false, false), + WordToken::Normal("a".into(), false, false), WordToken::Whitespace(" "), - WordToken::Normal("", false, false), + WordToken::Normal("".into(), false, false), ]; compare(input, expected); } diff --git a/src/lib/shell/completer.rs b/src/lib/shell/completer.rs index bdcde9798689b77e64927b1ae491e6f39c6f75a5..21e856f485d6cb9da91839d8d7ccf9a2dfd7388f 100644 --- a/src/lib/shell/completer.rs +++ b/src/lib/shell/completer.rs @@ -1,4 +1,4 @@ -use super::{directory_stack::DirectoryStack, variables::Variables}; +use super::{directory_stack::DirectoryStack, escape::{escape, unescape}, variables::Variables}; use glob::glob; use liner::{Completer, FilenameCompleter}; use smallvec::SmallVec; @@ -149,40 +149,6 @@ where }) } -/// Escapes filenames from the completer so that special characters will be properly escaped. -/// -/// NOTE: Perhaps we should submit a PR to Liner to add a &'static [u8] field to -/// `FilenameCompleter` so that we don't have to perform the escaping ourselves? -fn escape(input: &str) -> String { - let mut output = Vec::with_capacity(input.len()); - for character in input.bytes() { - match character { - b'(' | b')' | b'[' | b']' | b'&' | b'$' | b'@' | b'{' | b'}' | b'<' | b'>' | b';' - | b'"' | b'\'' | b'#' | b'^' | b'*' => output.push(b'\\'), - _ => (), - } - output.push(character); - } - unsafe { String::from_utf8_unchecked(output) } -} - -/// Unescapes filenames to be passed into the completer -fn unescape(input: &str) -> String { - let mut output = Vec::with_capacity(input.len()); - let mut bytes = input.bytes(); - while let Some(b) = bytes.next() { - match b { - b'\\' => if let Some(next) = bytes.next() { - output.push(next); - } else { - output.push(b'\\') - }, - _ => output.push(b), - } - } - unsafe { String::from_utf8_unchecked(output) } -} - /// A completer that combines suggestions from multiple completers. #[derive(Clone, Eq, PartialEq)] pub(crate) struct MultiCompleter<A, B> diff --git a/src/lib/shell/escape.rs b/src/lib/shell/escape.rs new file mode 100644 index 0000000000000000000000000000000000000000..c23d3060f867c163b4e57952427d89f2160b4123 --- /dev/null +++ b/src/lib/shell/escape.rs @@ -0,0 +1,31 @@ +use std::borrow::Cow; + +/// Escapes filenames from the completer so that special characters will be properly escaped. +/// +/// NOTE: Perhaps we should submit a PR to Liner to add a &'static [u8] field to +/// `FilenameCompleter` so that we don't have to perform the escaping ourselves? +pub(crate) fn escape(input: &str) -> String { + let mut output = Vec::with_capacity(input.len()); + for character in input.bytes() { + match character { + b'(' | b')' | b'[' | b']' | b'&' | b'$' | b'@' | b'{' | b'}' | b'<' | b'>' | b';' + | b'"' | b'\'' | b'#' | b'^' | b'*' => output.push(b'\\'), + _ => (), + } + output.push(character); + } + unsafe { String::from_utf8_unchecked(output) } +} + +/// Unescapes filenames to be passed into the completer +pub(crate) fn unescape(input: &str) -> Cow<str> { + let mut input: Cow<str> = input.into(); + while let Some(found) = input.find('\\') { + if input.as_ref().chars().skip(found + 1).next().is_some() { + input.to_mut().remove(found); + } else { + break; + } + } + input +} diff --git a/src/lib/shell/mod.rs b/src/lib/shell/mod.rs index f8cb21febde67c5d62a2b6eedc8fd6f439b5f99e..a39642d7c4869e218f37f9aa4cf0835760268e57 100644 --- a/src/lib/shell/mod.rs +++ b/src/lib/shell/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod binary; pub(crate) mod colors; mod completer; pub(crate) mod directory_stack; +pub(crate) mod escape; pub mod flags; mod flow; pub(crate) mod flow_control;