From 3877bfb61a7b575cfea185cd78afd049904b3b01 Mon Sep 17 00:00:00 2001 From: zc he Date: Wed, 20 Aug 2025 23:57:33 +0800 Subject: [PATCH] fix(parser): env shorthand false positive (#16337) More restrictions on env shorthand parsing to avoid false positives. Fixes #16332 --- crates/nu-parser/src/parser.rs | 81 ++++++++++++++++++---------------- tests/repl/test_parser.rs | 6 +++ 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 686deb5bf0..67f9c0de3f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -116,6 +116,22 @@ pub fn is_math_expression_like(working_set: &mut StateWorkingSet, span: Span) -> is_range } +fn is_env_variable_name(bytes: &[u8]) -> bool { + if bytes.is_empty() { + return false; + } + + let first = bytes[0]; + if !first.is_ascii_alphabetic() && first != b'_' { + return false; + } + + bytes + .iter() + .skip(1) + .all(|&b| b.is_ascii_alphanumeric() || b == b'_') +} + fn is_identifier(bytes: &[u8]) -> bool { bytes.iter().all(|x| is_identifier_byte(*x)) } @@ -5892,49 +5908,38 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex // Check if there is any environment shorthand let name = working_set.get_span_contents(spans[pos]); - let split = name.splitn(2, |x| *x == b'='); - let split: Vec<_> = split.collect(); - if !name.starts_with(b"^") - && split.len() == 2 - && !split[0].is_empty() - && !split[0].ends_with(b"..") - // was range op ..= - { - let point = split[0].len() + 1; + let split: Vec<_> = name.splitn(2, |x| *x == b'=').collect(); + if split.len() != 2 || !is_env_variable_name(split[0]) { + break; + } - let starting_error_count = working_set.parse_errors.len(); + let point = split[0].len() + 1; + let starting_error_count = working_set.parse_errors.len(); - let lhs_span = Span::new(spans[pos].start, spans[pos].start + point - 1); - if !is_identifier(working_set.get_span_contents(lhs_span)) { - break; - } - - let lhs = parse_string_strict(working_set, lhs_span); - let rhs = if spans[pos].start + point < spans[pos].end { - let rhs_span = Span::new(spans[pos].start + point, spans[pos].end); - - if working_set.get_span_contents(rhs_span).starts_with(b"$") { - parse_dollar_expr(working_set, rhs_span) - } else { - parse_string_strict(working_set, rhs_span) - } + let rhs = if spans[pos].start + point < spans[pos].end { + let rhs_span = Span::new(spans[pos].start + point, spans[pos].end); + if split[1].starts_with(b"$") { + parse_dollar_expr(working_set, rhs_span) } else { - Expression::new( - working_set, - Expr::String(String::new()), - Span::unknown(), - Type::Nothing, - ) - }; - - if starting_error_count == working_set.parse_errors.len() { - shorthand.push((lhs, rhs)); - pos += 1; - } else { - working_set.parse_errors.truncate(starting_error_count); - break; + parse_string_strict(working_set, rhs_span) } } else { + Expression::new( + working_set, + Expr::String(String::new()), + Span::unknown(), + Type::Nothing, + ) + }; + + let lhs_span = Span::new(spans[pos].start, spans[pos].start + point - 1); + let lhs = parse_string_strict(working_set, lhs_span); + + if starting_error_count == working_set.parse_errors.len() { + shorthand.push((lhs, rhs)); + pos += 1; + } else { + working_set.parse_errors.truncate(starting_error_count); break; } } diff --git a/tests/repl/test_parser.rs b/tests/repl/test_parser.rs index f532a74a4e..2373c357c0 100644 --- a/tests/repl/test_parser.rs +++ b/tests/repl/test_parser.rs @@ -1036,3 +1036,9 @@ fn external_argument_with_subexpressions() -> TestResult { run_test(r#"^echo ")('foo')(""#, ")('foo')(")?; fail_test(r#"^echo foo( 'bar'"#, "Unexpected end of code") } + +// https://github.com/nushell/nushell/issues/16332 +#[test] +fn quote_escape_but_not_env_shorthand() -> TestResult { + run_test(r#""\"=foo""#, "\"=foo") +}