@@ -17,18 +17,18 @@ pub fn escape(input: &str) -> Cow<'_, str> {
1717 }
1818}
1919
20+enum State {
21+ OnWhitespace,
22+ Unquoted,
23+ UnquotedEscaped,
24+ Quoted,
25+ QuoteEscaped,
26+ Dquoted,
27+ DquoteEscaped,
28+}
29+
2030/// Get the vec of escaped / quoted / doublequoted filenames from the input str
2131pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
22− enum State {
23− OnWhitespace,
24− Unquoted,
25− UnquotedEscaped,
26− Quoted,
27− QuoteEscaped,
28− Dquoted,
29− DquoteEscaped,
30− }
31−
3232 use State::*;
3333
3434 let mut state = Unquoted;
@@ -140,6 +140,70 @@ pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
140140 args
141141}
142142
143+/// Checks that the input ends with an ascii whitespace character which is
144+/// not escaped.
145+///
146+/// # Examples
147+///
148+/// ```rust
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);
154+/// #[cfg(unix)]
155+/// assert_eq!(ends_with_whitespace(":open a\\ "), false);
156+/// #[cfg(unix)]
157+/// assert_eq!(ends_with_whitespace(":open a\\ b.txt"), false);
158+/// ```
159+pub fn ends_with_whitespace(input: &str) -> bool {
160+ use State::*;
161+
162+ // Fast-lane: the input must end with a whitespace character
163+ // regardless of quoting.
164+ if !input.ends_with(|c: char| c.is_ascii_whitespace()) {
165+ return false;
166+ }
167+
168+ let mut state = Unquoted;
169+
170+ for c in input.chars() {
171+ state = match state {
172+ OnWhitespace => match c {
173+ '"' => Dquoted,
174+ '\'' => Quoted,
175+ '\\' if cfg!(unix) => UnquotedEscaped,
176+ '\\' => OnWhitespace,
177+ c if c.is_ascii_whitespace() => OnWhitespace,
178+ _ => Unquoted,
179+ },
180+ Unquoted => match c {
181+ '\\' if cfg!(unix) => UnquotedEscaped,
182+ '\\' => Unquoted,
183+ c if c.is_ascii_whitespace() => OnWhitespace,
184+ _ => Unquoted,
185+ },
186+ UnquotedEscaped => Unquoted,
187+ Quoted => match c {
188+ '\\' if cfg!(unix) => QuoteEscaped,
189+ '\\' => Quoted,
190+ '\'' => OnWhitespace,
191+ _ => Quoted,
192+ },
193+ QuoteEscaped => Quoted,
194+ Dquoted => match c {
195+ '\\' if cfg!(unix) => DquoteEscaped,
196+ '\\' => Dquoted,
197+ '"' => OnWhitespace,
198+ _ => Dquoted,
199+ },
200+ DquoteEscaped => Dquoted,
201+ }
202+ }
203+
204+ matches!(state, OnWhitespace)
205+}
206+
143207#[cfg(test)]
144208mod test {
145209 use super::*;