don't run subcommand if it's surrounded with backtick quote (#14210)

# Description
Fixes: #14202
After looking into the issue, I think #13910 it's not good to cut the
span if it's in external argument.
This pr is somehow revert the change, and fix
https://github.com/nushell/nushell/issues/13431 in another way.

It introduce a new state named `State::BackTickQuote`, so if an external
arg include backtick quote, it enters the state, so backtick quote won't
be the body of a string.

# User-Facing Changes
### Before
```nushell
> ^echo `(echo aa)`
aa
> ^echo `"aa"`   # maybe it's not right to remove the inner quote.
aa
```
### After
```nushell
> ^echo `(echo aa)`
(echo aa)
> ^echo `"aa"`    # inner quote is keeped if there are backtick quote outside.
"aa"
```

# Tests + Formatting
Added 3 tests.
This commit is contained in:
Wind 2024-10-31 23:13:05 +08:00 committed by GitHub
parent 4907575d3d
commit 0a2fb137af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 43 additions and 32 deletions

View file

@ -240,36 +240,23 @@ fn parse_unknown_arg(
/// string, where each balanced pair of quotes is parsed as a separate part of the string, and then
/// concatenated together.
///
/// `keep_surround_backtick_quote` should be true when parsing it as command name. Or else it
/// should be false.
///
/// For example, `-foo="bar\nbaz"` becomes `$"-foo=bar\nbaz"`
fn parse_external_string(
working_set: &mut StateWorkingSet,
mut span: Span,
keep_surround_bakctick_quote: bool,
) -> Expression {
let mut contents = working_set.get_span_contents(span);
fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expression {
let contents = working_set.get_span_contents(span);
if !keep_surround_bakctick_quote
&& contents.len() > 1
&& contents.starts_with(b"`")
&& contents.ends_with(b"`")
{
contents = &contents[1..contents.len() - 1];
// backtick quote is useless in this case, so span is required to updated.
span = Span::new(span.start + 1, span.end - 1);
}
if contents.starts_with(b"r#") {
parse_raw_string(working_set, span)
} else if contents
.iter()
.any(|b| matches!(b, b'"' | b'\'' | b'(' | b')'))
.any(|b| matches!(b, b'"' | b'\'' | b'(' | b')' | b'`'))
{
enum State {
Bare {
from: usize,
},
BackTickQuote {
from: usize,
},
Quote {
from: usize,
quote_char: u8,
@ -320,6 +307,12 @@ fn parse_external_string(
continue;
}
}
b'`' => {
if index != *from {
spans.push(make_span(*from, index))
}
state = State::BackTickQuote { from: index }
}
// Continue to consume
_ => (),
},
@ -342,13 +335,21 @@ fn parse_external_string(
*escaped = false;
}
},
State::BackTickQuote { from } => {
if ch == b'`' {
spans.push(make_span(*from, index + 1));
state = State::Bare { from: index + 1 };
}
}
}
index += 1;
}
// Add the final span
match state {
State::Bare { from } | State::Quote { from, .. } => {
State::Bare { from }
| State::Quote { from, .. }
| State::BackTickQuote { from, .. } => {
if from < contents.len() {
spans.push(make_span(from, contents.len()));
}
@ -457,7 +458,7 @@ fn parse_regular_external_arg(working_set: &mut StateWorkingSet, span: Span) ->
} else if contents.starts_with(b"[") {
parse_list_expression(working_set, span, &SyntaxShape::Any)
} else {
parse_external_string(working_set, span, false)
parse_external_string(working_set, span)
}
}
@ -479,7 +480,7 @@ pub fn parse_external_call(working_set: &mut StateWorkingSet, spans: &[Span]) ->
let arg = parse_expression(working_set, &[head_span]);
Box::new(arg)
} else {
Box::new(parse_external_string(working_set, head_span, true))
Box::new(parse_external_string(working_set, head_span))
};
let args = spans[1..]

View file

@ -1026,6 +1026,16 @@ pub fn test_external_call_head_interpolated_string(
r#"hello world"#,
"value is surrounded by backtick quote"
)]
#[case(
r#"^foo `"hello world"`"#,
"\"hello world\"",
"value is surrounded by backtick quote, with inner double quote"
)]
#[case(
r#"^foo `'hello world'`"#,
"'hello world'",
"value is surrounded by backtick quote, with inner single quote"
)]
pub fn test_external_call_arg_glob(#[case] input: &str, #[case] expected: &str, #[case] tag: &str) {
test_external_call(input, tag, |name, args| {
match &name.expr {
@ -1120,16 +1130,6 @@ pub fn test_external_call_arg_raw_string(
r#"foo\external call"#,
"double quote with backslash"
)]
#[case(
r#"^foo `"hello world"`"#,
r#"hello world"#,
"value is surrounded by backtick quote, with inner double quote"
)]
#[case(
r#"^foo `'hello world'`"#,
r#"hello world"#,
"value is surrounded by backtick quote, with inner single quote"
)]
pub fn test_external_call_arg_string(
#[case] input: &str,
#[case] expected: &str,

View file

@ -642,3 +642,13 @@ fn exit_code_stops_execution_for_loop() {
assert!(actual.out.is_empty());
assert!(!actual.err.contains("exited with code 42"));
}
#[test]
fn arg_dont_run_subcommand_if_surrounded_with_quote() {
let actual = nu!("nu --testbin cococo `(echo aa)`");
assert_eq!(actual.out, "(echo aa)");
let actual = nu!("nu --testbin cococo \"(echo aa)\"");
assert_eq!(actual.out, "(echo aa)");
let actual = nu!("nu --testbin cococo '(echo aa)'");
assert_eq!(actual.out, "(echo aa)");
}