command expansion: parsing expansions in shellwords accordingly.

This commit is contained in:
Théo Daron 2024-07-03 09:39:58 +02:00
parent 731985c133
commit 11a11fb4f2
4 changed files with 123 additions and 89 deletions

View File

@ -45,88 +45,114 @@ fn from(input: &'a str) -> Self {
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 {
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 {
}
} else {
if c == '}' {
inside_variable_expansion = false;
}
}
state = if !inside_variable_expansion {
match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
OnWhitespace
}
}
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();

View File

@ -225,11 +225,8 @@ pub fn execute(&self, cx: &mut Context) {
scroll: None,
};
let args = args.join(" ");
match cx.editor.expand_variables(&args) {
Ok(args) => {
let args = args.split_whitespace();
let args: Vec<Cow<str>> = args.map(Cow::Borrowed).collect();
if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate)
{
cx.editor.set_error(format!("{}", e));

View File

@ -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<str> = 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::<Vec<&str>>();
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));
}

View File

@ -2,7 +2,20 @@
use std::borrow::Cow;
impl Editor {
pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result<Cow<'a, str>> {
pub fn expand_variables<'a>(
&self,
args: &'a Vec<Cow<'a, str>>,
) -> anyhow::Result<Vec<Cow<'a, str>>> {
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<Cow<'a, str>> {
let (view, doc) = current_ref!(self);
let shell = &self.config().shell;
@ -81,8 +94,7 @@ pub fn expand_variables<'a>(&self, input: &'a str) -> anyhow::Result<Cow<'a, str
}
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 {