From ab6d95cdb9300f388625f56f0d112dc2517d92c4 Mon Sep 17 00:00:00 2001 From: Jeffrey Finkelstein Date: Sat, 18 Jan 2025 09:33:52 -0500 Subject: [PATCH] ls: display %Z alphabetic time zone abbreviation Display the alphabetic timezone abbreviation (like "UTC" or "CET") when the `--time-style` argument includes a `%Z` directive. This matches the behavior of `date`. Fixes #7035 --- Cargo.lock | 2 ++ src/uu/ls/Cargo.toml | 16 ++++++----- src/uu/ls/src/ls.rs | 60 ++++++++++++++++++++++++++++++++-------- tests/by-util/test_ls.rs | 11 ++++++++ 4 files changed, 70 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b2a67c13aa..29b14b9a820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2871,9 +2871,11 @@ version = "0.0.29" dependencies = [ "ansi-width", "chrono", + "chrono-tz", "clap", "glob", "hostname", + "iana-time-zone", "lscolors", "number_prefix", "once_cell", diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 17cef9b8aa4..0b60009e65b 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -18,13 +18,17 @@ path = "src/ls.rs" [dependencies] ansi-width = { workspace = true } -clap = { workspace = true, features = ["env"] } chrono = { workspace = true } -number_prefix = { workspace = true } -uutils_term_grid = { workspace = true } -terminal_size = { workspace = true } +chrono-tz = { workspace = true } +clap = { workspace = true, features = ["env"] } glob = { workspace = true } +hostname = { workspace = true } +iana-time-zone = { workspace = true } lscolors = { workspace = true } +number_prefix = { workspace = true } +once_cell = { workspace = true } +selinux = { workspace = true, optional = true } +terminal_size = { workspace = true } uucore = { workspace = true, features = [ "colors", "entries", @@ -34,9 +38,7 @@ uucore = { workspace = true, features = [ "quoting-style", "version-cmp", ] } -once_cell = { workspace = true } -selinux = { workspace = true, optional = true } -hostname = { workspace = true } +uutils_term_grid = { workspace = true } [[bin]] name = "ls" diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5850ff793aa..9aaa0d0a4e3 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -5,19 +5,9 @@ // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly -use clap::{ - builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, - crate_version, Arg, ArgAction, Command, -}; -use glob::{MatchOptions, Pattern}; -use lscolors::LsColors; - -use ansi_width::ansi_width; -use std::{cell::OnceCell, num::IntErrorKind}; -use std::{collections::HashSet, io::IsTerminal}; - #[cfg(windows)] use std::os::windows::fs::MetadataExt; +use std::{cell::OnceCell, num::IntErrorKind}; use std::{ cmp::Reverse, error::Error, @@ -34,7 +24,20 @@ use std::{ os::unix::fs::{FileTypeExt, MetadataExt}, time::Duration, }; +use std::{collections::HashSet, io::IsTerminal}; + +use ansi_width::ansi_width; +use chrono::{DateTime, Local, TimeDelta, TimeZone, Utc}; +use chrono_tz::{OffsetName, Tz}; +use clap::{ + builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, + crate_version, Arg, ArgAction, Command, +}; +use glob::{MatchOptions, Pattern}; +use iana_time_zone::get_timezone; +use lscolors::LsColors; use term_grid::{Direction, Filling, Grid, GridOptions}; + use uucore::error::USimpleError; use uucore::format::human::{human_readable, SizeFormat}; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] @@ -67,10 +70,12 @@ use uucore::{ version_cmp::version_cmp, }; use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning}; + mod dired; use dired::{is_dired_arg_present, DiredOutput}; mod colors; use colors::{color_name, StyleManager}; + #[cfg(not(feature = "selinux"))] static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)"; #[cfg(feature = "selinux")] @@ -334,6 +339,37 @@ enum TimeStyle { Format(String), } +/// Whether the given date is considered recent (i.e., in the last 6 months). +fn is_recent(time: DateTime) -> bool { + // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. + time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() +} + +/// Get the alphabetic abbreviation of the current timezone. +/// +/// For example, "UTC" or "CET" or "PDT". +fn timezone_abbrev() -> String { + let tz = match std::env::var("TZ") { + // TODO Support other time zones... + Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, + _ => match get_timezone() { + Ok(tz_str) => tz_str.parse().unwrap(), + Err(_) => Tz::Etc__UTC, + }, + }; + let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); + offset.abbreviation().unwrap_or("UTC").to_string() +} + +/// Format the given time according to a custom format string. +fn custom_time_format(fmt: &str, time: DateTime) -> String { + // TODO Refactor the common code from `ls` and `date` for rendering dates. + // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 + // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. + let fmt = fmt.replace("%N", "%f").replace("%Z", &timezone_abbrev()); + time.format(&fmt).to_string() +} + impl TimeStyle { /// Format the given time according to this time format style. fn format(&self, time: DateTime) -> String { @@ -350,7 +386,7 @@ impl TimeStyle { //So it's not yet implemented (Self::Locale, true) => time.format("%b %e %H:%M").to_string(), (Self::Locale, false) => time.format("%b %e %Y").to_string(), - (Self::Format(e), _) => time.format(e).to_string(), + (Self::Format(e), _) => custom_time_format(e, time), } } } diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 6ef7ac93a2e..715f18a1eaf 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -5628,3 +5628,14 @@ fn test_non_unicode_names() { .succeeds() .stdout_is_bytes(b"\xC0.dir\n\xC0.file\n"); } + +#[test] +fn test_time_style_timezone_name() { + let re_custom_format = Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* UTC f\n").unwrap(); + let (at, mut ucmd) = at_and_ucmd!(); + at.touch("f"); + ucmd.env("TZ", "UTC0") + .args(&["-l", "--time-style=+%Z"]) + .succeeds() + .stdout_matches(&re_custom_format); +}