1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

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
This commit is contained in:
Jeffrey Finkelstein 2025-01-18 09:33:52 -05:00
parent ef0377d3da
commit ab6d95cdb9
4 changed files with 70 additions and 19 deletions

2
Cargo.lock generated
View file

@ -2871,9 +2871,11 @@ version = "0.0.29"
dependencies = [ dependencies = [
"ansi-width", "ansi-width",
"chrono", "chrono",
"chrono-tz",
"clap", "clap",
"glob", "glob",
"hostname", "hostname",
"iana-time-zone",
"lscolors", "lscolors",
"number_prefix", "number_prefix",
"once_cell", "once_cell",

View file

@ -18,13 +18,17 @@ path = "src/ls.rs"
[dependencies] [dependencies]
ansi-width = { workspace = true } ansi-width = { workspace = true }
clap = { workspace = true, features = ["env"] }
chrono = { workspace = true } chrono = { workspace = true }
number_prefix = { workspace = true } chrono-tz = { workspace = true }
uutils_term_grid = { workspace = true } clap = { workspace = true, features = ["env"] }
terminal_size = { workspace = true }
glob = { workspace = true } glob = { workspace = true }
hostname = { workspace = true }
iana-time-zone = { workspace = true }
lscolors = { 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 = [ uucore = { workspace = true, features = [
"colors", "colors",
"entries", "entries",
@ -34,9 +38,7 @@ uucore = { workspace = true, features = [
"quoting-style", "quoting-style",
"version-cmp", "version-cmp",
] } ] }
once_cell = { workspace = true } uutils_term_grid = { workspace = true }
selinux = { workspace = true, optional = true }
hostname = { workspace = true }
[[bin]] [[bin]]
name = "ls" name = "ls"

View file

@ -5,19 +5,9 @@
// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly // 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)] #[cfg(windows)]
use std::os::windows::fs::MetadataExt; use std::os::windows::fs::MetadataExt;
use std::{cell::OnceCell, num::IntErrorKind};
use std::{ use std::{
cmp::Reverse, cmp::Reverse,
error::Error, error::Error,
@ -34,7 +24,20 @@ use std::{
os::unix::fs::{FileTypeExt, MetadataExt}, os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration, 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 term_grid::{Direction, Filling, Grid, GridOptions};
use uucore::error::USimpleError; use uucore::error::USimpleError;
use uucore::format::human::{human_readable, SizeFormat}; use uucore::format::human::{human_readable, SizeFormat};
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
@ -67,10 +70,12 @@ use uucore::{
version_cmp::version_cmp, version_cmp::version_cmp,
}; };
use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning}; use uucore::{help_about, help_section, help_usage, parse_glob, show, show_error, show_warning};
mod dired; mod dired;
use dired::{is_dired_arg_present, DiredOutput}; use dired::{is_dired_arg_present, DiredOutput};
mod colors; mod colors;
use colors::{color_name, StyleManager}; use colors::{color_name, StyleManager};
#[cfg(not(feature = "selinux"))] #[cfg(not(feature = "selinux"))]
static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)"; static CONTEXT_HELP_TEXT: &str = "print any security context of each file (not enabled)";
#[cfg(feature = "selinux")] #[cfg(feature = "selinux")]
@ -334,6 +339,37 @@ enum TimeStyle {
Format(String), Format(String),
} }
/// Whether the given date is considered recent (i.e., in the last 6 months).
fn is_recent(time: DateTime<Local>) -> 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<Local>) -> 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 { impl TimeStyle {
/// Format the given time according to this time format style. /// Format the given time according to this time format style.
fn format(&self, time: DateTime<Local>) -> String { fn format(&self, time: DateTime<Local>) -> String {
@ -350,7 +386,7 @@ impl TimeStyle {
//So it's not yet implemented //So it's not yet implemented
(Self::Locale, true) => time.format("%b %e %H:%M").to_string(), (Self::Locale, true) => time.format("%b %e %H:%M").to_string(),
(Self::Locale, false) => time.format("%b %e %Y").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),
} }
} }
} }

View file

@ -5628,3 +5628,14 @@ fn test_non_unicode_names() {
.succeeds() .succeeds()
.stdout_is_bytes(b"\xC0.dir\n\xC0.file\n"); .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);
}