@@ -27,181 +27,172 @@ enum State {
27 DquoteEscaped,
27 DquoteEscaped,
30−/// Get the vec of escaped / quoted / doublequoted filenames from the input str
30+pub struct Shellwords<'a> {
31−pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
32− use State::*;
31+ state: State,
32+ /// Shellwords where whitespace and escapes has been resolved.
33+ words: Vec<Cow<'a, str>>,
34+ /// The parts of the input that are divided into shellwords. This can be
35+ /// used to retrieve the original text for a given word by looking up the
36+ /// same index in the Vec as the word in `words`.
34− let mut state = Unquoted;
40+impl<'a> From<&'a str> for Shellwords<'a> {
35− let mut args: Vec<Cow<str>> = Vec::new();
41+ fn from(input: &'a str) -> Self {
36− let mut escaped = String::with_capacity(input.len());
42+ use State::*;
38− let mut start = 0;
44+ let mut state = Unquoted;
39− let mut end = 0;
45+ let mut words = Vec::new();
46+ let mut parts = Vec::new();
47+ let mut escaped = String::with_capacity(input.len());
49+ let mut part_start = 0;
50+ let mut unescaped_start = 0;
41− for (i, c) in input.char_indices() {
53+ for (i, c) in input.char_indices() {
42− state = match state {
54+ state = match state {
43− OnWhitespace => match c {
55+ OnWhitespace => match c {
48− '\'' => {
60+ '\'' => {
52− '\\' => {
64+ '\\' => {
53− if cfg!(unix) {
65+ if cfg!(unix) {
54− escaped.push_str(&input[start..i]);
66+ escaped.push_str(&input[unescaped_start..i]);
55− start = i + 1;
67+ unescaped_start = i + 1;
56− UnquotedEscaped
68+ UnquotedEscaped
73+ c if c.is_ascii_whitespace() => {
58 OnWhitespace
75 OnWhitespace
61− c if c.is_ascii_whitespace() => {
65− _ => Unquoted,
77+ _ => Unquoted,
67− Unquoted => match c {
79+ Unquoted => match c {
68− '\\' => {
80+ '\\' => {
69− if cfg!(unix) {
81+ if cfg!(unix) {
70− escaped.push_str(&input[start..i]);
82+ escaped.push_str(&input[unescaped_start..i]);
71− start = i + 1;
83+ unescaped_start = i + 1;
72− UnquotedEscaped
84+ UnquotedEscaped
77− c if c.is_ascii_whitespace() => {
89+ c if c.is_ascii_whitespace() => {
79− OnWhitespace
91+ OnWhitespace
83− UnquotedEscaped => Unquoted,
87− escaped.push_str(&input[start..i]);
100− QuoteEscaped => Quoted,
95+ UnquotedEscaped => Unquoted,
101− Dquoted => match c {
96+ Quoted => match c {
102− '\\' => {
97+ '\\' => {
103− if cfg!(unix) {
98+ if cfg!(unix) {
104− escaped.push_str(&input[start..i]);
99+ escaped.push_str(&input[unescaped_start..i]);
105− start = i + 1;
100+ unescaped_start = i + 1;
106− DquoteEscaped
101+ QuoteEscaped
107− } else {
102+ } else {
112+ QuoteEscaped => Quoted,
113+ Dquoted => match c {
116+ escaped.push_str(&input[unescaped_start..i]);
117+ unescaped_start = i + 1;
111− '"' => {
123+ '"' => {
112− end = i;
124+ end = i;
113− OnWhitespace
125+ OnWhitespace
115− _ => Dquoted,
127+ _ => Dquoted,
117− DquoteEscaped => Dquoted,
129+ DquoteEscaped => Dquoted,
120− if i >= input.len() - 1 && end == 0 {
132+ if i >= input.len() - 1 && end == 0 {
121− end = i + 1;
133+ end = i + 1;
124− if end > 0 {
136+ if end > 0 {
125− let esc_trim = escaped.trim();
137+ let esc_trim = escaped.trim();
126− let inp = &input[start..end];
138+ let inp = &input[unescaped_start..end];
128− if !(esc_trim.is_empty() && inp.trim().is_empty()) {
140+ if !(esc_trim.is_empty() && inp.trim().is_empty()) {
129− if esc_trim.is_empty() {
141+ if esc_trim.is_empty() {
130− args.push(inp.into());
142+ words.push(inp.into());
131− } else {
144+ } else {
132− args.push([escaped, inp.into()].concat().into());
145+ words.push([escaped, inp.into()].concat().into());
146+ parts.push(&input[part_start..end]);
133− escaped = "".to_string();
147+ escaped = "".to_string();
150+ unescaped_start = i + 1;
143−/// Checks that the input ends with an ascii whitespace character which is
156+ debug_assert!(words.len() == parts.len());
149−/// use helix_core::shellwords::ends_with_whitespace;
150−/// assert_eq!(ends_with_whitespace(" "), true);
151−/// assert_eq!(ends_with_whitespace(":open "), true);
152−/// assert_eq!(ends_with_whitespace(":open foo.txt "), true);
153−/// assert_eq!(ends_with_whitespace(":open"), false);
155−/// assert_eq!(ends_with_whitespace(":open a\\ "), false);
157−/// assert_eq!(ends_with_whitespace(":open a\\ b.txt"), false);
159−pub fn ends_with_whitespace(input: &str) -> bool {
162− // Fast-lane: the input must end with a whitespace character
158+ Self {
163− // regardless of quoting.
159+ state,
164− if !input.ends_with(|c: char| c.is_ascii_whitespace()) {
160+ words,
165− return false;
161+ parts,
168− let mut state = Unquoted;
166+impl<'a> Shellwords<'a> {
167+ /// Checks that the input ends with a whitespace character which is not escaped.
172+ /// use helix_core::shellwords::Shellwords;
173+ /// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true);
174+ /// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true);
175+ /// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true);
176+ /// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false);
178+ /// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), false);
180+ /// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false);
182+ pub fn ends_with_whitespace(&self) -> bool {
183+ matches!(self.state, State::OnWhitespace)
170− for c in input.chars() {
186+ /// Returns the list of shellwords calculated from the input string.
171− state = match state {
187+ pub fn words(&self) -> &[Cow<'a, str>] {
172− OnWhitespace => match c {
188+ &self.words
175− '\\' if cfg!(unix) => UnquotedEscaped,
176− '\\' => OnWhitespace,
177− c if c.is_ascii_whitespace() => OnWhitespace,
180− Unquoted => match c {
181− '\\' if cfg!(unix) => UnquotedEscaped,
183− c if c.is_ascii_whitespace() => OnWhitespace,
186− UnquotedEscaped => Unquoted,
188− '\\' if cfg!(unix) => QuoteEscaped,
190− '\'' => OnWhitespace,
193− QuoteEscaped => Quoted,
194− Dquoted => match c {
195− '\\' if cfg!(unix) => DquoteEscaped,
197− '"' => OnWhitespace,
200− DquoteEscaped => Dquoted,
204− matches!(state, OnWhitespace)
191+ /// Returns a list of strings which correspond to [`Self::words`] but represent the original
192+ /// text in the input string - including escape characters - without separating whitespace.
193+ pub fn parts(&self) -> &[&'a str] {
207#[cfg(test)]
198#[cfg(test)]
@@ -212,7 +203,8 @@ mod test {
212 #[cfg(windows)]
203 #[cfg(windows)]
213 fn test_normal() {
204 fn test_normal() {
214 let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
205 let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
215− let result = shellwords(input);
206+ let shellwords = Shellwords::from(input);
207+ let result = shellwords.words().to_vec();
216 let expected = vec![
208 let expected = vec![
217 Cow::from(":o"),
209 Cow::from(":o"),
218 Cow::from("single_word"),
210 Cow::from("single_word"),
@@ -230,7 +222,8 @@ mod test {
230 #[cfg(unix)]
222 #[cfg(unix)]
231 fn test_normal() {
223 fn test_normal() {
232 let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
224 let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
233− let result = shellwords(input);
225+ let shellwords = Shellwords::from(input);
226+ let result = shellwords.words().to_vec();
234 let expected = vec![
227 let expected = vec![
235 Cow::from(":o"),
228 Cow::from(":o"),
236 Cow::from("single_word"),
229 Cow::from("single_word"),
@@ -247,7 +240,8 @@ mod test {
247 fn test_quoted() {
240 fn test_quoted() {
248 let quoted =
241 let quoted =
249 r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
242 r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
250− let result = shellwords(quoted);
243+ let shellwords = Shellwords::from(quoted);
244+ let result = shellwords.words().to_vec();
251 let expected = vec![
245 let expected = vec![
252 Cow::from(":o"),
246 Cow::from(":o"),
253 Cow::from("single_word"),
247 Cow::from("single_word"),
@@ -262,7 +256,8 @@ mod test {
262 #[cfg(unix)]
256 #[cfg(unix)]
263 fn test_dquoted() {
257 fn test_dquoted() {
264 let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
258 let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
265− let result = shellwords(dquoted);
259+ let shellwords = Shellwords::from(dquoted);
260+ let result = shellwords.words().to_vec();
266 let expected = vec![
261 let expected = vec![
267 Cow::from(":o"),
262 Cow::from(":o"),
268 Cow::from("single_word"),
263 Cow::from("single_word"),
@@ -277,7 +272,8 @@ mod test {
277 #[cfg(unix)]
272 #[cfg(unix)]
278 fn test_mixed() {
273 fn test_mixed() {
279 let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
274 let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
280− let result = shellwords(dquoted);
275+ let shellwords = Shellwords::from(dquoted);
276+ let result = shellwords.words().to_vec();
281 let expected = vec![
277 let expected = vec![
282 Cow::from(":o"),
278 Cow::from(":o"),
283 Cow::from("single_word"),
279 Cow::from("single_word"),
@@ -298,7 +294,8 @@ mod test {
298 fn test_lists() {
294 fn test_lists() {
299 let input =
295 let input =
300 r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
296 r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "qoutes"]'"#;
301− let result = shellwords(input);
297+ let shellwords = Shellwords::from(input);
298+ let result = shellwords.words().to_vec();
302 let expected = vec![
299 let expected = vec![
303 Cow::from(":set"),
300 Cow::from(":set"),
304 Cow::from("statusline.center"),
301 Cow::from("statusline.center"),
@@ -322,4 +319,18 @@ mod test {
322 assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
319 assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
323 assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\""));
320 assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\""));
326+ assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
327+ assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\ "]);
333+ assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
334+ assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\"]);