Skip to content

Commit f877f64

Browse files
authored
Merge pull request #8415 from drinkcat/duls-time
`du`/`ls`: Improve time-style handling based on GNU coreutils manual
2 parents e48c4a7 + b13bf37 commit f877f64

File tree

10 files changed

+432
-84
lines changed

10 files changed

+432
-84
lines changed

src/uu/du/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ du-error-invalid-time-style = invalid argument { $style } for 'time style'
5252
- 'full-iso'
5353
- 'long-iso'
5454
- 'iso'
55+
- +FORMAT (e.g., +%H:%M) for a 'date'-style format
5556
Try '{ $help }' for more information.
5657
du-error-invalid-time-arg = 'birth' and 'creation' arguments for --time are not supported on this platform.
5758
du-error-invalid-glob = Invalid exclude syntax: { $error }

src/uu/du/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ du-error-invalid-time-style = argument invalide { $style } pour 'style de temps'
5252
- 'full-iso'
5353
- 'long-iso'
5454
- 'iso'
55+
- +FORMAT (e.g., +%H:%M) pour un format de type 'date'
5556
Essayez '{ $help }' pour plus d'informations.
5657
du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas supportés sur cette plateforme.
5758
du-error-invalid-glob = Syntaxe d'exclusion invalide : { $error }

src/uu/du/src/du.rs

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use uucore::translate;
2828
use uucore::parser::parse_glob;
2929
use uucore::parser::parse_size::{ParseSizeError, parse_size_u64};
3030
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
31-
use uucore::time::{FormatSystemTimeFallback, format_system_time};
31+
use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
3232
use uucore::{format_usage, show, show_error, show_warning};
3333
#[cfg(windows)]
3434
use windows_sys::Win32::Foundation::HANDLE;
@@ -666,9 +666,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
666666
};
667667

668668
let time_format = if time.is_some() {
669-
parse_time_style(matches.get_one::<String>("time-style").map(|s| s.as_str()))?.to_string()
669+
parse_time_style(matches.get_one::<String>("time-style"))?
670670
} else {
671-
"%Y-%m-%d %H:%M".to_string()
671+
format::LONG_ISO.to_string()
672672
};
673673

674674
let stat_printer = StatPrinter {
@@ -755,15 +755,40 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
755755
Ok(())
756756
}
757757

