From 11a11fb4f2ff62502021919b01a46195149475fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Daron?= Date: Wed, 3 Jul 2024 09:39:58 +0200 Subject: [PATCH] command expansion: parsing expansions in shellwords accordingly. --- helix-core/src/shellwords.rs | 164 ++++++++++++-------- helix-term/src/commands.rs | 3 - helix-term/src/commands/typed.rs | 25 ++- helix-view/src/editor/variable_expansion.rs | 18 ++- 4 files changed, 122 insertions(+), 88 deletions(-) diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index 9d873c366..9a5ab37cb 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -45,88 +45,114 @@ impl<'a> From<&'a str> for Shellwords<'a> { let mut words = Vec::new(); let mut parts = Vec::new(); let mut escaped = String::with_capacity(input.len()); + let mut inside_variable_expansion = false; let mut part_start = 0; let mut unescaped_start = 0; let mut end = 0; for (i, c) in input.char_indices() { - state = match state { - OnWhitespace => match c { - '"' => { - end = i; - Dquoted - } - '\'' => { - end = i; - Quoted - } - '\\' => { - if cfg!(unix) { - escaped.push_str(&input[unescaped_start..i]); - unescaped_start = i + 1; - UnquotedEscaped - } else { - OnWhitespace + if !inside_variable_expansion { + if c == '%' { + //%sh{this "should" be escaped} + if let Some(t) = input.get(i + 1..i + 3) { + if t == "sh" { + inside_variable_expansion = true; } } - c if c.is_ascii_whitespace() => { - end = i; - OnWhitespace - } - _ => Unquoted, - }, - Unquoted => match c { - '\\' => { - if cfg!(unix) { - escaped.push_str(&input[unescaped_start..i]); - unescaped_start = i + 1; - UnquotedEscaped - } else { - Unquoted + //%{this "should" be escaped} + if let Some(t) = input.get(i + 1..i + 2) { + if t == "{" { + inside_variable_expansion = true; } } - c if c.is_ascii_whitespace() => { - end = i; - OnWhitespace - } - _ => Unquoted, - }, - UnquotedEscaped => Unquoted, - Quoted => match c { - '\\' => { - if cfg!(unix) { - escaped.push_str(&input[unescaped_start..i]); - unescaped_start = i + 1; - QuoteEscaped - } else { + } + } else { + if c == '}' { + inside_variable_expansion = false; + } + } + + state = if !inside_variable_expansion { + match state { + OnWhitespace => match c { + '"' => { + end = i; + Dquoted + } + '\'' => { + end = i; Quoted } - } - '\'' => { - end = i; - OnWhitespace - } - _ => Quoted, - }, - QuoteEscaped => Quoted, - Dquoted => match c { - '\\' => { - if cfg!(unix) { - escaped.push_str(&input[unescaped_start..i]); - unescaped_start = i + 1; - DquoteEscaped - } else { - Dquoted + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + UnquotedEscaped + } else { + OnWhitespace + } } - } - '"' => { - end = i; - OnWhitespace - } - _ => Dquoted, - }, - DquoteEscaped => Dquoted, + c if c.is_ascii_whitespace() => { + end = i; + Unquoted + } + _ => Unquoted, + }, + Unquoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + UnquotedEscaped + } else { + Unquoted + } + } + c if c.is_ascii_whitespace() => { + end = i; + OnWhitespace + } + _ => Unquoted, + }, + UnquotedEscaped => Unquoted, + Quoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + QuoteEscaped + } else { + Quoted + } + } + '\'' => { + end = i; + OnWhitespace + } + _ => Quoted, + }, + QuoteEscaped => Quoted, + Dquoted => match c { + '\\' => { + if cfg!(unix) { + escaped.push_str(&input[unescaped_start..i]); + unescaped_start = i + 1; + DquoteEscaped + } else { + Dquoted + } + } + '"' => { + end = i; + OnWhitespace + } + _ => Dquoted, + }, + DquoteEscaped => Dquoted, + } + } else { + state }; let c_len = c.len_utf8(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48da41835..6d8bce9e5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -225,11 +225,8 @@ impl MappableCommand { scroll: None, }; - let args = args.join(" "); match cx.editor.expand_variables(&args) { Ok(args) => { - let args = args.split_whitespace(); - let args: Vec> = args.map(Cow::Borrowed).collect(); if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 25e6efd44..c74d82cb8 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3194,17 +3194,6 @@ pub(super) fn command_mode(cx: &mut Context) { } }, // completion move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - let input: Cow = if event == PromptEvent::Validate { - match cx.editor.expand_variables(input) { - Ok(args) => args, - Err(e) => { - cx.editor.set_error(format!("{}", e)); - return; - } - } - } else { - Cow::Borrowed(input) - }; let parts = input.split_whitespace().collect::>(); if parts.is_empty() { return; @@ -3221,8 +3210,18 @@ pub(super) fn command_mode(cx: &mut Context) { // Handle typable commands if let Some(cmd) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) { let shellwords = Shellwords::from(input.as_ref()); - let args = shellwords.words(); - + let words = shellwords.words().to_vec(); + let args = if event == PromptEvent::Validate { + match cx.editor.expand_variables(&words) { + Ok(args) => args, + Err(e) => { + cx.editor.set_error(format!("{}", e)); + return; + } + } + } else { + words + }; if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } diff --git a/helix-view/src/editor/variable_expansion.rs b/helix-view/src/editor/variable_expansion.rs index d86a8b1f2..f79d24c0e 100644 --- a/helix-view/src/editor/variable_expansion.rs +++ b/helix-view/src/editor/variable_expansion.rs @@ -2,7 +2,20 @@ use crate::Editor; use std::borrow::Cow; impl Editor { - pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result> { + pub fn expand_variables<'a>( + &self, + args: &'a Vec>, + ) -> anyhow::Result>> { + let mut output = Vec::with_capacity(args.len()); + for arg in args { + if let Ok(s) = self.expand_arg(arg) { + output.push(s); + } + } + + Ok(output) + } + fn expand_arg<'a>(&self, input: &'a str) -> anyhow::Result> { let (view, doc) = current_ref!(self); let shell = &self.config().shell; @@ -81,8 +94,7 @@ impl Editor { } if let Some(o) = output.as_mut() { - let body = - self.expand_variables(&input[index + 4..end])?; + let body = self.expand_arg(&input[index + 4..end])?; let output = tokio::task::block_in_place(move || { helix_lsp::block_on(async move {