diff --git a/src/uu/env/locales/en-US.ftl b/src/uu/env/locales/en-US.ftl index d2cdd15d140..c4381b41eb3 100644 --- a/src/uu/env/locales/en-US.ftl +++ b/src/uu/env/locales/en-US.ftl @@ -23,6 +23,8 @@ env-error-backslash-c-not-allowed = '\c' must not appear in double-quoted -S str env-error-invalid-sequence = invalid sequence '\{ $char }' in -S at position { $position } env-error-missing-closing-brace = Missing closing brace at position { $position } env-error-missing-variable = Missing variable name at position { $position } +env-error-only-braced-variable = only ${VARNAME} expansion is supported at position { $position } +env-error-only-braced-variable-at = only {"${VARNAME}"} expansion is supported, error at: { $rest } env-error-missing-closing-brace-after-value = Missing closing brace after default value at position { $position } env-error-unexpected-number = Unexpected character: '{ $char }', expected variable name must not start with 0..9 at position { $position } env-error-expected-brace-or-colon = Unexpected character: '{ $char }', expected a closing brace ('{"}"}') or colon (':') at position { $position } diff --git a/src/uu/env/locales/fr-FR.ftl b/src/uu/env/locales/fr-FR.ftl index 78351e0da4b..6594ea63d5b 100644 --- a/src/uu/env/locales/fr-FR.ftl +++ b/src/uu/env/locales/fr-FR.ftl @@ -23,6 +23,8 @@ env-error-backslash-c-not-allowed = '\\c' ne doit pas apparaître dans une chaî env-error-invalid-sequence = séquence invalide '\\{ $char }' dans -S à la position { $position } env-error-missing-closing-brace = Accolade fermante manquante à la position { $position } env-error-missing-variable = Nom de variable manquant à la position { $position } +env-error-only-braced-variable = seule l'expansion ${VARNAME} est prise en charge à la position { $position } +env-error-only-braced-variable-at = seule l'expansion {"${VARNAME}"} est prise en charge, erreur à : { $rest } env-error-missing-closing-brace-after-value = Accolade fermante manquante après la valeur par défaut à la position { $position } env-error-unexpected-number = Caractère inattendu : '{ $char }', le nom de variable attendu ne doit pas commencer par 0..9 à la position { $position } env-error-expected-brace-or-colon = Caractère inattendu : '{ $char }', accolade fermante ('{"\\}"}') ou deux-points (':') attendu à la position { $position } diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 3343544253e..e8463f163bd 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -63,12 +63,10 @@ pub enum EnvError { EnvParsingOfVariableMissingClosingBrace(usize), #[error("{}", translate!("env-error-missing-variable", "position" => .0))] EnvParsingOfMissingVariable(usize), - #[error("{}", translate!("env-error-missing-closing-brace-after-value", "position" => .0))] - EnvParsingOfVariableMissingClosingBraceAfterValue(usize), + #[error("{}", translate!("env-error-only-braced-variable", "position" => .0))] + EnvParsingOfVariableOnlyBracedName(usize), #[error("{}", translate!("env-error-unexpected-number", "position" => .0, "char" => .1.clone()))] EnvParsingOfVariableUnexpectedNumber(usize, String), - #[error("{}", translate!("env-error-expected-brace-or-colon", "position" => .0, "char" => .1.clone()))] - EnvParsingOfVariableExceptedBraceOrColon(usize, String), #[error("")] EnvReachedEnd, #[error("")] @@ -460,39 +458,40 @@ pub fn uu_app() -> Command { } pub fn parse_args_from_str(text: &NativeIntStr) -> UResult> { - split_iterator::split(text).map_err(|e| match e { - EnvError::EnvBackslashCNotAllowedInDoubleQuotes(_) => USimpleError::new(125, e.to_string()), - EnvError::EnvInvalidBackslashAtEndOfStringInMinusS(_, _) => { - USimpleError::new(125, e.to_string()) - } - EnvError::EnvInvalidSequenceBackslashXInMinusS(_, _) => { - USimpleError::new(125, e.to_string()) + split_iterator::split(text).map_err(|e| { + let var_error = |pos: usize| { + // Find the '$' that started this variable reference and format + // the error like GNU: "only ${VARNAME} expansion is supported, error at: $..." + let dollar_pos = text[..pos] + .iter() + .rposition(|&c| c == b'$') + .unwrap_or(pos); + let rest = String::from_utf8_lossy(&text[dollar_pos..]); + USimpleError::new( + 125, + translate!("env-error-only-braced-variable-at", "rest" => rest), + ) + }; + match e { + EnvError::EnvBackslashCNotAllowedInDoubleQuotes(_) => { + USimpleError::new(125, e.to_string()) + } + EnvError::EnvInvalidBackslashAtEndOfStringInMinusS(_, _) => { + USimpleError::new(125, e.to_string()) + } + EnvError::EnvInvalidSequenceBackslashXInMinusS(_, _) => { + USimpleError::new(125, e.to_string()) + } + EnvError::EnvMissingClosingQuote(_, _) => USimpleError::new(125, e.to_string()), + EnvError::EnvParsingOfVariableMissingClosingBrace(pos) + | EnvError::EnvParsingOfMissingVariable(pos) + | EnvError::EnvParsingOfVariableOnlyBracedName(pos) + | EnvError::EnvParsingOfVariableUnexpectedNumber(pos, _) => var_error(pos), + _ => USimpleError::new( + 125, + translate!("env-error-generic", "error" => format!("{e:?}")), + ), } - EnvError::EnvMissingClosingQuote(_, _) => USimpleError::new(125, e.to_string()), - EnvError::EnvParsingOfVariableMissingClosingBrace(pos) => USimpleError::new( - 125, - translate!("env-error-variable-name-issue", "position" => pos, "error" => e), - ), - EnvError::EnvParsingOfMissingVariable(pos) => USimpleError::new( - 125, - translate!("env-error-variable-name-issue", "position" => pos, "error" => e), - ), - EnvError::EnvParsingOfVariableMissingClosingBraceAfterValue(pos) => USimpleError::new( - 125, - translate!("env-error-variable-name-issue", "position" => pos, "error" => e), - ), - EnvError::EnvParsingOfVariableUnexpectedNumber(pos, _) => USimpleError::new( - 125, - translate!("env-error-variable-name-issue", "position" => pos, "error" => e), - ), - EnvError::EnvParsingOfVariableExceptedBraceOrColon(pos, _) => USimpleError::new( - 125, - translate!("env-error-variable-name-issue", "position" => pos, "error" => e), - ), - _ => USimpleError::new( - 125, - translate!("env-error-generic", "error" => format!("{e:?}")), - ), }) } @@ -509,13 +508,29 @@ fn check_and_handle_string_args( prefix_to_test: &str, all_args: &mut Vec, do_debug_print_args: Option<&Vec>, + require_non_empty_payload: bool, + strip_optional_leading_equals: bool, ) -> UResult { let native_arg = NCvt::convert(arg); if let Some(remaining_arg) = native_arg.strip_prefix(&*NCvt::convert(prefix_to_test)) { + if require_non_empty_payload && remaining_arg.is_empty() { + return Ok(false); + } + if let Some(input_args) = do_debug_print_args { debug_print_args(input_args); // do it here, such that its also printed when we get an error/panic during parsing } + let remaining_arg = if strip_optional_leading_equals { + if let Some(stripped_remaining_arg) = remaining_arg.strip_prefix(&*NCvt::convert("=")) { + stripped_remaining_arg + } else { + remaining_arg + } + } else { + remaining_arg + }; + let arg_strings = parse_args_from_str(remaining_arg)?; all_args.extend( arg_strings @@ -570,7 +585,12 @@ impl EnvAppData { options::UNSET, ]; let short_flags_with_args = ['a', 'C', 'f', 'u']; + let mut consumed_split_payload_arg: Option = None; for (n, arg) in original_args.iter().enumerate() { + if consumed_split_payload_arg == Some(n) { + consumed_split_payload_arg = None; + continue; + } let arg_str = arg.to_string_lossy(); // Stop processing env flags once we reach the command or -- argument if 0 < n @@ -585,13 +605,21 @@ impl EnvAppData { } expecting_arg = false; match arg { - b if check_and_handle_string_args(b, "--split-string", &mut all_args, None)? => { + b if check_and_handle_string_args( + b, + "--split-string", + &mut all_args, + None, + true, + true, + )? => + { self.had_string_argument = true; } - b if check_and_handle_string_args(b, "-S", &mut all_args, None)? => { + b if check_and_handle_string_args(b, "-S", &mut all_args, None, true, false)? => { self.had_string_argument = true; } - b if check_and_handle_string_args(b, "-vS", &mut all_args, None)? => { + b if check_and_handle_string_args(b, "-vS", &mut all_args, None, true, false)? => { self.do_debug_printing = true; self.had_string_argument = true; } @@ -600,12 +628,39 @@ impl EnvAppData { "-vvS", &mut all_args, Some(original_args), + true, + false, )? => { self.do_debug_printing = true; self.do_input_debug_printing = Some(false); // already done self.had_string_argument = true; } + b if b == "--split-string" || b == "-S" || b == "-vS" || b == "-vvS" => { + let Some(next_arg) = original_args.get(n + 1) else { + all_args.push(arg.clone()); + continue; + }; + + if b == "-vS" || b == "-vvS" { + self.do_debug_printing = true; + } + if b == "-vvS" { + debug_print_args(original_args); + self.do_input_debug_printing = Some(false); + } + + let native_next_arg = NCvt::convert(next_arg); + let arg_strings = parse_args_from_str(native_next_arg.as_ref())?; + all_args.extend( + arg_strings + .into_iter() + .map(from_native_int_representation_owned), + ); + self.had_string_argument = true; + expecting_arg = false; + consumed_split_payload_arg = Some(n + 1); + } _ => { if let Some(flag) = arg_str.strip_prefix("--") { if flags_with_args.contains(&flag) { @@ -710,8 +765,6 @@ impl EnvAppData { &signal_apply_all, )?; - apply_change_directory(&opts)?; - // NOTE: we manually set and unset the env vars below rather than using Command::env() to more // easily handle the case where no command is given @@ -750,6 +803,7 @@ impl EnvAppData { } } + apply_change_directory(&opts)?; if opts.program.is_empty() { // no program provided, so just dump all env vars to stdout print_all_env_vars(opts.line_ending)?; @@ -1242,21 +1296,34 @@ mod tests { .contains("variable name issue (at 10): Missing closing brace") ); - let result = parse_args_from_str(&NCvt::convert(r"echo ${FOO:-value")); + let result = parse_args_from_str(&NCvt::convert(r"echo ${FOO:-value}")); assert!(result.is_err()); assert!( result .unwrap_err() .to_string() - .contains("variable name issue (at 17): Missing closing brace after default value") + .contains("only ${VARNAME} expansion is supported") ); + let result = parse_args_from_str(&NCvt::convert(r"echo $FOO")); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("only ${VARNAME} expansion is supported") + ); let result = parse_args_from_str(&NCvt::convert(r"echo ${1FOO}")); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("variable name issue (at 7): Unexpected character: '1', expected variable name must not start with 0..9")); let result = parse_args_from_str(&NCvt::convert(r"echo ${FOO?}")); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("variable name issue (at 10): Unexpected character: '?', expected a closing brace ('}') or colon (':')")); + assert!( + result + .unwrap_err() + .to_string() + .contains("only ${VARNAME} expansion is supported") + ); } } diff --git a/src/uu/env/src/split_iterator.rs b/src/uu/env/src/split_iterator.rs index a511fb9e037..692f4601630 100644 --- a/src/uu/env/src/split_iterator.rs +++ b/src/uu/env/src/split_iterator.rs @@ -99,18 +99,11 @@ impl<'a> SplitIterator<'a> { parser: self.get_parser_mut(), }; - let (name, default) = var_parse.parse_variable()?; + let name = var_parse.parse_variable()?; let varname_os_str_cow = from_native_int_representation(Cow::Borrowed(name)); - let value = std::env::var_os(varname_os_str_cow); - match (&value, default) { - (None, None) => {} // do nothing, just replace it with "" - (Some(value), _) => { - self.expander.put_string(value); - } - (None, Some(default)) => { - self.expander.put_native_string(default); - } + if let Some(value) = std::env::var_os(varname_os_str_cow) { + self.expander.put_string(value); } Ok(()) @@ -286,15 +279,10 @@ impl<'a> SplitIterator<'a> { self.take_one()?; Ok(()) } - Some(c) if REPLACEMENTS.iter().any(|&x| x.0 == c) => { - // See GNU test-suite e11: In single quotes, \t remains as it is. - // Comparing with GNU behavior: \a is not accepted and issues an error. - // So apparently only known sequences are allowed, even though they are not expanded.... bug of GNU? + Some(_) => { self.push_char_to_word(BACKSLASH); - self.take_one()?; Ok(()) } - Some(c) => Err(self.make_invalid_sequence_backslash_xin_minus_s(c)), } } diff --git a/src/uu/env/src/variable_parser.rs b/src/uu/env/src/variable_parser.rs index da200c9dba6..032a801604c 100644 --- a/src/uu/env/src/variable_parser.rs +++ b/src/uu/env/src/variable_parser.rs @@ -34,126 +34,60 @@ impl<'a> VariableParser<'a, '_> { Ok(()) } - fn parse_braced_variable_name( - &mut self, - ) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), EnvError> { + fn parse_braced_variable_name(&mut self) -> Result<&'a NativeIntStr, EnvError> { let pos_start = self.parser.get_peek_position(); self.check_variable_name_start()?; - let (varname_end, default_end); - loop { + let varname_end = loop { match self.get_current_char() { None => { return Err(EnvError::EnvParsingOfVariableMissingClosingBrace( self.parser.get_peek_position(), )); } - Some(c) if !c.is_ascii() || c.is_ascii_alphanumeric() || c == '_' => { + Some(c) if c.is_ascii_alphanumeric() || c == '_' => { self.skip_one()?; } - Some(':') => { - varname_end = self.parser.get_peek_position(); - loop { - match self.get_current_char() { - None => { - return Err( - EnvError::EnvParsingOfVariableMissingClosingBraceAfterValue( - self.parser.get_peek_position(), - ), - ); - } - Some('}') => { - default_end = Some(self.parser.get_peek_position()); - self.skip_one()?; - break; - } - Some(_) => { - self.skip_one()?; - } - } - } - break; - } Some('}') => { - varname_end = self.parser.get_peek_position(); - default_end = None; + let varname_end = self.parser.get_peek_position(); self.skip_one()?; - break; + break varname_end; } - Some(c) => { - return Err(EnvError::EnvParsingOfVariableExceptedBraceOrColon( + Some(_) => { + return Err(EnvError::EnvParsingOfVariableOnlyBracedName( self.parser.get_peek_position(), - c.to_string(), )); } } - } - - let default_opt = if let Some(default_end) = default_end { - Some(self.parser.substring(&Range { - start: varname_end + 1, - end: default_end, - })) - } else { - None }; - let varname = self.parser.substring(&Range { - start: pos_start, - end: varname_end, - }); - - Ok((varname, default_opt)) - } - - fn parse_unbraced_variable_name(&mut self) -> Result<&'a NativeIntStr, EnvError> { - let pos_start = self.parser.get_peek_position(); - - self.check_variable_name_start()?; - - loop { - match self.get_current_char() { - None => break, - Some(c) if c.is_ascii_alphanumeric() || c == '_' => { - self.skip_one()?; - } - Some(_) => break, - } - } - - let pos_end = self.parser.get_peek_position(); - - if pos_end == pos_start { + if varname_end == pos_start { return Err(EnvError::EnvParsingOfMissingVariable(pos_start)); } let varname = self.parser.substring(&Range { start: pos_start, - end: pos_end, + end: varname_end, }); Ok(varname) } - pub fn parse_variable( - &mut self, - ) -> Result<(&'a NativeIntStr, Option<&'a NativeIntStr>), EnvError> { + pub fn parse_variable(&mut self) -> Result<&'a NativeIntStr, EnvError> { self.skip_one()?; - let (name, default) = match self.get_current_char() { - None => { - return Err(EnvError::EnvParsingOfMissingVariable( - self.parser.get_peek_position(), - )); - } + match self.get_current_char() { + None => Err(EnvError::EnvParsingOfMissingVariable( + self.parser.get_peek_position(), + )), Some('{') => { self.skip_one()?; - self.parse_braced_variable_name()? + self.parse_braced_variable_name() } - Some(_) => (self.parse_unbraced_variable_name()?, None), - }; - - Ok((name, default)) + Some(_) => Err(EnvError::EnvParsingOfVariableOnlyBracedName( + self.parser.get_peek_position(), + )), + } } } diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 4a989bd053b..716f87c2bd9 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -480,6 +480,35 @@ fn test_fail_change_directory() { assert!(out.contains("env: cannot change directory to ")); } +#[test] +fn test_chdir_happens_after_relative_file_loading() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.mkdir("target"); + scene + .fixtures + .write("config.env", "CONFIG_SOURCE=from-root\n"); + scene + .fixtures + .write("target/config.env", "CONFIG_SOURCE=from-target\n"); + + let out = scene + .ucmd() + .args(&["--chdir", "target", "--file", "config.env", "-i"]) + .arg(uutests::util::get_tests_binary()) + .arg(util_name!()) + .succeeds() + .stdout_move_str(); + + assert!( + out.contains("CONFIG_SOURCE=from-root\n"), + "expected config file from invocation directory, got: {out:?}" + ); + assert!( + !out.contains("CONFIG_SOURCE=from-target\n"), + "unexpectedly loaded config from --chdir target directory: {out:?}" + ); +} + #[cfg(not(target_os = "windows"))] // windows has no executable "echo", its only supported as part of a batch-file #[test] fn test_split_string_into_args_one_argument_no_quotes() { @@ -556,6 +585,30 @@ fn test_split_string_into_args_long_option_whitespace_handling() { assert_eq!(out, "xAx\nxBx\n"); } +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_option_forms_match_gnu_required_argument_handling() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .args(&["-S", "printf x:%s\\n one two"]) + .succeeds() + .stdout_is("x:one\nx:two\n"); + + scene + .ucmd() + .args(&["--split-string", "printf x:%s\\n one two"]) + .succeeds() + .stdout_is("x:one\nx:two\n"); + + scene + .ucmd() + .arg("--split-string=printf x:%s\\n one two") + .succeeds() + .stdout_is("x:one\nx:two\n"); +} + #[cfg(not(target_os = "windows"))] // no printf available #[test] fn test_split_string_into_args_debug_output_whitespace_handling() { @@ -603,6 +656,67 @@ fn test_gnu_e20() { assert_eq!(out.stdout_str(), output); } +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_single_quotes_keep_unknown_backslash_sequences_literal() { + let scene = TestScenario::new(util_name!()); + scene + .ucmd() + .arg("-Sprintf %s '\\x'") + .succeeds() + .stdout_is("\\x"); + scene + .ucmd() + .arg("-Sprintf %s '\\a'") + .succeeds() + .stdout_is("\\a"); + scene + .ucmd() + .arg("-Sprintf %s '\\`'") + .succeeds() + .stdout_is("\\`"); + scene + .ucmd() + .arg("-Sprintf %s '\\q'") + .succeeds() + .stdout_is("\\q"); + scene + .ucmd() + .arg("-Sprintf %s '\\|'") + .succeeds() + .stdout_is("\\|"); + scene + .ucmd() + .arg("-Sprintf %s '\\9'") + .succeeds() + .stdout_is("\\9"); +} + +#[cfg(not(target_os = "windows"))] // no printf available +#[test] +fn test_split_string_backslash_a_behavior_matches_gnu_quoting_context() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("-Sprintf %s '\\a'") + .succeeds() + .stdout_is("\\a"); + + scene + .ucmd() + .arg("-Sprintf %s \"\\a\"") + .fails_with_code(125) + .no_stdout() + .stderr_contains("invalid sequence '\\a' in -S"); + + scene + .ucmd() + .arg("-Sprintf %s \\a") + .fails_with_code(125) + .no_stdout() + .stderr_contains("invalid sequence '\\a' in -S"); +} #[test] #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign fn test_env_parsing_errors() { @@ -632,12 +746,6 @@ fn test_env_parsing_errors() { .no_stdout() .stderr_is("env: invalid sequence '\\a' in -S at position 2\n"); - ts.ucmd() - .arg("-S'\\a'") // single quotes, invalid escape sequence a - .fails_with_code(125) - .no_stdout() - .stderr_is("env: invalid sequence '\\a' in -S at position 2\n"); - ts.ucmd() .arg(r"-S\|\&\;") // no quotes, invalid escape sequence | .fails_with_code(125) @@ -668,12 +776,6 @@ fn test_env_parsing_errors() { .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); - ts.ucmd() - .arg(r"-S'\`\&\;'") // single quotes, invalid escape sequence ` - .fails_with_code(125) - .no_stdout() - .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); - ts.ucmd() .arg(r"-S\`") // ` escaped without quotes .fails_with_code(125) @@ -686,12 +788,6 @@ fn test_env_parsing_errors() { .no_stdout() .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); - ts.ucmd() - .arg(r"-S'\`'") // ` escaped in single quotes - .fails_with_code(125) - .no_stdout() - .stderr_is("env: invalid sequence '\\`' in -S at position 2\n"); - ts.ucmd() .args(&[r"-S\🦉"]) // ` escaped in single quotes .fails_with_code(125) @@ -1279,7 +1375,7 @@ mod tests_split_iterator { assert_eq!(split("'\\"), Err(EnvError::EnvMissingClosingQuote(2, '\''))); assert_eq!( split(r#""$""#), - Err(EnvError::EnvParsingOfMissingVariable(2)), + Err(EnvError::EnvParsingOfVariableOnlyBracedName(2)), ); } @@ -1293,10 +1389,8 @@ mod tests_split_iterator { split("\"\\a\""), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) ); - assert_eq!( - split("'\\a'"), - Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) - ); + assert_eq!(split("'\\a'"), Ok(vec![OsString::from("\\a")])); + assert_eq!(split("'\\`'"), Ok(vec![OsString::from("\\`")])); assert_eq!( split(r#""\a""#), Err(EnvError::EnvInvalidSequenceBackslashXInMinusS(2, 'a')) @@ -1860,21 +1954,19 @@ fn test_shebang_error() { #[test] #[cfg(not(target_os = "windows"))] -fn test_braced_variable_with_default_value() { - new_ucmd!() - .arg("-Secho ${UNSET_VAR_UNLIKELY_12345:fallback}") - .succeeds() - .stdout_is("fallback\n"); -} - -#[test] -#[cfg(not(target_os = "windows"))] -fn test_braced_variable_with_default_when_set() { - new_ucmd!() - .env("TEST_VAR_12345", "actual") - .arg("-Secho ${TEST_VAR_12345:fallback}") - .succeeds() - .stdout_is("actual\n"); +fn test_reject_shell_style_variable_expansions() { + for split in [ + "-Secho $TEST_VAR_12345", + "-Secho ${TEST_VAR_12345:fallback}", + "-Secho ${TEST_VAR_12345:-fallback}", + "-Secho ${TEST_VAR_12345-default}", + ] { + new_ucmd!() + .env("TEST_VAR_12345", "value") + .arg(split) + .fails_with_code(125) + .stderr_contains("only ${VARNAME} expansion is supported"); + } } #[test] @@ -1892,15 +1984,15 @@ fn test_braced_variable_error_missing_closing_brace() { new_ucmd!() .arg("-Secho ${FOO") .fails_with_code(125) - .stderr_contains("Missing closing brace"); + .stderr_contains("only ${VARNAME} expansion is supported, error at: ${FOO"); } #[test] -fn test_braced_variable_error_missing_closing_brace_after_default() { +fn test_braced_variable_error_rejects_default_syntax() { new_ucmd!() - .arg("-Secho ${FOO:-value") + .arg("-Secho ${FOO:-value}") .fails_with_code(125) - .stderr_contains("Missing closing brace after default value"); + .stderr_contains("only ${VARNAME} expansion is supported"); } #[test] @@ -1908,7 +2000,7 @@ fn test_braced_variable_error_starts_with_digit() { new_ucmd!() .arg("-Secho ${1FOO}") .fails_with_code(125) - .stderr_contains("Unexpected character: '1'"); + .stderr_contains("only ${VARNAME} expansion is supported, error at: ${1FOO}"); } #[test] @@ -1916,7 +2008,7 @@ fn test_braced_variable_error_unexpected_character() { new_ucmd!() .arg("-Secho ${FOO?}") .fails_with_code(125) - .stderr_contains("Unexpected character: '?'"); + .stderr_contains("only ${VARNAME} expansion is supported"); } #[test] diff --git a/util/gnu-patches/tests_env_env-S.pl.patch b/util/gnu-patches/tests_env_env-S.pl.patch index 45328352d6e..a7ad95c6672 100644 --- a/util/gnu-patches/tests_env_env-S.pl.patch +++ b/util/gnu-patches/tests_env_env-S.pl.patch @@ -2,7 +2,7 @@ Index: gnu/tests/env/env-S.pl =================================================================== --- gnu.orig/tests/env/env-S.pl +++ gnu/tests/env/env-S.pl -@@ -200,36 +200,37 @@ my @Tests = +@@ -200,10 +200,10 @@ my @Tests = # Test Error Conditions ['err1', q[-S'"\\c"'], {EXIT=>125}, @@ -19,23 +19,9 @@ Index: gnu/tests/env/env-S.pl + {ERR=>"$prog: no terminating quote in -S string at position 6 for quote '''\n"}], ['err5', q[-S'A=B\\q'], {EXIT=>125}, - {ERR=>"$prog: invalid sequence '\\q' in -S\n"}], -- ['err6', q[-S'A=$B'], {EXIT=>125}, -- {ERR=>"$prog: only \${VARNAME} expansion is supported, error at: \$B\n"}], + {ERR=>"$prog: invalid sequence '\\q' in -S at position 4\n"}], -+ ['err6', q[-S'A=$B echo hello'], {EXIT=>0}, -+ {OUT=>"hello"}], - ['err7', q[-S'A=${B'], {EXIT=>125}, -- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . -- "error at: \${B\n"}], -+ {ERR=>"$prog" . qq[: variable name issue (at 5): Missing closing brace at position 5\n]}], - ['err8', q[-S'A=${B%B}'], {EXIT=>125}, -- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . -- "error at: \${B%B}\n"}], -+ {ERR=>"$prog" . qq[: variable name issue (at 5): Unexpected character: '%', expected a closing brace ('}') or colon (':') at position 5\n]}], - ['err9', q[-S'A=${9B}'], {EXIT=>125}, -- {ERR=>"$prog: only \${VARNAME} expansion is supported, " . -- "error at: \${9B}\n"}], -+ {ERR=>"$prog" . qq[: variable name issue (at 4): Unexpected character: '9', expected variable name must not start with 0..9 at position 4\n]}], + ['err6', q[-S'A=$B'], {EXIT=>125}, +@@ -218,12 +218,18 @@ my @Tests = # Test incorrect shebang usage (extraneous whitespace). ['err_sp2', q['-v -S cat -n'], {EXIT=>125},