758-
fn parse_time_style(s: Option<&str>) -> UResult<&str> {
758+
// Parse --time-style argument, falling back to environment variable if necessary.
759+
fn parse_time_style(s: Option<&String>) -> UResult<String> {
760+
let s = match s {
761+
Some(s) => Some(s.into()),
762+
None => {
763+
match env::var("TIME_STYLE") {
764+
// Per GNU manual, strip `posix-` if present, ignore anything after a newline if
765+
// the string starts with +, and ignore "locale".
766+
Ok(s) => {
767+
let s = s.strip_prefix("posix-").unwrap_or(s.as_str());
768+
let s = match s.chars().next().unwrap() {
769+
'+' => s.split('\n').next().unwrap(),
770+
_ => s,
771+
};
772+
match s {
773+
"locale" => None,
774+
_ => Some(s.to_string()),
775+
}
776+
}
777+
Err(_) => None,
778+
}
779+
}
780+
};
759781
match s {
760-
Some(s) => match s {
761-
"full-iso" => Ok("%Y-%m-%d %H:%M:%S.%f %z"),
762-
"long-iso" => Ok("%Y-%m-%d %H:%M"),
763-
"iso" => Ok("%Y-%m-%d"),
764-
_ => Err(DuError::InvalidTimeStyleArg(s.into()).into()),
782+
Some(s) => match s.as_ref() {
783+
"full-iso" => Ok(format::FULL_ISO.to_string()),
784+
"long-iso" => Ok(format::LONG_ISO.to_string()),
785+
"iso" => Ok(format::ISO.to_string()),
786+
_ => match s.chars().next().unwrap() {
787+
'+' => Ok(s[1..].to_string()),
788+
_ => Err(DuError::InvalidTimeStyleArg(s).into()),
789+
},
765790
},
766-
None => Ok("%Y-%m-%d %H:%M"),
791+
None => Ok(format::LONG_ISO.to_string()),
767792
}
768793
}
769794

src/uu/ls/locales/en-US.ftl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ ls-error-invalid-block-size = invalid --block-size argument {$size}
1616
ls-error-dired-and-zero-incompatible = --dired and --zero are incompatible
1717
ls-error-not-listing-already-listed = {$path}: not listing already-listed directory
1818
ls-error-invalid-time-style = invalid --time-style argument {$style}
19-
Possible values are: {$values}
19+
Possible values are:
20+
- [posix-]full-iso
21+
- [posix-]long-iso
22+
- [posix-]iso
23+
- [posix-]locale
24+
- +FORMAT (e.g., +%H:%M) for a 'date'-style format
2025
2126
For more information try --help
2227

src/uu/ls/locales/fr-FR.ftl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ ls-error-invalid-block-size = argument --block-size invalide {$size}
1616
ls-error-dired-and-zero-incompatible = --dired et --zero sont incompatibles
1717
ls-error-not-listing-already-listed = {$path} : ne liste pas un répertoire déjà listé
1818
ls-error-invalid-time-style = argument --time-style invalide {$style}
19-
Les valeurs possibles sont : {$values}
19+
Les valeurs possibles sont :
20+
- [posix-]full-iso
21+
- [posix-]long-iso
22+
- [posix-]iso
23+
- [posix-]locale
24+
- +FORMAT (e.g., +%H:%M) pour un format de type 'date'
2025
2126
Pour plus d'informations, essayez --help
2227

src/uu/ls/src/ls.rs

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,26 @@
55

66
// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime
77

8+
#[cfg(unix)]
89
use std::collections::HashMap;
9-
use std::iter;
1010
#[cfg(unix)]
1111
use std::os::unix::fs::{FileTypeExt, MetadataExt};
1212
#[cfg(windows)]
1313
use std::os::windows::fs::MetadataExt;
14-
use std::{cell::LazyCell, cell::OnceCell, num::IntErrorKind};
1514
use std::{
15+
cell::{LazyCell, OnceCell},
1616
cmp::Reverse,
17+
collections::HashSet,
1718
ffi::{OsStr, OsString},
1819
fmt::Write as FmtWrite,
1920
fs::{self, DirEntry, FileType, Metadata, ReadDir},
20-
io::{BufWriter, ErrorKind, Stdout, Write, stdout},
21+
io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout},
22+
iter,
23+
num::IntErrorKind,
24+
ops::RangeInclusive,
2125
path::{Path, PathBuf},
2226
time::{Duration, SystemTime, UNIX_EPOCH},
2327
};
24-
use std::{collections::HashSet, io::IsTerminal};
2528

2629
use ansi_width::ansi_width;
2730
use clap::{
@@ -32,12 +35,9 @@ use glob::{MatchOptions, Pattern};
3235
use lscolors::LsColors;
3336
use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB};
3437
use thiserror::Error;
38+
3539
#[cfg(unix)]
3640
use uucore::entries;
37-
use uucore::error::USimpleError;
38-
use uucore::format::human::{SizeFormat, human_readable};
39-
use uucore::fs::FileInformation;
40-
use uucore::fsext::{MetadataTimeField, metadata_get_time};
4141
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
4242
use uucore::fsxattr::has_acl;
4343
#[cfg(unix)]
@@ -55,22 +55,25 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
5555
target_os = "solaris"
5656
))]
5757
use uucore::libc::{dev_t, major, minor};
58-
use uucore::line_ending::LineEnding;
59-
use uucore::translate;
60-
61-
use uucore::quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name};
62-
use uucore::time::{FormatSystemTimeFallback, format_system_time};
6358
use uucore::{
6459
display::Quotable,
65-
error::{UError, UResult, set_exit_code},
60+
error::{UError, UResult, USimpleError, set_exit_code},
61+
format::human::{SizeFormat, human_readable},
6662
format_usage,
63+
fs::FileInformation,
6764
fs::display_permissions,
65+
fsext::{MetadataTimeField, metadata_get_time},
66+
line_ending::LineEnding,
6867
os_str_as_bytes_lossy,
68+
parser::parse_glob,
6969
parser::parse_size::parse_size_u64,
7070
parser::shortcut_value_parser::ShortcutValueParser,
71+
quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name},
72+
show, show_error, show_warning,
73+
time::{FormatSystemTimeFallback, format, format_system_time},
74+
translate,
7175
version_cmp::version_cmp,
7276
};
73-
use uucore::{parser::parse_glob, show, show_error, show_warning};
7477

