Merge pull request #8415 from drinkcat/duls-time

`du`/`ls`: Improve time-style handling based on GNU coreutils manual
This commit is contained in:
Sylvestre Ledru 2025-07-31 08:58:34 +02:00 committed by GitHub
commit f877f64362
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 432 additions and 84 deletions

View file

@ -52,6 +52,7 @@ du-error-invalid-time-style = invalid argument { $style } for 'time style'
- 'full-iso'
- 'long-iso'
- 'iso'
- +FORMAT (e.g., +%H:%M) for a 'date'-style format
Try '{ $help }' for more information.
du-error-invalid-time-arg = 'birth' and 'creation' arguments for --time are not supported on this platform.
du-error-invalid-glob = Invalid exclude syntax: { $error }

View file

@ -52,6 +52,7 @@ du-error-invalid-time-style = argument invalide { $style } pour 'style de temps'
- 'full-iso'
- 'long-iso'
- 'iso'
- +FORMAT (e.g., +%H:%M) pour un format de type 'date'
Essayez '{ $help }' pour plus d'informations.
du-error-invalid-time-arg = les arguments 'birth' et 'creation' pour --time ne sont pas supportés sur cette plateforme.
du-error-invalid-glob = Syntaxe d'exclusion invalide : { $error }

View file

@ -28,7 +28,7 @@ use uucore::translate;
use uucore::parser::parse_glob;
use uucore::parser::parse_size::{ParseSizeError, parse_size_u64};
use uucore::parser::shortcut_value_parser::ShortcutValueParser;
use uucore::time::{FormatSystemTimeFallback, format_system_time};
use uucore::time::{FormatSystemTimeFallback, format, format_system_time};
use uucore::{format_usage, show, show_error, show_warning};
#[cfg(windows)]
use windows_sys::Win32::Foundation::HANDLE;
@ -666,9 +666,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let time_format = if time.is_some() {
parse_time_style(matches.get_one::<String>("time-style").map(|s| s.as_str()))?.to_string()
parse_time_style(matches.get_one::<String>("time-style"))?
} else {
"%Y-%m-%d %H:%M".to_string()
format::LONG_ISO.to_string()
};
let stat_printer = StatPrinter {
@ -755,15 +755,40 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Ok(())
}
fn parse_time_style(s: Option<&str>) -> UResult<&str> {
// Parse --time-style argument, falling back to environment variable if necessary.
fn parse_time_style(s: Option<&String>) -> UResult<String> {
let s = match s {
Some(s) => Some(s.into()),
None => {
match env::var("TIME_STYLE") {
// Per GNU manual, strip `posix-` if present, ignore anything after a newline if
// the string starts with +, and ignore "locale".
Ok(s) => {
let s = s.strip_prefix("posix-").unwrap_or(s.as_str());
let s = match s.chars().next().unwrap() {
'+' => s.split('\n').next().unwrap(),
_ => s,
};
match s {
"locale" => None,
_ => Some(s.to_string()),
}
}
Err(_) => None,
}
}
};
match s {
Some(s) => match s {
"full-iso" => Ok("%Y-%m-%d %H:%M:%S.%f %z"),
"long-iso" => Ok("%Y-%m-%d %H:%M"),
"iso" => Ok("%Y-%m-%d"),
_ => Err(DuError::InvalidTimeStyleArg(s.into()).into()),
Some(s) => match s.as_ref() {
"full-iso" => Ok(format::FULL_ISO.to_string()),
"long-iso" => Ok(format::LONG_ISO.to_string()),
"iso" => Ok(format::ISO.to_string()),
_ => match s.chars().next().unwrap() {
'+' => Ok(s[1..].to_string()),
_ => Err(DuError::InvalidTimeStyleArg(s).into()),
},
},
None => Ok("%Y-%m-%d %H:%M"),
None => Ok(format::LONG_ISO.to_string()),
}
}

View file

@ -16,7 +16,12 @@ ls-error-invalid-block-size = invalid --block-size argument {$size}
ls-error-dired-and-zero-incompatible = --dired and --zero are incompatible
ls-error-not-listing-already-listed = {$path}: not listing already-listed directory
ls-error-invalid-time-style = invalid --time-style argument {$style}
Possible values are: {$values}
Possible values are:
- [posix-]full-iso
- [posix-]long-iso
- [posix-]iso
- [posix-]locale
- +FORMAT (e.g., +%H:%M) for a 'date'-style format
For more information try --help

View file

@ -16,7 +16,12 @@ ls-error-invalid-block-size = argument --block-size invalide {$size}
ls-error-dired-and-zero-incompatible = --dired et --zero sont incompatibles
ls-error-not-listing-already-listed = {$path} : ne liste pas un répertoire déjà listé
ls-error-invalid-time-style = argument --time-style invalide {$style}
Les valeurs possibles sont : {$values}
Les valeurs possibles sont :
- [posix-]full-iso
- [posix-]long-iso
- [posix-]iso
- [posix-]locale
- +FORMAT (e.g., +%H:%M) pour un format de type 'date'
Pour plus d'informations, essayez --help

View file

@ -5,23 +5,26 @@
// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime
#[cfg(unix)]
use std::collections::HashMap;
use std::iter;
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt};
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
use std::{cell::LazyCell, cell::OnceCell, num::IntErrorKind};
use std::{
cell::{LazyCell, OnceCell},
cmp::Reverse,
collections::HashSet,
ffi::{OsStr, OsString},
fmt::Write as FmtWrite,
fs::{self, DirEntry, FileType, Metadata, ReadDir},
io::{BufWriter, ErrorKind, Stdout, Write, stdout},
io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout},
iter,
num::IntErrorKind,
ops::RangeInclusive,
path::{Path, PathBuf},
time::{Duration, SystemTime, UNIX_EPOCH},
};
use std::{collections::HashSet, io::IsTerminal};
use ansi_width::ansi_width;
use clap::{
@ -32,12 +35,9 @@ use glob::{MatchOptions, Pattern};
use lscolors::LsColors;
use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB};
use thiserror::Error;
#[cfg(unix)]
use uucore::entries;
use uucore::error::USimpleError;
use uucore::format::human::{SizeFormat, human_readable};
use uucore::fs::FileInformation;
use uucore::fsext::{MetadataTimeField, metadata_get_time};
#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))]
use uucore::fsxattr::has_acl;
#[cfg(unix)]
@ -55,22 +55,25 @@ use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
target_os = "solaris"
))]
use uucore::libc::{dev_t, major, minor};
use uucore::line_ending::LineEnding;
use uucore::translate;
use uucore::quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name};
use uucore::time::{FormatSystemTimeFallback, format_system_time};
use uucore::{
display::Quotable,
error::{UError, UResult, set_exit_code},
error::{UError, UResult, USimpleError, set_exit_code},
format::human::{SizeFormat, human_readable},
format_usage,
fs::FileInformation,
fs::display_permissions,
fsext::{MetadataTimeField, metadata_get_time},
line_ending::LineEnding,
os_str_as_bytes_lossy,
parser::parse_glob,
parser::parse_size::parse_size_u64,
parser::shortcut_value_parser::ShortcutValueParser,
quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name},
show, show_error, show_warning,
time::{FormatSystemTimeFallback, format, format_system_time},
translate,
version_cmp::version_cmp,
};
use uucore::{parser::parse_glob, show, show_error, show_warning};
mod dired;
use dired::{DiredOutput, is_dired_arg_present};
@ -203,8 +206,8 @@ enum LsError {
#[error("{}", translate!("ls-error-not-listing-already-listed", "path" => .0.to_string_lossy()))]
AlreadyListedError(PathBuf),
#[error("{}", translate!("ls-error-invalid-time-style", "style" => .0.quote(), "values" => format!("{:?}", .1)))]
TimeStyleParseError(String, Vec<String>),
#[error("{}", translate!("ls-error-invalid-time-style", "style" => .0.quote()))]
TimeStyleParseError(String),
}
impl UError for LsError {
@ -217,7 +220,7 @@ impl UError for LsError {
Self::BlockSizeParseError(_) => 2,
Self::DiredAndZeroAreIncompatible => 2,
Self::AlreadyListedError(_) => 2,
Self::TimeStyleParseError(_, _) => 2,
Self::TimeStyleParseError(_) => 2,
}
}
}
@ -250,53 +253,70 @@ enum Files {
}
fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option<String>), LsError> {
const TIME_STYLES: [(&str, (&str, Option<&str>)); 4] = [
("full-iso", ("%Y-%m-%d %H:%M:%S.%f %z", None)),
("long-iso", ("%Y-%m-%d %H:%M", None)),
("iso", ("%m-%d %H:%M", Some("%Y-%m-%d "))),
// TODO: Using correct locale string is not implemented.
("locale", ("%b %e %H:%M", Some("%b %e %Y"))),
];
// A map from a time-style parameter to a length-2 tuple of formats:
// the first one is used for recent dates, the second one for older ones (optional).
let time_styles = HashMap::from(TIME_STYLES);
let possible_time_styles = TIME_STYLES
.iter()
.map(|(x, _)| *x)
.chain(iter::once(
"+FORMAT (e.g., +%H:%M) for a 'date'-style format",
))
.map(|s| s.to_string());
// TODO: Using correct locale string is not implemented.
const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y"));
// Convert time_styles references to owned String/option.
fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option<String>), LsError> {
Ok((recent.to_string(), older.map(String::from)))
}
if let Some(field) = options.get_one::<String>(options::TIME_STYLE) {
if let Some(field) = options
.get_one::<String>(options::TIME_STYLE)
.map(|s| s.to_owned())
.or_else(|| std::env::var("TIME_STYLE").ok())
{
//If both FULL_TIME and TIME_STYLE are present
//The one added last is dominant
if options.get_flag(options::FULL_TIME)
&& options.indices_of(options::FULL_TIME).unwrap().next_back()
> options.indices_of(options::TIME_STYLE).unwrap().next_back()
{
ok(time_styles["full-iso"])
ok((format::FULL_ISO, None))
} else {
match time_styles.get(field.as_str()) {
Some(formats) => ok(*formats),
None => match field.chars().next().unwrap() {
'+' => Ok((field[1..].to_string(), None)),
_ => Err(LsError::TimeStyleParseError(
String::from(field),
possible_time_styles.collect(),
)),
let field = if let Some(field) = field.strip_prefix("posix-") {
// See GNU documentation, set format to "locale" if LC_TIME="POSIX",
// else just strip the prefix and continue (even "posix+FORMAT" is
// supported).
// TODO: This needs to be moved to uucore and handled by icu?
if std::env::var("LC_TIME").unwrap_or_default() == "POSIX"
|| std::env::var("LC_ALL").unwrap_or_default() == "POSIX"
{
return ok(LOCALE_FORMAT);
}
field
} else {
&field
};
match field {
"full-iso" => ok((format::FULL_ISO, None)),
"long-iso" => ok((format::LONG_ISO, None)),
// ISO older format needs extra padding.
"iso" => Ok((
"%m-%d %H:%M".to_string(),
Some(format::ISO.to_string() + " "),
)),
"locale" => ok(LOCALE_FORMAT),
_ => match field.chars().next().unwrap() {
'+' => {
// recent/older formats are (optionally) separated by a newline
let mut it = field[1..].split('\n');
let recent = it.next().unwrap_or_default();
let older = it.next();
match it.next() {
None => ok((recent, older)),
Some(_) => Err(LsError::TimeStyleParseError(String::from(field))),
}
}
_ => Err(LsError::TimeStyleParseError(String::from(field))),
},
}
}
} else if options.get_flag(options::FULL_TIME) {
ok(time_styles["full-iso"])
ok((format::FULL_ISO, None))
} else {
ok(time_styles["locale"])
ok(LOCALE_FORMAT)
}
}
@ -1941,7 +1961,7 @@ struct ListState<'a> {
uid_cache: HashMap<u32, String>,
#[cfg(unix)]
gid_cache: HashMap<u32, String>,
recent_time_threshold: SystemTime,
recent_time_range: RangeInclusive<SystemTime>,
}
#[allow(clippy::cognitive_complexity)]
@ -1958,8 +1978,11 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
uid_cache: HashMap::new(),
#[cfg(unix)]
gid_cache: HashMap::new(),
// Time range for which to use the "recent" format. Anything from 0.5 year in the past to now
// (files with modification time in the future use "old" format).
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
recent_time_threshold: SystemTime::now() - Duration::new(31_556_952 / 2, 0),
recent_time_range: (SystemTime::now() - Duration::new(31_556_952 / 2, 0))
..=SystemTime::now(),
};
for loc in locs {
@ -2943,7 +2966,7 @@ fn display_date(
// Use "recent" format if the given date is considered recent (i.e., in the last 6 months),
// or if no "older" format is available.
let fmt = match &config.time_format_older {
Some(time_format_older) if time <= state.recent_time_threshold => time_format_older,
Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older,
_ => &config.time_format_recent,
};

View file

@ -36,6 +36,12 @@ pub fn system_time_to_sec(time: SystemTime) -> (i64, u32) {
}
}
pub mod format {
pub static FULL_ISO: &str = "%Y-%m-%d %H:%M:%S.%N %z";
pub static LONG_ISO: &str = "%Y-%m-%d %H:%M";
pub static ISO: &str = "%Y-%m-%d";
}
/// Sets how `format_system_time` behaves if the time cannot be converted.
pub enum FormatSystemTimeFallback {
Integer, // Just print seconds since epoch (`ls`)

View file

@ -591,6 +591,7 @@ fn test_du_h_precision() {
}
}
#[allow(clippy::too_many_lines)]
#[cfg(feature = "touch")]
#[test]
fn test_du_time() {
@ -626,6 +627,107 @@ fn test_du_time() {
.succeeds();
result.stdout_only("0\t2016-06-16 00:00\tdate_test\n");
// long-iso (same as default)
let result = ts
.ucmd()
.env("TZ", "UTC")
.arg("--time")
.arg("--time-style=long-iso")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16 00:00\tdate_test\n");
// full-iso
let result = ts
.ucmd()
.env("TZ", "UTC")
.arg("--time")
.arg("--time-style=full-iso")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16 00:00:00.000000000 +0000\tdate_test\n");
// iso
let result = ts
.ucmd()
.env("TZ", "UTC")
.arg("--time")
.arg("--time-style=iso")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16\tdate_test\n");
// custom +FORMAT
let result = ts
.ucmd()
.env("TZ", "UTC")
.arg("--time")
.arg("--time-style=+%Y__%H")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016__00\tdate_test\n");
// ls has special handling for new line in format, du doesn't.
let result = ts
.ucmd()
.env("TZ", "UTC")
.arg("--time")
.arg("--time-style=+%Y_\n_%H")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016_\n_00\tdate_test\n");
// Time style can also be setup from environment
let result = ts
.ucmd()
.env("TZ", "UTC")
.env("TIME_STYLE", "full-iso")
.arg("--time")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16 00:00:00.000000000 +0000\tdate_test\n");
// For compatibility reason, we also allow posix- prefix.
let result = ts
.ucmd()
.env("TZ", "UTC")
.env("TIME_STYLE", "posix-full-iso")
.arg("--time")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16 00:00:00.000000000 +0000\tdate_test\n");
// ... and we strip content after a new line
let result = ts
.ucmd()
.env("TZ", "UTC")
.env("TIME_STYLE", "+XXX\nYYY")
.arg("--time")
.arg("date_test")
.succeeds();
result.stdout_only("0\tXXX\tdate_test\n");
// ... and we ignore "locale", fall back to full-iso.
let result = ts
.ucmd()
.env("TZ", "UTC")
.env("TIME_STYLE", "locale")
.arg("--time")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16 00:00\tdate_test\n");
// Command line option takes precedence
let result = ts
.ucmd()
.env("TZ", "UTC")
.env("TIME_STYLE", "full-iso")
.arg("--time")
.arg("--time-style=iso")
.arg("date_test")
.succeeds();
result.stdout_only("0\t2016-06-16\tdate_test\n");
for argument in ["--time=atime", "--time=atim", "--time=a"] {
let result = ts
.ucmd()

View file

@ -19,11 +19,11 @@ use std::collections::HashMap;
use std::ffi::OsStr;
#[cfg(target_os = "linux")]
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
#[cfg(not(windows))]
use std::path::PathBuf;
use std::thread::sleep;
use std::time::Duration;
use std::{path::Path, time::SystemTime};
use uutests::new_ucmd;
#[cfg(unix)]
use uutests::unwrap_or_return;
@ -1923,24 +1923,38 @@ fn test_ls_order_birthtime() {
}
#[test]
fn test_ls_styles() {
fn test_ls_time_styles() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
// Create a recent and old (<6 months) file, as format can be different.
at.touch("test");
let f3 = at.make_file("test-old");
f3.set_modified(SystemTime::now() - Duration::from_secs(3600 * 24 * 365))
.unwrap();
let re_full = Regex::new(
let re_full_recent = Regex::new(
r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d* (\+|\-)\d{4} test\n",
)
.unwrap();
let re_long =
let re_long_recent =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_iso =
let re_iso_recent =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_locale =
let re_locale_recent =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* [A-Z][a-z]{2} ( |\d)\d \d{2}:\d{2} test\n")
.unwrap();
let re_custom_format =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}__\d{2} test\n").unwrap();
let re_full_old = Regex::new(
r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d* (\+|\-)\d{4} test-old\n",
)
.unwrap();
let re_long_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} \d{2}:\d{2} test-old\n")
.unwrap();
let re_iso_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} test-old\n").unwrap();
let re_locale_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* [A-Z][a-z]{2} ( |\d)\d \d{4} test-old\n")
.unwrap();
//full-iso
scene
@ -1948,36 +1962,99 @@ fn test_ls_styles() {
.arg("-l")
.arg("--time-style=full-iso")
.succeeds()
.stdout_matches(&re_full);
.stdout_matches(&re_full_recent)
.stdout_matches(&re_full_old);
//long-iso
scene
.ucmd()
.arg("-l")
.arg("--time-style=long-iso")
.succeeds()
.stdout_matches(&re_long);
.stdout_matches(&re_long_recent)
.stdout_matches(&re_long_old);
//iso
scene
.ucmd()
.arg("-l")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso);
.stdout_matches(&re_iso_recent)
.stdout_matches(&re_iso_old);
//locale
scene
.ucmd()
.arg("-l")
.arg("--time-style=locale")
.succeeds()
.stdout_matches(&re_locale);
.stdout_matches(&re_locale_recent)
.stdout_matches(&re_locale_old);
//posix-full-iso
scene
.ucmd()
.arg("-l")
.arg("--time-style=posix-full-iso")
.succeeds()
.stdout_matches(&re_full_recent)
.stdout_matches(&re_full_old);
//posix-long-iso
scene
.ucmd()
.arg("-l")
.arg("--time-style=posix-long-iso")
.succeeds()
.stdout_matches(&re_long_recent)
.stdout_matches(&re_long_old);
//posix-iso
scene
.ucmd()
.arg("-l")
.arg("--time-style=posix-iso")
.succeeds()
.stdout_matches(&re_iso_recent)
.stdout_matches(&re_iso_old);
//posix-* with LC_TIME/LC_ALL=POSIX is equivalent to locale
scene
.ucmd()
.env("LC_TIME", "POSIX")
.arg("-l")
.arg("--time-style=posix-full-iso")
.succeeds()
.stdout_matches(&re_locale_recent)
.stdout_matches(&re_locale_old);
scene
.ucmd()
.env("LC_ALL", "POSIX")
.arg("-l")
.arg("--time-style=posix-iso")
.succeeds()
.stdout_matches(&re_locale_recent)
.stdout_matches(&re_locale_old);
//+FORMAT
let re_custom_format_recent =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}__\d{2} test\n").unwrap();
let re_custom_format_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}__\d{2} test-old\n").unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=+%Y__%M")
.succeeds()
.stdout_matches(&re_custom_format);
.stdout_matches(&re_custom_format_recent)
.stdout_matches(&re_custom_format_old);
//+FORMAT_RECENT\nFORMAT_OLD
let re_custom_format_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}--\d{2} test-old\n").unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=+%Y__%M\n%Y--%M")
.succeeds()
.stdout_matches(&re_custom_format_recent)
.stdout_matches(&re_custom_format_old);
// Also fails due to not having full clap support for time_styles
scene
@ -1986,6 +2063,13 @@ fn test_ls_styles() {
.arg("--time-style=invalid")
.fails_with_code(2);
// Cannot have 2 new lines in custom format
scene
.ucmd()
.arg("-l")
.arg("--time-style=+%Y__%M\n%Y--%M\n")
.fails_with_code(2);
//Overwrite options tests
scene
.ucmd()
@ -1993,19 +2077,19 @@ fn test_ls_styles() {
.arg("--time-style=long-iso")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso);
.stdout_matches(&re_iso_recent);
scene
.ucmd()
.arg("--time-style=iso")
.arg("--full-time")
.succeeds()
.stdout_matches(&re_full);
.stdout_matches(&re_full_recent);
scene
.ucmd()
.arg("--full-time")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso);
.stdout_matches(&re_iso_recent);
scene
.ucmd()
@ -2013,7 +2097,7 @@ fn test_ls_styles() {
.arg("--time-style=iso")
.arg("--full-time")
.succeeds()
.stdout_matches(&re_full);
.stdout_matches(&re_full_recent);
scene
.ucmd()
@ -2021,15 +2105,109 @@ fn test_ls_styles() {
.arg("-x")
.arg("-l")
.succeeds()
.stdout_matches(&re_full);
.stdout_matches(&re_full_recent);
at.touch("test2");
scene
.ucmd()
.arg("--full-time")
.arg("-x")
.succeeds()
.stdout_is("test test2\n");
.stdout_is("test test-old\n");
// Time style can also be setup from environment
scene
.ucmd()
.env("TIME_STYLE", "full-iso")
.arg("-l")
.succeeds()
.stdout_matches(&re_full_recent);
// ... but option takes precedence
scene
.ucmd()
.env("TIME_STYLE", "full-iso")
.arg("-l")
.arg("--time-style=long-iso")
.succeeds()
.stdout_matches(&re_long_recent);
}
#[test]
fn test_ls_time_recent_future() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
let f = at.make_file("test");
let re_iso_recent =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_iso_old =
Regex::new(r"[a-z-]* \d* [\w.]* [\w.]* \d* \d{4}-\d{2}-\d{2} test\n").unwrap();
// `test` has just been created, so it's recent
scene
.ucmd()
.arg("-l")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso_recent);
// 100 days ago is still recent (<0.5 years)
f.set_modified(SystemTime::now() - Duration::from_secs(3600 * 24 * 100))
.unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso_recent);
// 200 days ago is not recent
f.set_modified(SystemTime::now() - Duration::from_secs(3600 * 24 * 200))
.unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso_old);
// A timestamp in the future (even just a minute), is not considered "recent"
f.set_modified(SystemTime::now() + Duration::from_secs(60))
.unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=iso")
.succeeds()
.stdout_matches(&re_iso_old);
// Also test that we can set a format that varies for recent of older files.
//+FORMAT_RECENT\nFORMAT_OLD
f.set_modified(SystemTime::now()).unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=+RECENT\nOLD")
.succeeds()
.stdout_contains("RECENT");
// Old file
f.set_modified(SystemTime::now() - Duration::from_secs(3600 * 24 * 200))
.unwrap();
scene
.ucmd()
.arg("-l")
.arg("--time-style=+RECENT\nOLD")
.succeeds()
.stdout_contains("OLD");
// RECENT format is still used if no "OLD" one provided.
scene
.ucmd()
.arg("-l")
.arg("--time-style=+RECENT")
.succeeds()
.stdout_contains("RECENT");
}
#[test]

View file

@ -329,8 +329,10 @@ sed -i -e "s|44 45|48 49|" tests/ls/stat-failed.sh
# small difference in the error message
# Use GNU sed for /c command
"${SED}" -i -e "/ls: invalid argument 'XX' for 'time style'/,/Try 'ls --help' for more information\./c\
ls: invalid --time-style argument 'XX'\nPossible values are: [\"full-iso\", \"long-iso\", \"iso\", \"locale\", \"+FORMAT (e.g., +%H:%M) for a 'date'-style format\"]\n\nFor more information try --help" tests/ls/time-style-diag.sh
"${SED}" -i -e "s/ls: invalid argument 'XX' for 'time style'/ls: invalid --time-style argument 'XX'/" \
-e "s/Valid arguments are:/Possible values are:/" \
-e "s/Try 'ls --help' for more information./\nFor more information try --help/" \
tests/ls/time-style-diag.sh
# disable two kind of tests:
# "hostid BEFORE --help" doesn't fail for GNU. we fail. we are probably doing better