fix parsing of bare word string interpolations that start with a sub expression (#15735)

- fixes #15731

# Description
Existing bare word string interpolation only works if the string doesn't
start with a subxpression.
```nushell
echo fork(2)
# => fork2

echo (2)fork
# => Error: nu::parser::unclosed_delimiter
# => 
# =>   × Unclosed delimiter.
# =>    ╭─[entry #25:1:13]
# =>  1 │ echo (2)fork
# =>    ╰────
```
This PR lifts that restriction.
```nushell
echo fork(2)
# => fork2

echo (2)fork
# => 2fork
```

This was first brought to my attention on discord with the following
command failing to parse.
```nushell
docker run -u (id -u):(id -g)
```
It now works.

# User-Facing Changes

# Tests + Formatting
No existing test broke or required tweaking. Additional tests covering
this case was added.
- 🟢 toolkit fmt
- 🟢 toolkit clippy
- 🟢 toolkit test
- 🟢 toolkit test stdlib

# After Submitting

---------

Co-authored-by: Bahex <17417311+Bahex@users.noreply.github.com>
This commit is contained in:
Bahex
2025-05-13 22:25:07 +03:00
committed by GitHub
parent e0eb29f161
commit 36c30ade3a
2 changed files with 99 additions and 7 deletions

View File

@ -1576,6 +1576,78 @@ mod string {
assert!(matches!(subexprs[3], &Expr::FullCellPath(..)));
}
#[test]
pub fn parse_string_interpolation_bare_starting_subexpr() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(
&mut working_set,
None,
b"\"\" ++ (1 + 3)foo(7 - 5)bar",
true,
);
assert!(working_set.parse_errors.is_empty());
assert_eq!(block.len(), 1);
let pipeline = &block.pipelines[0];
assert_eq!(pipeline.len(), 1);
let element = &pipeline.elements[0];
assert!(element.redirection.is_none());
let subexprs: Vec<&Expr> = match &element.expr.expr {
Expr::BinaryOp(_, _, rhs) => match &rhs.expr {
Expr::StringInterpolation(expressions) => {
expressions.iter().map(|e| &e.expr).collect()
}
_ => panic!("Expected an `Expr::StringInterpolation`"),
},
_ => panic!("Expected an `Expr::BinaryOp`"),
};
assert_eq!(subexprs.len(), 4);
assert!(matches!(subexprs[0], &Expr::FullCellPath(..)));
assert_eq!(subexprs[1], &Expr::String("foo".to_string()));
assert!(matches!(subexprs[2], &Expr::FullCellPath(..)));
assert_eq!(subexprs[3], &Expr::String("bar".to_string()));
}
#[test]
pub fn parse_string_interpolation_bare_starting_subexpr_external_arg() {
let engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let block = parse(&mut working_set, None, b"^echo ($nu.home-path)/path", true);
assert!(working_set.parse_errors.is_empty());
assert_eq!(block.len(), 1);
let pipeline = &block.pipelines[0];
assert_eq!(pipeline.len(), 1);
let element = &pipeline.elements[0];
assert!(element.redirection.is_none());
let subexprs: Vec<&Expr> = match &element.expr.expr {
Expr::ExternalCall(_, args) => match &args[0] {
ExternalArgument::Regular(expression) => match &expression.expr {
Expr::StringInterpolation(expressions) => {
expressions.iter().map(|e| &e.expr).collect()
}
_ => panic!("Expected an `ExternalArgument::Regular`"),
},
_ => panic!("Expected an `Expr::StringInterpolation`"),
},
_ => panic!("Expected an `Expr::BinaryOp`"),
};
assert_eq!(subexprs.len(), 2);
assert!(matches!(subexprs[0], &Expr::FullCellPath(..)));
assert_eq!(subexprs[1], &Expr::String("/path".to_string()));
}
#[test]
pub fn parse_nested_expressions() {
let engine_state = EngineState::new();