7578
mod dired;
7679
use dired::{DiredOutput, is_dired_arg_present};
@@ -203,8 +206,8 @@ enum LsError {
203206
#[error("{}", translate!("ls-error-not-listing-already-listed", "path" => .0.to_string_lossy()))]
204207
AlreadyListedError(PathBuf),
205208

206-
#[error("{}", translate!("ls-error-invalid-time-style", "style" => .0.quote(), "values" => format!("{:?}", .1)))]
207-
TimeStyleParseError(String, Vec<String>),
209+
#[error("{}", translate!("ls-error-invalid-time-style", "style" => .0.quote()))]
210+
TimeStyleParseError(String),
208211
}
209212

210213
impl UError for LsError {
@@ -217,7 +220,7 @@ impl UError for LsError {
217220
Self::BlockSizeParseError(_) => 2,
218221
Self::DiredAndZeroAreIncompatible => 2,
219222
Self::AlreadyListedError(_) => 2,
220-
Self::TimeStyleParseError(_, _) => 2,
223+
Self::TimeStyleParseError(_) => 2,
221224
}
222225
}
223226
}
@@ -250,53 +253,70 @@ enum Files {
250253
}
251254

252255
fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option<String>), LsError> {
253-
const TIME_STYLES: [(&str, (&str, Option<&str>)); 4] = [
254-
("full-iso", ("%Y-%m-%d %H:%M:%S.%f %z", None)),
255-
("long-iso", ("%Y-%m-%d %H:%M", None)),
256-
("iso", ("%m-%d %H:%M", Some("%Y-%m-%d "))),
257-
// TODO: Using correct locale string is not implemented.
258-
("locale", ("%b %e %H:%M", Some("%b %e %Y"))),
259-
];
260-
// A map from a time-style parameter to a length-2 tuple of formats:
261-
// the first one is used for recent dates, the second one for older ones (optional).
262-
let time_styles = HashMap::from(TIME_STYLES);
263-
let possible_time_styles = TIME_STYLES
264-
.iter()
265-
.map(|(x, _)| *x)
266-
.chain(iter::once(
267-
"+FORMAT (e.g., +%H:%M) for a 'date'-style format",
268-
))
269-
.map(|s| s.to_string());
256+
// TODO: Using correct locale string is not implemented.
257+
const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y"));
270258

271259
// Convert time_styles references to owned String/option.
272260
fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option<String>), LsError> {
273261
Ok((recent.to_string(), older.map(String::from)))
274262
}
275263

