diff --git a/src/uu/env/src/split_iterator.rs b/src/uu/env/src/split_iterator.rs index ec2b1d9d283..d44226f1765 100644 --- a/src/uu/env/src/split_iterator.rs +++ b/src/uu/env/src/split_iterator.rs @@ -95,7 +95,7 @@ impl<'a> SplitIterator<'a> { self.expander.get_parser_mut() } - fn substitute_variable<'x>(&'x mut self) -> Result<(), EnvError> { + fn substitute_variable<'x>(&'x mut self) -> Result { let mut var_parse = VariableParser::<'a, '_> { parser: self.get_parser_mut(), }; @@ -105,9 +105,10 @@ impl<'a> SplitIterator<'a> { let varname_os_str_cow = from_native_int_representation(Cow::Borrowed(name)); if let Some(value) = std::env::var_os(varname_os_str_cow) { self.expander.put_string(value); + return Ok(true); } - Ok(()) + Ok(false) } fn check_and_replace_ascii_escape_code(&mut self, c: char) -> Result { @@ -152,6 +153,12 @@ impl<'a> SplitIterator<'a> { Some(c) if ASCII_WHITESPACE_CHARS.contains(&c) => { self.skip_one()?; } + Some(DOLLAR) => { + // An unset variable must not start a word. + if self.substitute_variable()? { + self.state_unquoted()?; + } + } Some(_) => { // Don't consume char. Will be done in unquoted state. self.state_unquoted()?; diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 7da28738f47..3599e9a6012 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -692,6 +692,32 @@ fn test_split_string_backslash_a_behavior_matches_gnu_quoting_context() { .no_stdout() .stderr_contains("invalid sequence '\\a' in -S"); } + +#[cfg(not(windows))] // no printf available +#[test] +fn test_split_string_unset_variable_does_not_start_a_word() { + let scene = TestScenario::new(util_name!()); + + scene + .ucmd() + .arg("-Sprintf [%s] a ${UU_UNSET}") + .succeeds() + .stdout_is("[a]"); + + scene + .ucmd() + .arg("-S${UU_UNSET} printf hi") + .succeeds() + .stdout_is("hi"); + + scene + .ucmd() + .env("UU_EMPTY", "") + .arg("-Sprintf [%s] a ${UU_EMPTY}") + .succeeds() + .stdout_is("[a][]"); +} + #[test] #[allow(clippy::cognitive_complexity)] // Ignore clippy lint of too long function sign fn test_env_parsing_errors() { @@ -1464,6 +1490,33 @@ mod tests_split_iterator { ]); } + #[test] + fn split_unset_variable_on_its_own_adds_no_word() { + assert!(std::env::var_os("UU_UNSET").is_none()); + split_ok(&[ + ("${UU_UNSET}", &[]), + ("a ${UU_UNSET}", &["a"]), + ("a ${UU_UNSET} b", &["a", "b"]), + ("${UU_UNSET} a", &["a"]), + ("a ${UU_UNSET}#c d", &["a"]), + (r"a ${UU_UNSET}\_b", &["a", "b"]), + (r"a ${UU_UNSET}\c", &["a"]), + ]); + } + + #[test] + fn split_unset_variable_next_to_other_text_keeps_the_word() { + assert!(std::env::var_os("UU_UNSET").is_none()); + split_ok(&[ + ("x${UU_UNSET}", &["x"]), + ("${UU_UNSET}x", &["x"]), + ("a ${UU_UNSET}${UU_UNSET}x", &["a", "x"]), + (r#""${UU_UNSET}""#, &[""]), + (r#"""${UU_UNSET}"#, &[""]), + (r#"${UU_UNSET}"""#, &[""]), + ]); + } + #[test] fn test_quote() { assert_eq!(quote(""), "''");