Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

touch: add support for --ref and --date together #4717

Merged
merged 2 commits into from
Apr 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 74 additions & 45 deletions src/uu/touch/src/touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,57 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
),
)
})?;
let (mut atime, mut mtime) =
if let Some(reference) = matches.get_one::<OsString>(options::sources::REFERENCE) {
stat(Path::new(reference), !matches.get_flag(options::NO_DEREF))?
} else {
let timestamp = if let Some(date) = matches.get_one::<String>(options::sources::DATE) {
parse_date(date)?
} else if let Some(current) = matches.get_one::<String>(options::sources::CURRENT) {
parse_timestamp(current)?
let (mut atime, mut mtime) = match (
matches.get_one::<OsString>(options::sources::REFERENCE),
matches.get_one::<String>(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;
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::<String>(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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)> {
Expand Down Expand Up @@ -384,33 +428,22 @@ fn parse_date(s: &str) -> UResult<FileTime> {
}
}

// 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<Duration> {
// 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::<i64>()
.ok()
Expand All @@ -430,15 +463,11 @@ fn parse_date(s: &str) -> UResult<FileTime> {
num_str.parse::<i64>().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<FileTime> {
Expand Down
29 changes: 27 additions & 2 deletions tests/by-util/test_touch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down