diff --git a/src/uu/env/src/env.rs b/src/uu/env/src/env.rs index 61efcbcaf99..79c51d5d0c3 100644 --- a/src/uu/env/src/env.rs +++ b/src/uu/env/src/env.rs @@ -550,6 +550,48 @@ fn check_and_handle_string_args( } } +/// Keep a leading `=` in a value attached to a short option. Clap consumes it +/// as an option/value separator, whereas env treats it as part of the value. +fn preserve_attached_equals(args: &mut [OsString]) -> UResult<()> { + let mut skip_value = false; + for arg in args.iter_mut().skip(1) { + if std::mem::take(&mut skip_value) { + continue; + } + let bytes = arg.as_encoded_bytes(); + if bytes == b"--" { + break; + } + if let Some(long) = bytes.strip_prefix(b"--") { + skip_value = [options::CHDIR, options::UNSET, options::ARGV0] + .iter() + .any(|name| name.as_bytes().starts_with(long)); + continue; + } + // The first operand ends the options. + let Some(short) = bytes.strip_prefix(b"-").filter(|s| !s.is_empty()) else { + break; + }; + for (index, option) in short.iter().enumerate() { + match option { + b'a' | b'C' | b'u' => { + if short.get(index + 1) == Some(&b'=') { + let mut value = bytes.to_vec(); + value.insert(index + 2, b'='); + *arg = uucore::os_str_from_bytes(&value)?.to_os_string(); + } else { + skip_value = index + 1 == short.len(); + } + break; + } + b'i' | b'0' | b'v' => {} + _ => break, + } + } + } + Ok(()) +} + #[derive(Default)] struct EnvAppData { do_debug_printing: bool, @@ -725,7 +767,8 @@ impl EnvAppData { original_args: impl uucore::Args, ) -> Result> { let original_args: Vec = original_args.collect(); - let args = self.process_all_string_arguments(&original_args)?; + let mut args = self.process_all_string_arguments(&original_args)?; + preserve_attached_equals(&mut args)?; #[cfg(unix)] let mut signal_apply_all = BTreeSet::new(); diff --git a/tests/by-util/test_env.rs b/tests/by-util/test_env.rs index 7da28738f47..c8fbe08133c 100644 --- a/tests/by-util/test_env.rs +++ b/tests/by-util/test_env.rs @@ -429,6 +429,26 @@ fn test_change_directory() { assert_eq!(out.trim(), temporary_path.as_os_str()); } +#[cfg(not(windows))] +#[test] +fn test_short_option_attached_value_keeps_leading_equals() { + let scene = TestScenario::new(util_name!()); + scene.fixtures.mkdir("=d"); + let expected = std::fs::canonicalize(scene.fixtures.plus("=d")).unwrap(); + let out = scene + .ucmd() + .args(&["-C=d", "pwd"]) + .succeeds() + .stdout_move_str(); + assert_eq!(out.trim(), expected.as_os_str()); + + scene + .ucmd() + .args(&["-a=name", "sh", "-c", "echo $0"]) + .succeeds() + .stdout_is("=name\n"); +} + #[cfg(windows)] #[test] fn test_change_directory() { @@ -1151,6 +1171,10 @@ fn disallow_equals_sign_on_short_unset_option() { .arg("-u=A1B2C3") .fails_with_code(125) .stderr_contains("env: cannot unset '=A1B2C3': Invalid argument"); + ts.ucmd() + .arg("-vu=A1B2C3") + .fails_with_code(125) + .stderr_contains("env: cannot unset '=A1B2C3': Invalid argument"); ts.ucmd().arg("--split-string=A1B=2C3=").succeeds(); ts.ucmd() .arg("--unset=")