ls: improvements on time handling (#1986)

* ls: added creation time

* ls: Added most time features

Missing support for posix-,Format+, translating via locales. Also required more tests

* ls: rustfmt

* ls: Additional changes and fixes

Fixed the argument order, fixed a wrong iso format.

* ls: additional tests for styles

* ls: perfected arg parsing on time styles

* fix birthime test

* ls: Use 'stdout_str' in new tests

* ls: Disabled birthtime test for windows

* ls: removed indoc as a dependency

* ls: birthime test, sync first created file

* ls: birthime test, add comment explaining sync

* Removed ruby testfile birth_test.rb

This accidentally got commited in a merge
This commit is contained in:
Rein F 2021-04-28 20:54:27 +02:00 committed by GitHub
parent 167520067c
commit a60fd07bc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 245 additions and 23 deletions

33
Cargo.lock generated
View file

@ -165,13 +165,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.11" version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [ dependencies = [
"libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time", "time",
"winapi 0.3.9",
] ]
[[package]] [[package]]
@ -732,6 +734,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec" checksum = "46dbcb333e86939721589d25a3557e180b52778cb33c7fdfe9e0158ff790d5ec"
[[package]]
name = "indoc"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136"
dependencies = [
"unindent",
]
[[package]] [[package]]
name = "ioctl-sys" name = "ioctl-sys"
version = "0.5.2" version = "0.5.2"
@ -802,6 +813,15 @@ version = "0.2.85"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3"
[[package]]
name = "locale"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fdbe492a9c0238da900a1165c42fc5067161ce292678a6fe80921f30fe307fd"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.14" version = "0.4.14"
@ -1573,12 +1593,11 @@ dependencies = [
[[package]] [[package]]
name = "time" name = "time"
version = "0.1.42" version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
dependencies = [ dependencies = [
"libc", "libc",
"redox_syscall 0.1.57",
"winapi 0.3.9", "winapi 0.3.9",
] ]
@ -2060,15 +2079,17 @@ name = "uu_ls"
version = "0.0.6" version = "0.0.6"
dependencies = [ dependencies = [
"atty", "atty",
"chrono",
"clap", "clap",
"globset", "globset",
"indoc",
"lazy_static", "lazy_static",
"locale",
"lscolors", "lscolors",
"number_prefix", "number_prefix",
"once_cell", "once_cell",
"term_grid", "term_grid",
"termsize", "termsize",
"time",
"unicode-width", "unicode-width",
"uucore", "uucore",
"uucore_procs", "uucore_procs",

View file

@ -15,16 +15,17 @@ edition = "2018"
path = "src/ls.rs" path = "src/ls.rs"
[dependencies] [dependencies]
locale = "0.2.2"
chrono = "0.4.19"
clap = "2.33" clap = "2.33"
unicode-width = "0.1.8" unicode-width = "0.1.8"
number_prefix = "0.4" number_prefix = "0.4"
term_grid = "0.1.5" term_grid = "0.1.5"
termsize = "0.1.6" termsize = "0.1.6"
time = "0.1.40"
globset = "0.4.6" globset = "0.4.6"
lscolors = { version="0.7.1", features=["ansi_term"] } lscolors = { version = "0.7.1", features = ["ansi_term"] }
uucore = { version=">=0.0.8", package="uucore", path="../../uucore", features=["entries", "fs"] } uucore = { version = ">=0.0.8", package = "uucore", path = "../../uucore", features = ["entries", "fs"] }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version = ">=0.0.5", package = "uucore_procs", path = "../../uucore_procs" }
once_cell = "1.7.2" once_cell = "1.7.2"
atty = "0.2" atty = "0.2"

View file

@ -38,8 +38,11 @@ use std::{
os::unix::fs::{FileTypeExt, MetadataExt}, os::unix::fs::{FileTypeExt, MetadataExt},
time::Duration, time::Duration,
}; };
use chrono;
use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
use time::{strftime, Timespec};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
#[cfg(unix)] #[cfg(unix)]
use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR};
@ -50,6 +53,8 @@ static ABOUT: &str = "
the command line, expect that it will ignore files and directories the command line, expect that it will ignore files and directories
whose names start with '.' whose names start with '.'
"; ";
static AFTER_HELP: &str = "The TIME_STYLE argument can be full-iso, long-iso, iso.
Also the TIME_STYLE environment variable sets the default style to use.";
fn get_usage() -> String { fn get_usage() -> String {
format!("{0} [OPTION]... [FILE]...", executable!()) format!("{0} [OPTION]... [FILE]...", executable!())
@ -117,6 +122,8 @@ pub mod options {
pub static COLOR: &str = "color"; pub static COLOR: &str = "color";
pub static PATHS: &str = "paths"; pub static PATHS: &str = "paths";
pub static INDICATOR_STYLE: &str = "indicator-style"; pub static INDICATOR_STYLE: &str = "indicator-style";
pub static TIME_STYLE: &str = "time-style";
pub static FULL_TIME: &str = "full-time";
pub static HIDE: &str = "hide"; pub static HIDE: &str = "hide";
pub static IGNORE: &str = "ignore"; pub static IGNORE: &str = "ignore";
} }
@ -156,6 +163,15 @@ enum Time {
Modification, Modification,
Access, Access,
Change, Change,
Birth,
}
#[derive(Debug)]
enum TimeStyle {
FullIso,
LongIso,
Iso,
Locale,
} }
enum Dereference { enum Dereference {
@ -191,6 +207,7 @@ struct Config {
width: Option<u16>, width: Option<u16>,
quoting_style: QuotingStyle, quoting_style: QuotingStyle,
indicator_style: IndicatorStyle, indicator_style: IndicatorStyle,
time_style: TimeStyle,
} }
// Fields that can be removed or added to the long format // Fields that can be removed or added to the long format
@ -251,6 +268,7 @@ impl Config {
options::format::LONG_NO_OWNER, options::format::LONG_NO_OWNER,
options::format::LONG_NO_GROUP, options::format::LONG_NO_GROUP,
options::format::LONG_NUMERIC_UID_GID, options::format::LONG_NUMERIC_UID_GID,
options::FULL_TIME,
] ]
.iter() .iter()
.flat_map(|opt| options.indices_of(opt)) .flat_map(|opt| options.indices_of(opt))
@ -302,6 +320,7 @@ impl Config {
match field { match field {
"ctime" | "status" => Time::Change, "ctime" | "status" => Time::Change,
"access" | "atime" | "use" => Time::Access, "access" | "atime" | "use" => Time::Access,
"birth" | "creation" => Time::Birth,
// below should never happen as clap already restricts the values. // below should never happen as clap already restricts the values.
_ => unreachable!("Invalid field for --time"), _ => unreachable!("Invalid field for --time"),
} }
@ -439,6 +458,30 @@ impl Config {
IndicatorStyle::None IndicatorStyle::None
}; };
let time_style = if let Some(field) = options.value_of(options::TIME_STYLE) {
//If both FULL_TIME and TIME_STYLE are present
//The one added last is dominant
if options.is_present(options::FULL_TIME)
&& options.indices_of(options::FULL_TIME).unwrap().last()
> options.indices_of(options::TIME_STYLE).unwrap().last()
{
TimeStyle::FullIso
} else {
//Clap handles the env variable "TIME_STYLE"
match field {
"full-iso" => TimeStyle::FullIso,
"long-iso" => TimeStyle::LongIso,
"iso" => TimeStyle::Iso,
"locale" => TimeStyle::Locale,
// below should never happen as clap already restricts the values.
_ => unreachable!("Invalid field for --time-style"),
}
}
} else if options.is_present(options::FULL_TIME) {
TimeStyle::FullIso
} else {
TimeStyle::Locale
};
let mut ignore_patterns = GlobSetBuilder::new(); let mut ignore_patterns = GlobSetBuilder::new();
if options.is_present(options::IGNORE_BACKUPS) { if options.is_present(options::IGNORE_BACKUPS) {
ignore_patterns.add(Glob::new("*~").unwrap()); ignore_patterns.add(Glob::new("*~").unwrap());
@ -504,6 +547,7 @@ impl Config {
width, width,
quoting_style, quoting_style,
indicator_style, indicator_style,
time_style,
} }
} }
} }
@ -696,10 +740,11 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
.long(options::TIME) .long(options::TIME)
.help("Show time in <field>:\n\ .help("Show time in <field>:\n\
\taccess time (-u): atime, access, use;\n\ \taccess time (-u): atime, access, use;\n\
\tchange time (-t): ctime, status.") \tchange time (-t): ctime, status.\n\
\tbirth time: birth, creation;")
.value_name("field") .value_name("field")
.takes_value(true) .takes_value(true)
.possible_values(&["atime", "access", "use", "ctime", "status"]) .possible_values(&["atime", "access", "use", "ctime", "status", "birth", "creation"])
.hide_possible_values(true) .hide_possible_values(true)
.require_equals(true) .require_equals(true)
.overrides_with_all(&[ .overrides_with_all(&[
@ -1020,9 +1065,34 @@ pub fn uumain(args: impl uucore::Args) -> i32 {
options::indicator_style::CLASSIFY, options::indicator_style::CLASSIFY,
options::INDICATOR_STYLE, options::INDICATOR_STYLE,
])) ]))
.arg(
//This still needs support for posix-*, +FORMAT
Arg::with_name(options::TIME_STYLE)
.long(options::TIME_STYLE)
.help("time/date format with -l; see TIME_STYLE below")
.value_name("TIME_STYLE")
.env("TIME_STYLE")
.possible_values(&[
"full-iso",
"long-iso",
"iso",
"locale",
])
.overrides_with_all(&[
options::TIME_STYLE
])
)
.arg(
Arg::with_name(options::FULL_TIME)
.long(options::FULL_TIME)
.overrides_with(options::FULL_TIME)
.help("like -l --time-style=full-iso")
)
// Positional arguments // Positional arguments
.arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true)); .arg(Arg::with_name(options::PATHS).multiple(true).takes_value(true))
.after_help(AFTER_HELP);
let matches = app.get_matches_from(args); let matches = app.get_matches_from(args);
@ -1480,6 +1550,7 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)), Time::Change => Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)),
Time::Modification => md.modified().ok(), Time::Modification => md.modified().ok(),
Time::Access => md.accessed().ok(), Time::Access => md.accessed().ok(),
Time::Birth => md.created().ok(),
} }
} }
@ -1492,18 +1563,35 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
} }
} }
fn get_time(md: &Metadata, config: &Config) -> Option<time::Tm> { fn get_time(md: &Metadata, config: &Config) -> Option<chrono::DateTime<chrono::Local>> {
let duration = get_system_time(md, config)? let time = get_system_time(md, config)?;
.duration_since(UNIX_EPOCH) Some(time.into())
.ok()?;
let secs = duration.as_secs() as i64;
let nsec = duration.subsec_nanos() as i32;
Some(time::at(Timespec::new(secs, nsec)))
} }
fn display_date(metadata: &Metadata, config: &Config) -> String { fn display_date(metadata: &Metadata, config: &Config) -> String {
match get_time(metadata, config) { match get_time(metadata, config) {
Some(time) => strftime("%F %R", &time).unwrap(), Some(time) => {
//Date is recent if from past 6 months
//According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
let recent = time + chrono::Duration::seconds(31556952 / 2) > chrono::Local::now();
match config.time_style {
TimeStyle::FullIso => time.format("%Y-%m-%d %H:%M:%S.%f %z"),
TimeStyle::LongIso => time.format("%Y-%m-%d %H:%M"),
TimeStyle::Iso => time.format(if recent { "%m-%d %H:%M" } else { "%Y-%m-%d " }),
TimeStyle::Locale => {
let fmt = if recent { "%b %e %H:%M" } else { "%b %e %Y" };
//In this version of chrono translating can be done
//The function is chrono::datetime::DateTime::format_localized
//However it's currently still hard to get the current pure-rust-locale
//So it's not yet implemented
time.format(fmt)
}
}
.to_string()
}
None => "???".into(), None => "???".into(),
} }
} }