276-
if let Some(field) = options.get_one::<String>(options::TIME_STYLE) {
264+
if let Some(field) = options
265+
.get_one::<String>(options::TIME_STYLE)
266+
.map(|s| s.to_owned())
267+
.or_else(|| std::env::var("TIME_STYLE").ok())
268+
{
277269
//If both FULL_TIME and TIME_STYLE are present
278270
//The one added last is dominant
279271
if options.get_flag(options::FULL_TIME)
280272
&& options.indices_of(options::FULL_TIME).unwrap().next_back()
281273
> options.indices_of(options::TIME_STYLE).unwrap().next_back()
282274
{
283-
ok(time_styles["full-iso"])
275+
ok((format::FULL_ISO, None))
284276
} else {
285-
match time_styles.get(field.as_str()) {
286-
Some(formats) => ok(*formats),
287-
None => match field.chars().next().unwrap() {
288-
'+' => Ok((field[1..].to_string(), None)),
289-
_ => Err(LsError::TimeStyleParseError(
290-
String::from(field),
291-
possible_time_styles.collect(),
292-
)),
277+
let field = if let Some(field) = field.strip_prefix("posix-") {
278+
// See GNU documentation, set format to "locale" if LC_TIME="POSIX",
279+
// else just strip the prefix and continue (even "posix+FORMAT" is
280+
// supported).
281+
// TODO: This needs to be moved to uucore and handled by icu?
282+
if std::env::var("LC_TIME").unwrap_or_default() == "POSIX"
283+
|| std::env::var("LC_ALL").unwrap_or_default() == "POSIX"
284+
{
285+
return ok(LOCALE_FORMAT);
286+
}
287+
field
288+
} else {
289+
&field
290+
};
291+
292+
match field {
293+
"full-iso" => ok((format::FULL_ISO, None)),
294+
"long-iso" => ok((format::LONG_ISO, None)),
295+
// ISO older format needs extra padding.
296+
"iso" => Ok((
297+
"%m-%d %H:%M".to_string(),
298+
Some(format::ISO.to_string() + " "),
299+
)),
300+
"locale" => ok(LOCALE_FORMAT),
301+
_ => match field.chars().next().unwrap() {
302+
'+' => {
303+
// recent/older formats are (optionally) separated by a newline
304+
let mut it = field[1..].split('\n');
305+
let recent = it.next().unwrap_or_default();
306+
let older = it.next();
307+
match it.next() {
308+
None => ok((recent, older)),
309+
Some(_) => Err(LsError::TimeStyleParseError(String::from(field))),
310+
}
311+
}
312+
_ => Err(LsError::TimeStyleParseError(String::from(field))),
293313
},
294314
}
295315
}
296316
} else if options.get_flag(options::FULL_TIME) {
297-
ok(time_styles["full-iso"])
317+
ok((format::FULL_ISO, None))
298318
} else {
299-
ok(time_styles["locale"])
319+
ok(LOCALE_FORMAT)
300320
}
301321
}
302322

@@ -1941,7 +1961,7 @@ struct ListState<'a> {
19411961
uid_cache: HashMap<u32, String>,
19421962
#[cfg(unix)]
19431963
gid_cache: HashMap<u32, String>,
1944-
recent_time_threshold: SystemTime,
1964+
recent_time_range: RangeInclusive<SystemTime>,
19451965
}
19461966

19471967
#[allow(clippy::cognitive_complexity)]
@@ -1958,8 +1978,11 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
19581978
uid_cache: HashMap::new(),
19591979
#[cfg(unix)]
19601980
gid_cache: HashMap::new(),
1981+
// Time range for which to use the "recent" format. Anything from 0.5 year in the past to now
1982+
// (files with modification time in the future use "old" format).
19611983
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
1962-
recent_time_threshold: SystemTime::now() - Duration::new(31_556_952 / 2, 0),
1984+
recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0))
1985+
..=SystemTime::now(),
19631986
};
19641987

19651988
for loc in locs {
@@ -2943,7 +2966,7 @@ fn display_date(
29432966
// Use "recent" format if the given date is considered recent (i.e., in the last 6 months),
29442967
// or if no "older" format is available.
29452968
let fmt = match &config.time_format_older {
2946-
Some(time_format_older) if time <= state.recent_time_threshold => time_format_older,
2969+
Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older,
29472970
_ => &config.time_format_recent,
29482971
};
29492972

src/uucore/src/lib/features/time.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ pub fn system_time_to_sec(time: SystemTime) -> (i64, u32) {
3636
}
3737
}
3838

39+
pub mod format {
40+
pub static FULL_ISO: &str = "%Y-%m-%d %H:%M:%S.%N %z";
41+
pub static LONG_ISO: &str = "%Y-%m-%d %H:%M";
42+
pub static ISO: &str = "%Y-%m-%d";
43+
}
44+
3945
/// Sets how `format_system_time` behaves if the time cannot be converted.
4046
pub enum FormatSystemTimeFallback {
4147
Integer, // Just print seconds since epoch (`ls`)

0 commit comments

Comments
 (0)