From 4b36064303d26b0c60698024ddfa5512cfedee3e Mon Sep 17 00:00:00 2001 From: yuankunzhang Date: Sun, 31 Aug 2025 21:31:51 +0800 Subject: [PATCH] who: honor locale settings --- src/uu/who/src/platform/unix.rs | 17 +++++++++++---- tests/by-util/test_who.rs | 27 +++++++++++++++++++++++- tests/uutests/src/lib/util.rs | 37 ++++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs index d06f199b132..5266ce0ed47 100644 --- a/src/uu/who/src/platform/unix.rs +++ b/src/uu/who/src/platform/unix.rs @@ -163,11 +163,20 @@ fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> { } fn time_string(ut: &UtmpxRecord) -> String { - // "%b %e %H:%M" - let time_format: Vec = + let lc_time = std::env::var("LC_ALL") + .or_else(|_| std::env::var("LC_TIME")) + .or_else(|_| std::env::var("LANG")) + .unwrap_or_default(); + + let time_format: Vec = if lc_time == "C" { + // "%b %e %H:%M" time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") - .unwrap(); - ut.login_time().format(&time_format).unwrap() // LC_ALL=C + .unwrap() + } else { + // "%Y-%m-%d %H:%M" + time::format_description::parse("[year]-[month]-[day] [hour]:[minute]").unwrap() + }; + ut.login_time().format(&time_format).unwrap() } #[inline] diff --git a/tests/by-util/test_who.rs b/tests/by-util/test_who.rs index 74475895cb0..d94a7b99a8f 100644 --- a/tests/by-util/test_who.rs +++ b/tests/by-util/test_who.rs @@ -7,7 +7,7 @@ use uutests::new_ucmd; use uutests::unwrap_or_return; -use uutests::util::{TestScenario, expected_result}; +use uutests::util::{TestScenario, expected_result, gnu_cmd_result}; use uutests::util_name; #[test] fn test_invalid_arg() { @@ -250,3 +250,28 @@ fn test_all() { ts.ucmd().arg(opt).succeeds().stdout_is(expected_stdout); } } + +#[cfg(unix)] +#[test] +#[ignore = "issue #3219"] +fn test_locale() { + let ts = TestScenario::new(util_name!()); + + let expected_stdout = + unwrap_or_return!(gnu_cmd_result(&ts, &[], &[("LC_ALL", "C")])).stdout_move_str(); + ts.ucmd() + .env("LC_ALL", "C") + .succeeds() + .stdout_is(&expected_stdout); + + let expected_stdout = + unwrap_or_return!(gnu_cmd_result(&ts, &[], &[("LC_ALL", "en_US.UTF-8")])).stdout_move_str(); + ts.ucmd() + .env("LC_ALL", "C") + .succeeds() + .stdout_str_check(|s| s != expected_stdout); + ts.ucmd() + .env("LC_ALL", "en_US.UTF-8") + .succeeds() + .stdout_is(&expected_stdout); +} diff --git a/tests/uutests/src/lib/util.rs b/tests/uutests/src/lib/util.rs index 93b3fd7d98f..69e38bf952f 100644 --- a/tests/uutests/src/lib/util.rs +++ b/tests/uutests/src/lib/util.rs @@ -3003,6 +3003,11 @@ fn parse_coreutil_version(version_string: &str) -> f32 { /// If the `util_name` in `$PATH` doesn't include a coreutils version string, /// or the version is too low, this returns an error and the test should be skipped. /// +/// Arguments: +/// - `ts`: The test context. +/// - `args`: Command-line variables applied to the command. +/// - `envs`: Environment variables applied to the command invocation. +/// /// Example: /// /// ```no_run @@ -3019,7 +3024,11 @@ fn parse_coreutil_version(version_string: &str) -> f32 { /// } ///``` #[cfg(unix)] -pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { +pub fn gnu_cmd_result( + ts: &TestScenario, + args: &[&str], + envs: &[(&str, &str)], +) -> std::result::Result { let util_name = ts.util_name.as_str(); println!("{}", check_coreutil_version(util_name, VERSION_MIN)?); let util_name = host_name_for(util_name); @@ -3028,6 +3037,7 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result< .cmd(util_name.as_ref()) .env("PATH", PATH) .envs(DEFAULT_ENV) + .envs(envs.iter().copied()) .args(args) .run(); @@ -3056,6 +3066,31 @@ pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result< )) } +/// This runs the GNU coreutils `util_name` binary in `$PATH` in order to +/// dynamically gather reference values on the system. +/// If the `util_name` in `$PATH` doesn't include a coreutils version string, +/// or the version is too low, this returns an error and the test should be skipped. +/// +/// Example: +/// +/// ```no_run +/// use uutests::util::*; +/// #[test] +/// fn test_xyz() { +/// let ts = TestScenario::new(util_name!()); +/// let result = ts.ucmd().run(); +/// let exp_result = unwrap_or_return!(expected_result(&ts, &[])); +/// result +/// .stdout_is(exp_result.stdout_str()) +/// .stderr_is(exp_result.stderr_str()) +/// .code_is(exp_result.code()); +/// } +///``` +#[cfg(unix)] +pub fn expected_result(ts: &TestScenario, args: &[&str]) -> std::result::Result { + gnu_cmd_result(ts, args, &[]) +} + /// This is a convenience wrapper to run a ucmd with root permissions. /// It can be used to test programs when being root is needed /// This runs `sudo -E --non-interactive target/debug/coreutils util_name args`