View file

@ -24,7 +24,7 @@ thiserror = { version="1.0", optional=true }
lazy_static = { version="1.3", optional=true } lazy_static = { version="1.3", optional=true }
nix = { version="<= 0.13", optional=true } nix = { version="<= 0.13", optional=true }
platform-info = { version="<= 0.1", optional=true } platform-info = { version="<= 0.1", optional=true }
time = { version="<= 0.1.42", optional=true } time = { version="<= 0.1.43", optional=true }
# * "problem" dependencies (pinned) # * "problem" dependencies (pinned)
data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0 data-encoding = { version="~2.1", optional=true } ## data-encoding: require v2.1; but v2.2.0 breaks the build for MinSRV v1.31.0
libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0 libc = { version="0.2.15, <= 0.2.85", optional=true } ## libc: initial utmp support added in v0.2.15; but v0.2.68 breaks the build for MinSRV v1.31.0

View file

@ -558,6 +558,118 @@ fn test_ls_long_ctime() {
} }
} }
#[test]
#[cfg(not(windows))]
// This test is currently failing on windows
fn test_ls_order_birthtime() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
/*
Here we make 2 files with a timeout in between.
After creating the first file try to sync it.
This ensures the file gets created immediately instead of being saved
inside the OS's IO operation buffer.
Without this, both files might accidentally be created at the same time,
even though we placed a timeout between creating the two.
https://github.com/uutils/coreutils/pull/1986/#issuecomment-828490651
*/
at.make_file("test-birthtime-1").sync_all().unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
at.make_file("test-birthtime-2");
at.touch("test-birthtime-1");
let result = scene.ucmd().arg("--time=birth").arg("-t").run();
#[cfg(not(windows))]
assert_eq!(result.stdout_str(), "test-birthtime-2\ntest-birthtime-1\n");
#[cfg(windows)]
assert_eq!(result.stdout_str(), "test-birthtime-2 test-birthtime-1\n");
}
#[test]
fn test_ls_styles() {
let scene = TestScenario::new(util_name!());
let at = &scene.fixtures;
at.touch("test");
let re_full = 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 =
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 = Regex::new(r"[a-z-]* \d* \w* \w* \d* \d{2}-\d{2} \d{2}:\d{2} test\n").unwrap();
let re_locale =
Regex::new(r"[a-z-]* \d* \w* \w* \d* [A-Z][a-z]{2} \d{2} \d{2}:\d{2} test\n").unwrap();
//full-iso
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=full-iso")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
//long-iso
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=long-iso")
.succeeds();
assert!(re_long.is_match(&result.stdout_str()));
//iso
let result = scene.ucmd().arg("-l").arg("--time-style=iso").succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
//locale
let result = scene.ucmd().arg("-l").arg("--time-style=locale").succeeds();
assert!(re_locale.is_match(&result.stdout_str()));
//Overwrite options tests
let result = scene
.ucmd()
.arg("-l")
.arg("--time-style=long-iso")
.arg("--time-style=iso")
.succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--time-style=iso")
.arg("--full-time")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("--time-style=iso")
.succeeds();
assert!(re_iso.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("--time-style=iso")
.arg("--full-time")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
let result = scene
.ucmd()
.arg("--full-time")
.arg("-x")
.arg("-l")
.succeeds();
assert!(re_full.is_match(&result.stdout_str()));
at.touch("test2");
let result = scene.ucmd().arg("--full-time").arg("-x").succeeds();
#[cfg(not(windows))]
assert_eq!(result.stdout_str(), "test\ntest2\n");
#[cfg(windows)]
assert_eq!(result.stdout_str(), "test test2\n");
}
#[test] #[test]
fn test_ls_order_time() { fn test_ls_order_time() {
let scene = TestScenario::new(util_name!()); let scene = TestScenario::new(util_name!());