diff --git a/src/uu/du/locales/en-US.ftl b/src/uu/du/locales/en-US.ftl index c9bd3bf52dc..b503d8d539f 100644 --- a/src/uu/du/locales/en-US.ftl +++ b/src/uu/du/locales/en-US.ftl @@ -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 } diff --git a/src/uu/du/locales/fr-FR.ftl b/src/uu/du/locales/fr-FR.ftl index fd242706221..e89385213aa 100644 --- a/src/uu/du/locales/fr-FR.ftl +++ b/src/uu/du/locales/fr-FR.ftl @@ -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 } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index a6ddc8a6a0d..f39d257de56 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -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::("time-style").map(|s| s.as_str()))?.to_string() + parse_time_style(matches.get_one::("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 { + 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()), } } diff --git a/src/uu/ls/locales/en-US.ftl b/src/uu/ls/locales/en-US.ftl index 308e9d6d727..43660330538 100644 --- a/src/uu/ls/locales/en-US.ftl +++ b/src/uu/ls/locales/en-US.ftl @@ -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 diff --git a/src/uu/ls/locales/fr-FR.ftl b/src/uu/ls/locales/fr-FR.ftl index 9aa81bd282e..e5016594685 100644 --- a/src/uu/ls/locales/fr-FR.ftl +++ b/src/uu/ls/locales/fr-FR.ftl @@ -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 diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 31108229bd1..272d563090d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -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), + #[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), 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), LsError> { Ok((recent.to_string(), older.map(String::from))) } - if let Some(field) = options.get_one::(options::TIME_STYLE) { + if let Some(field) = options + .get_one::(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, #[cfg(unix)] gid_cache: HashMap, - recent_time_threshold: SystemTime, + recent_time_range: RangeInclusive, } #[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, }; diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs index 4f1505b7027..b64d250c1aa 100644 --- a/src/uucore/src/lib/features/time.rs +++ b/src/uucore/src/lib/features/time.rs @@ -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`) diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 40352e88d90..6c4191a2089 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -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() diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 66227d06a59..734f70b1fc8 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -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] diff --git a/util/build-gnu.sh b/util/build-gnu.sh index f6270160031..e9a54998654 100755 --- a/util/build-gnu.sh +++ b/util/build-gnu.sh @@ -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