diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 02737dca28e..e02e8fa9afd 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -91,6 +91,7 @@ enum DateSource { Now, Custom(String), File(PathBuf), + Reference(PathBuf), Stdin, Human(TimeDelta), } @@ -178,11 +179,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { "-" => DateSource::Stdin, _ => DateSource::File(file.into()), } + } else if let Some(file) = matches.get_one::(OPT_REFERENCE) { + DateSource::Reference(file.into()) } else { DateSource::Now }; - let set_to = match matches.get_one::(OPT_SET).map(parse_date) { + let set_option = matches.get_one::(OPT_SET); + + // Before parsing an eventual OPT_SET, check if it can't be present. + if !matches!(date_source, DateSource::Now) && set_option.is_some() { + return Err(USimpleError::new( + 1, + "the options to print and set the time may not be used together", + )); + } + + let set_to = match set_option.map(parse_date) { None => None, Some(Err((input, _err))) => { return Err(USimpleError::new( @@ -260,6 +273,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let iter = lines.map_while(Result::ok).map(parse_date); Box::new(iter) } + DateSource::Reference(ref path) => { + let modified_time: std::time::SystemTime = std::fs::metadata(path)?.modified()?; + let chrono_time: DateTime = modified_time.into(); + Box::new(std::iter::once(Ok(chrono_time.into()))) + } DateSource::Now => { let iter = std::iter::once(Ok(now)); Box::new(iter) @@ -318,7 +336,10 @@ pub fn uu_app() -> Command { .short('d') .long(OPT_DATE) .value_name("STRING") - .help("display time described by STRING, not 'now'"), + .action(ArgAction::Set) + .overrides_with(OPT_DATE) + .help("display time described by STRING, not 'now'") + .conflicts_with_all([OPT_FILE, OPT_REFERENCE]), ) .arg( Arg::new(OPT_FILE) @@ -326,7 +347,10 @@ pub fn uu_app() -> Command { .long(OPT_FILE) .value_name("DATEFILE") .value_hint(clap::ValueHint::FilePath) - .help("like --date; once for each line of DATEFILE"), + .action(ArgAction::Set) + .overrides_with(OPT_FILE) // several -f can be passed, but only the last is kept. + .help("like --date; once for each line of DATEFILE") + .conflicts_with_all([OPT_REFERENCE]), ) .arg( Arg::new(OPT_ISO_8601) @@ -366,6 +390,8 @@ pub fn uu_app() -> Command { .long(OPT_REFERENCE) .value_name("FILE") .value_hint(clap::ValueHint::AnyPath) + .action(ArgAction::Set) + .overrides_with(OPT_REFERENCE) .help("display the last modification time of FILE"), ) .arg( diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 553414af853..ce0ff25152f 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -1,3 +1,5 @@ +use std::time::SystemTime; + // This file is part of the uutils coreutils package. // // For the full copyright and license information, please view the LICENSE @@ -478,3 +480,78 @@ fn test_date_from_stdin() { Sat Apr 15 18:30:00 2023\n", ); } + +#[test] +fn test_date_exclusive_date_sources() { + new_ucmd!() + .arg("-f") + .arg("foo.txt") + .arg("-d") + .arg("1111-11-11") + .fails() + .stderr_matches(&Regex::new("error: the argument .* cannot be used with .*").unwrap()); + + new_ucmd!() + .arg("-r") + .arg("foo.txt") + .arg("-d") + .arg("1111-11-11") + .fails() + .stderr_matches(&Regex::new("error: the argument .* cannot be used with .*").unwrap()); + + new_ucmd!() + .arg("-r") + .arg("foo.txt") + .arg("-f") + .arg("bar.txt") + .fails() + .stderr_matches(&Regex::new("error: the argument .* cannot be used with .*").unwrap()); +} + +#[test] +fn test_date_successive_file() { + const FILE: &str = "file-with-dates"; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write( + FILE, + "2023-03-27 08:30:00\n\ + 2023-04-01 12:00:00\n\ + 2023-04-15 18:30:00", + ); + ucmd.arg("-f") + .arg("not-existent-file") + .arg("-f") + .arg("file-with-dates") + .succeeds(); +} + +#[test] +fn test_date_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + + at.touch_and_set_modified("ref-file", SystemTime::UNIX_EPOCH); + + ucmd.arg("-r") + .arg("ref-file") + .succeeds() + .stdout_is("Thu Jan 1 00:00:00 1970\n"); +} + +#[test] +fn test_date_successive_reference() { + const FILE: &str = "file-with-dates"; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write( + FILE, + "2023-03-27 08:30:00\n\ + 2023-04-01 12:00:00\n\ + 2023-04-15 18:30:00", + ); + ucmd.arg("-r") + .arg("not-existent-file") + .arg("-r") + .arg("file-with-dates") + .succeeds(); +} diff --git a/tests/common/util.rs b/tests/common/util.rs index 2d1fd91d17a..10d32fd2c83 100644 --- a/tests/common/util.rs +++ b/tests/common/util.rs @@ -46,7 +46,7 @@ use std::process::{Child, Command, ExitStatus, Output, Stdio}; use std::rc::Rc; use std::sync::mpsc::{self, RecvTimeoutError}; use std::thread::{sleep, JoinHandle}; -use std::time::{Duration, Instant}; +use std::time::{Duration, Instant, SystemTime}; use std::{env, hint, mem, thread}; use tempfile::{Builder, TempDir}; @@ -974,6 +974,13 @@ impl AtPath { File::create(self.plus(file)).unwrap(); } + pub fn touch_and_set_modified>(&self, file: P, modified: SystemTime) { + let file = file.as_ref(); + log_info("touch", self.plus_as_string(file)); + let f = File::create(self.plus(file)).unwrap(); + f.set_modified(modified).unwrap(); + } + #[cfg(not(windows))] pub fn mkfifo(&self, fifo: &str) { let full_path = self.plus_as_string(fifo);