From 19e456fe2ab22fefeeb20347c540e54500fe2bee Mon Sep 17 00:00:00 2001 From: xalfer Date: Fri, 7 Apr 2023 21:36:26 +0200 Subject: [PATCH] touch: add support for --ref and --date together This commit resolves issue #4608. To make it relatively straightforward to implement I have taken some code from parse_date and put it inside a function parse_relative_time. This commit changes and adds some test as well to work with the new functionality. --- src/uu/touch/src/touch.rs | 119 ++++++++++++++++++++++-------------- tests/by-util/test_touch.rs | 29 ++++++++- 2 files changed, 101 insertions(+), 47 deletions(-) diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index ec88586c6..230f55625 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -77,19 +77,57 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ), ) })?; - let (mut atime, mut mtime) = - if let Some(reference) = matches.get_one::(options::sources::REFERENCE) { - stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))? - } else { - let timestamp = if let Some(date) = matches.get_one::(options::sources::DATE) { - parse_date(date)? - } else if let Some(current) = matches.get_one::(options::sources::CURRENT) { - parse_timestamp(current)? + let (mut atime, mut mtime) = match ( + matches.get_one::(options::sources::REFERENCE), + matches.get_one::(options::sources::DATE), + ) { + (Some(reference), Some(date)) => { + let (atime, mtime) = stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?; + if let Some(offset) = parse_relative_time(date) { + let mut seconds = offset.whole_seconds(); + let mut nanos = offset.subsec_nanoseconds(); + if nanos < 0 { + nanos = 1_000_000_000 + nanos; + seconds -= 1; + } + + let ref_atime_secs = atime.unix_seconds(); + let ref_atime_nanos = atime.nanoseconds(); + let atime = FileTime::from_unix_time( + ref_atime_secs + seconds, + ref_atime_nanos + nanos as u32, + ); + + let ref_mtime_secs = mtime.unix_seconds(); + let ref_mtime_nanos = mtime.nanoseconds(); + let mtime = FileTime::from_unix_time( + ref_mtime_secs + seconds, + ref_mtime_nanos + nanos as u32, + ); + + (atime, mtime) } else { - local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) - }; + let timestamp = parse_date(date)?; + (timestamp, timestamp) + } + } + (Some(reference), None) => { + stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))? + } + (None, Some(date)) => { + let timestamp = parse_date(date)?; (timestamp, timestamp) - }; + } + (None, None) => { + let timestamp = + if let Some(current) = matches.get_one::(options::sources::CURRENT) { + parse_timestamp(current)? + } else { + local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) + }; + (timestamp, timestamp) + } + }; for filename in files { // FIXME: find a way to avoid having to clone the path @@ -202,7 +240,8 @@ pub fn uu_app() -> Command { .long(options::sources::DATE) .allow_hyphen_values(true) .help("parse argument and use it instead of current time") - .value_name("STRING"), + .value_name("STRING") + .conflicts_with(options::sources::CURRENT), ) .arg( Arg::new(options::MODIFICATION) @@ -234,7 +273,8 @@ pub fn uu_app() -> Command { .help("use this file's times instead of the current time") .value_name("FILE") .value_parser(ValueParser::os_string()) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .conflicts_with(options::sources::CURRENT), ) .arg( Arg::new(options::TIME) @@ -254,11 +294,15 @@ pub fn uu_app() -> Command { .value_parser(ValueParser::os_string()) .value_hint(clap::ValueHint::AnyPath), ) - .group(ArgGroup::new(options::SOURCES).args([ - options::sources::CURRENT, - options::sources::DATE, - options::sources::REFERENCE, - ])) + .group( + ArgGroup::new(options::SOURCES) + .args([ + options::sources::CURRENT, + options::sources::DATE, + options::sources::REFERENCE, + ]) + .multiple(true), + ) } fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { @@ -384,33 +428,22 @@ fn parse_date(s: &str) -> UResult { } } - // Relative day, like "today", "tomorrow", or "yesterday". - match s { - "now" | "today" => { - let now_local = time::OffsetDateTime::now_local().unwrap(); - return Ok(local_dt_to_filetime(now_local)); - } - "tomorrow" => { - let duration = time::Duration::days(1); - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local.checked_add(duration).unwrap(); - return Ok(local_dt_to_filetime(diff)); - } - "yesterday" => { - let duration = time::Duration::days(1); - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local.checked_sub(duration).unwrap(); - return Ok(local_dt_to_filetime(diff)); - } - _ => {} + if let Some(duration) = parse_relative_time(s) { + let now_local = time::OffsetDateTime::now_local().unwrap(); + let diff = now_local.checked_add(duration).unwrap(); + return Ok(local_dt_to_filetime(diff)); } + Err(USimpleError::new(1, format!("Unable to parse date: {s}"))) +} + +fn parse_relative_time(s: &str) -> Option { // Relative time, like "-1 hour" or "+3 days". // // TODO Add support for "year" and "month". // TODO Add support for times without spaces like "-1hour". let tokens: Vec<&str> = s.split_whitespace().collect(); - let maybe_duration = match &tokens[..] { + match &tokens[..] { [num_str, "fortnight" | "fortnights"] => num_str .parse::() .ok() @@ -430,15 +463,11 @@ fn parse_date(s: &str) -> UResult { num_str.parse::().ok().map(time::Duration::seconds) } ["second" | "seconds" | "sec" | "secs"] => Some(time::Duration::seconds(1)), + ["now" | "today"] => Some(time::Duration::ZERO), + ["yesterday"] => Some(time::Duration::days(-1)), + ["tomorrow"] => Some(time::Duration::days(1)), _ => None, - }; - if let Some(duration) = maybe_duration { - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local.checked_add(duration).unwrap(); - return Ok(local_dt_to_filetime(diff)); } - - Err(USimpleError::new(1, format!("Unable to parse date: {s}"))) } fn parse_timestamp(s: &str) -> UResult { diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 80fb26710..b21f95645 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -251,14 +251,39 @@ fn test_touch_set_both_date_and_reference() { let ref_file = "test_touch_reference"; let file = "test_touch_set_both_date_and_reference"; - let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501010000"); + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501011234"); at.touch(ref_file); set_file_times(&at, ref_file, start_of_year, start_of_year); assert!(at.file_exists(ref_file)); ucmd.args(&["-d", "Thu Jan 01 12:34:00 2015", "-r", ref_file, file]) - .fails(); + .succeeds() + .no_stderr(); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, start_of_year); + assert_eq!(mtime, start_of_year); +} + +#[test] +fn test_touch_set_both_offset_date_and_reference() { + let (at, mut ucmd) = at_and_ucmd!(); + let ref_file = "test_touch_reference"; + let file = "test_touch_set_both_date_and_reference"; + + let start_of_year = str_to_filetime("%Y%m%d%H%M", "201501011234"); + let five_days_later = str_to_filetime("%Y%m%d%H%M", "201501061234"); + + at.touch(ref_file); + set_file_times(&at, ref_file, start_of_year, start_of_year); + assert!(at.file_exists(ref_file)); + + ucmd.args(&["-d", "+5 days", "-r", ref_file, file]) + .succeeds() + .no_stderr(); + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, five_days_later); + assert_eq!(mtime, five_days_later); } #[test]