From f63f8cb15406c9f3f94e271f71767444763aa9a8 Mon Sep 17 00:00:00 2001 From: Yash Thakur <45539777+ysthakur@users.noreply.github.com> Date: Sun, 17 Nov 2024 19:03:21 -0500 Subject: [PATCH] Add utouch command from uutils/coreutils (#11817) Part of https://github.com/nushell/nushell/issues/11549 # Description This PR adds a `utouch` command that uses the `touch` command from https://github.com/uutils/coreutils. Eventually, `utouch` may be able to replace `touch`. The conflicts in Cargo.lock and Cargo.toml are because I'm using the uutils/coreutils main rather than the latest release, since the changes that expose `uu_touch`'s internal functionality aren't available in the latest release. # User-Facing Changes Users will have access to a new `utouch` command with the following flags: todo # Tests + Formatting # After Submitting --- Cargo.lock | 26 + Cargo.toml | 1 + crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/filesystem/mod.rs | 2 + crates/nu-command/src/filesystem/utouch.rs | 268 ++++++++ crates/nu-command/tests/commands/mod.rs | 1 + crates/nu-command/tests/commands/utouch.rs | 740 +++++++++++++++++++++ 8 files changed, 1040 insertions(+) create mode 100644 crates/nu-command/src/filesystem/utouch.rs create mode 100644 crates/nu-command/tests/commands/utouch.rs diff --git a/Cargo.lock b/Cargo.lock index 2754f5bd23..884f1993d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3229,6 +3229,7 @@ dependencies = [ "uu_mkdir", "uu_mktemp", "uu_mv", + "uu_touch", "uu_uname", "uu_whoami", "uucore", @@ -4081,6 +4082,17 @@ dependencies = [ "regex", ] +[[package]] +name = "parse_datetime" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8720474e3dd4af20cea8716703498b9f3b690f318fa9d9d9e2e38eaf44b96d0" +dependencies = [ + "chrono", + "nom", + "regex", +] + [[package]] name = "paste" version = "1.0.15" @@ -6745,6 +6757,20 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_touch" +version = "0.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55476bec11d5b70c578233a2e94f685058e0d65fc5d66c7ed465877c15124c7c" +dependencies = [ + "chrono", + "clap", + "filetime", + "parse_datetime", + "uucore", + "windows-sys 0.59.0", +] + [[package]] name = "uu_uname" version = "0.0.27" diff --git a/Cargo.toml b/Cargo.toml index a976aeec17..0297f5fdb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -169,6 +169,7 @@ uu_cp = "0.0.27" uu_mkdir = "0.0.27" uu_mktemp = "0.0.27" uu_mv = "0.0.27" +uu_touch = "0.0.28" uu_whoami = "0.0.27" uu_uname = "0.0.27" uucore = "0.0.27" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index e9c3b9a784..c37954e9c1 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -96,6 +96,7 @@ uu_cp = { workspace = true } uu_mkdir = { workspace = true } uu_mktemp = { workspace = true } uu_mv = { workspace = true } +uu_touch = { workspace = true } uu_uname = { workspace = true } uu_whoami = { workspace = true } uuid = { workspace = true, features = ["v4"] } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ac6fb46631..cbbee717d4 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -230,6 +230,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Rm, Save, Touch, + UTouch, Glob, Watch, }; diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index acfa54fee3..089e899dda 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -12,6 +12,7 @@ mod ucp; mod umkdir; mod umv; mod util; +mod utouch; mod watch; pub use self::open::Open; @@ -27,4 +28,5 @@ pub use touch::Touch; pub use ucp::UCp; pub use umkdir::UMkdir; pub use umv::UMv; +pub use utouch::UTouch; pub use watch::Watch; diff --git a/crates/nu-command/src/filesystem/utouch.rs b/crates/nu-command/src/filesystem/utouch.rs new file mode 100644 index 0000000000..f32364b28a --- /dev/null +++ b/crates/nu-command/src/filesystem/utouch.rs @@ -0,0 +1,268 @@ +use std::io::ErrorKind; +use std::path::PathBuf; + +use chrono::{DateTime, FixedOffset}; +use filetime::FileTime; + +use nu_engine::CallExt; +use nu_path::expand_path_with; +use nu_protocol::engine::{Call, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, NuGlob, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type, +}; +use uu_touch::error::TouchError; +use uu_touch::{ChangeTimes, InputFile, Options, Source}; + +use super::util::get_rest_for_glob_pattern; + +#[derive(Clone)] +pub struct UTouch; + +impl Command for UTouch { + fn name(&self) -> &str { + "utouch" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["create", "file"] + } + + fn signature(&self) -> Signature { + Signature::build("utouch") + .input_output_types(vec![ (Type::Nothing, Type::Nothing) ]) + .rest( + "files", + SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Filepath]), + "The file(s) to create. '-' is used to represent stdout." + ) + .named( + "reference", + SyntaxShape::Filepath, + "Use the access and modification times of the reference file/directory instead of the current time", + Some('r'), + ) + .named( + "timestamp", + SyntaxShape::DateTime, + "Use the given timestamp instead of the current time", + Some('t') + ) + .named( + "date", + SyntaxShape::String, + "Use the given time instead of the current time. This can be a full timestamp or it can be relative to either the current time or reference file time (if given). For more information, see https://www.gnu.org/software/coreutils/manual/html_node/touch-invocation.html", + Some('d') + ) + .switch( + "modified", + "Change only the modification time (if used with -a, access time is changed too)", + Some('m'), + ) + .switch( + "access", + "Change only the access time (if used with -m, modification time is changed too)", + Some('a'), + ) + .switch( + "no-create", + "Don't create the file if it doesn't exist", + Some('c'), + ) + .switch( + "no-deref", + "Affect each symbolic link instead of any referenced file (only for systems that can change the timestamps of a symlink). Ignored if touching stdout", + Some('s'), + ) + .category(Category::FileSystem) + } + + fn description(&self) -> &str { + "Creates one or more files." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let change_mtime: bool = call.has_flag(engine_state, stack, "modified")?; + let change_atime: bool = call.has_flag(engine_state, stack, "access")?; + let no_create: bool = call.has_flag(engine_state, stack, "no-create")?; + let no_deref: bool = call.has_flag(engine_state, stack, "no-dereference")?; + let file_globs: Vec> = + get_rest_for_glob_pattern(engine_state, stack, call, 0)?; + let cwd = engine_state.cwd(Some(stack))?; + + if file_globs.is_empty() { + return Err(ShellError::MissingParameter { + param_name: "requires file paths".to_string(), + span: call.head, + }); + } + + let (reference_file, reference_span) = if let Some(reference) = + call.get_flag::>(engine_state, stack, "reference")? + { + (Some(reference.item), Some(reference.span)) + } else { + (None, None) + }; + let (date_str, date_span) = + if let Some(date) = call.get_flag::>(engine_state, stack, "date")? { + (Some(date.item), Some(date.span)) + } else { + (None, None) + }; + let timestamp: Option>> = + call.get_flag(engine_state, stack, "timestamp")?; + + let source = if let Some(timestamp) = timestamp { + if let Some(reference_span) = reference_span { + return Err(ShellError::IncompatibleParameters { + left_message: "timestamp given".to_string(), + left_span: timestamp.span, + right_message: "reference given".to_string(), + right_span: reference_span, + }); + } + if let Some(date_span) = date_span { + return Err(ShellError::IncompatibleParameters { + left_message: "timestamp given".to_string(), + left_span: timestamp.span, + right_message: "date given".to_string(), + right_span: date_span, + }); + } + Source::Timestamp(FileTime::from_unix_time( + timestamp.item.timestamp(), + timestamp.item.timestamp_subsec_nanos(), + )) + } else if let Some(reference_file) = reference_file { + let reference_file = expand_path_with(reference_file, &cwd, true); + Source::Reference(reference_file) + } else { + Source::Now + }; + + let change_times = if change_atime && !change_mtime { + ChangeTimes::AtimeOnly + } else if change_mtime && !change_atime { + ChangeTimes::MtimeOnly + } else { + ChangeTimes::Both + }; + + let mut input_files = Vec::new(); + for file_glob in &file_globs { + if file_glob.item.as_ref() == "-" { + input_files.push(InputFile::Stdout); + } else { + let path = + expand_path_with(file_glob.item.as_ref(), &cwd, file_glob.item.is_expand()); + input_files.push(InputFile::Path(path)); + } + } + + if let Err(err) = uu_touch::touch( + &input_files, + &Options { + no_create, + no_deref, + source, + date: date_str, + change_times, + strict: true, + }, + ) { + let nu_err = match err { + TouchError::TouchFileError { path, index, error } => ShellError::GenericError { + error: format!("Could not touch {}", path.display()), + msg: error.to_string(), + span: Some(file_globs[index].span), + help: None, + inner: Vec::new(), + }, + TouchError::InvalidDateFormat(date) => ShellError::IncorrectValue { + msg: format!("Invalid date: {}", date), + val_span: date_span.expect("utouch should've been given a date"), + call_span: call.head, + }, + TouchError::ReferenceFileInaccessible(reference_path, io_err) => { + let span = + reference_span.expect("utouch should've been given a reference file"); + if io_err.kind() == ErrorKind::NotFound { + ShellError::FileNotFound { + span, + file: reference_path.display().to_string(), + } + } else { + ShellError::GenericError { + error: io_err.to_string(), + msg: format!("Failed to read metadata of {}", reference_path.display()), + span: Some(span), + help: None, + inner: Vec::new(), + } + } + } + _ => ShellError::GenericError { + error: err.to_string(), + msg: err.to_string(), + span: Some(call.head), + help: None, + inner: Vec::new(), + }, + }; + return Err(nu_err); + } + + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Creates \"fixture.json\"", + example: "utouch fixture.json", + result: None, + }, + Example { + description: "Creates files a, b and c", + example: "utouch a b c", + result: None, + }, + Example { + description: r#"Changes the last modified time of "fixture.json" to today's date"#, + example: "utouch -m fixture.json", + result: None, + }, + Example { + description: "Changes the last accessed and modified times of files a, b and c to the current time but yesterday", + example: r#"utouch -d "yesterday" a b c"#, + result: None, + }, + Example { + description: r#"Changes the last modified time of files d and e to "fixture.json"'s last modified time"#, + example: r#"utouch -m -r fixture.json d e"#, + result: None, + }, + Example { + description: r#"Changes the last accessed time of "fixture.json" to a datetime"#, + example: r#"utouch -a -t 2019-08-24T12:30:30 fixture.json"#, + result: None, + }, + Example { + description: r#"Change the last accessed and modified times of stdout"#, + example: r#"utouch -"#, + result: None, + }, + Example { + description: r#"Changes the last accessed and modified times of file a to 1 month before "fixture.json"'s last modified time"#, + example: r#"utouch -r fixture.json -d "-1 month" a"#, + result: None, + }, + ] + } +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 63911ebfbc..678b8d8896 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -127,6 +127,7 @@ mod update; mod upsert; mod url; mod use_; +mod utouch; mod where_; mod which; mod while_; diff --git a/crates/nu-command/tests/commands/utouch.rs b/crates/nu-command/tests/commands/utouch.rs new file mode 100644 index 0000000000..062ec7ddfc --- /dev/null +++ b/crates/nu-command/tests/commands/utouch.rs @@ -0,0 +1,740 @@ +use chrono::{DateTime, Days, Local, TimeDelta, Utc}; +use filetime::FileTime; +use nu_test_support::fs::{files_exist_at, Stub}; +use nu_test_support::nu; +use nu_test_support::playground::{Dirs, Playground}; +use std::path::Path; + +// Use 1 instead of 0 because 0 has a special meaning in Windows +const TIME_ONE: FileTime = FileTime::from_unix_time(1, 0); + +fn file_times(file: impl AsRef) -> (FileTime, FileTime) { + ( + file.as_ref().metadata().unwrap().accessed().unwrap().into(), + file.as_ref().metadata().unwrap().modified().unwrap().into(), + ) +} + +fn symlink_times(path: &nu_path::AbsolutePath) -> (filetime::FileTime, filetime::FileTime) { + let metadata = path.symlink_metadata().unwrap(); + + ( + filetime::FileTime::from_system_time(metadata.accessed().unwrap()), + filetime::FileTime::from_system_time(metadata.modified().unwrap()), + ) +} + +// From https://github.com/nushell/nushell/pull/14214 +fn setup_symlink_fs(dirs: &Dirs, sandbox: &mut Playground<'_>) { + sandbox.mkdir("d"); + sandbox.with_files(&[Stub::EmptyFile("f"), Stub::EmptyFile("d/f")]); + sandbox.symlink("f", "fs"); + sandbox.symlink("d", "ds"); + sandbox.symlink("d/f", "fds"); + + // sandbox.symlink does not handle symlinks to missing files well. It panics + // But they are useful, and they should be tested. + #[cfg(unix)] + { + std::os::unix::fs::symlink(dirs.test().join("m"), dirs.test().join("fms")).unwrap(); + } + + #[cfg(windows)] + { + std::os::windows::fs::symlink_file(dirs.test().join("m"), dirs.test().join("fms")).unwrap(); + } + + // Change the file times to a known "old" value for comparison + filetime::set_symlink_file_times(dirs.test().join("f"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("d"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("d/f"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("ds"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("fs"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("fds"), TIME_ONE, TIME_ONE).unwrap(); + filetime::set_symlink_file_times(dirs.test().join("fms"), TIME_ONE, TIME_ONE).unwrap(); +} + +#[test] +fn creates_a_file_when_it_doesnt_exist() { + Playground::setup("create_test_1", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch i_will_be_created.txt" + ); + + let path = dirs.test().join("i_will_be_created.txt"); + assert!(path.exists()); + }) +} + +#[test] +fn creates_two_files() { + Playground::setup("create_test_2", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch a b" + ); + + let path = dirs.test().join("a"); + assert!(path.exists()); + + let path2 = dirs.test().join("b"); + assert!(path2.exists()); + }) +} + +#[test] +fn change_modified_time_of_file_to_today() { + Playground::setup("change_time_test_9", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("file.txt")]); + let path = dirs.test().join("file.txt"); + + // Set file.txt's times to the past before the test to make sure `utouch` actually changes the mtime to today + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -m file.txt" + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = DateTime::::from(metadata.modified().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + + // Check that atime remains unchanged + assert_eq!( + TIME_ONE, + FileTime::from_system_time(metadata.accessed().unwrap()) + ); + }) +} + +#[test] +fn change_access_time_of_file_to_today() { + Playground::setup("change_time_test_18", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("file.txt")]); + let path = dirs.test().join("file.txt"); + + // Set file.txt's times to the past before the test to make sure `utouch` actually changes the atime to today + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -a file.txt" + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let atime_day = DateTime::::from(metadata.accessed().unwrap()).date_naive(); + + assert_eq!(today, atime_day); + + // Check that mtime remains unchanged + assert_eq!( + TIME_ONE, + FileTime::from_system_time(metadata.modified().unwrap()) + ); + }) +} + +#[test] +fn change_modified_and_access_time_of_file_to_today() { + Playground::setup("change_time_test_27", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("file.txt")]); + let path = dirs.test().join("file.txt"); + + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -a -m file.txt" + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = DateTime::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::from(metadata.accessed().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + assert_eq!(today, atime_day); + }) +} + +#[test] +fn not_create_file_if_it_not_exists() { + Playground::setup("change_time_test_28", |dirs, _sandbox| { + let outcome = nu!( + cwd: dirs.test(), + "utouch -c file.txt" + ); + + let path = dirs.test().join("file.txt"); + + assert!(!path.exists()); + + // If --no-create is improperly handled `utouch` may error when trying to change the times of a nonexistent file + assert!(outcome.status.success()) + }) +} + +#[test] +fn change_file_times_if_exists_with_no_create() { + Playground::setup( + "change_file_times_if_exists_with_no_create", + |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("file.txt")]); + let path = dirs.test().join("file.txt"); + + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -c file.txt" + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = DateTime::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::from(metadata.accessed().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + assert_eq!(today, atime_day); + }, + ) +} + +#[test] +fn creates_file_three_dots() { + Playground::setup("create_test_1", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch file..." + ); + + let path = dirs.test().join("file..."); + assert!(path.exists()); + }) +} + +#[test] +fn creates_file_four_dots() { + Playground::setup("create_test_1", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch file...." + ); + + let path = dirs.test().join("file...."); + assert!(path.exists()); + }) +} + +#[test] +fn creates_file_four_dots_quotation_marks() { + Playground::setup("create_test_1", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch 'file....'" + ); + + let path = dirs.test().join("file...."); + assert!(path.exists()); + }) +} + +#[test] +fn change_file_times_to_reference_file() { + Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| { + sandbox.with_files(&[ + Stub::EmptyFile("reference_file"), + Stub::EmptyFile("target_file"), + ]); + + let reference = dirs.test().join("reference_file"); + let target = dirs.test().join("target_file"); + + // Change the times for reference + filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap(); + + // target should have today's date since it was just created, but reference should be different + assert_ne!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_ne!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + + nu!( + cwd: dirs.test(), + "utouch -r reference_file target_file" + ); + + assert_eq!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_eq!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + }) +} + +#[test] +fn change_file_mtime_to_reference() { + Playground::setup("change_file_mtime_to_reference", |dirs, sandbox| { + sandbox.with_files(&[ + Stub::EmptyFile("reference_file"), + Stub::EmptyFile("target_file"), + ]); + + let reference = dirs.test().join("reference_file"); + let target = dirs.test().join("target_file"); + + // Change the times for reference + filetime::set_file_times(&reference, TIME_ONE, FileTime::from_unix_time(1337, 0)).unwrap(); + + // target should have today's date since it was just created, but reference should be different + assert_ne!(file_times(&reference), file_times(&target)); + + // Save target's current atime to make sure it is preserved + let target_original_atime = target.metadata().unwrap().accessed().unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -mr reference_file target_file" + ); + + assert_eq!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + assert_ne!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_eq!( + target_original_atime, + target.metadata().unwrap().accessed().unwrap() + ); + }) +} + +// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed, +// unignore this test +#[test] +#[ignore] +fn change_file_times_to_reference_file_with_date() { + Playground::setup( + "change_file_times_to_reference_file_with_date", + |dirs, sandbox| { + sandbox.with_files(&[ + Stub::EmptyFile("reference_file"), + Stub::EmptyFile("target_file"), + ]); + + let reference = dirs.test().join("reference_file"); + let target = dirs.test().join("target_file"); + + let now = Utc::now(); + + let ref_atime = now; + let ref_mtime = now.checked_sub_days(Days::new(5)).unwrap(); + + // Change the times for reference + filetime::set_file_times( + reference, + FileTime::from_unix_time(ref_atime.timestamp(), ref_atime.timestamp_subsec_nanos()), + FileTime::from_unix_time(ref_mtime.timestamp(), ref_mtime.timestamp_subsec_nanos()), + ) + .unwrap(); + + nu!( + cwd: dirs.test(), + r#"utouch -r reference_file -d "yesterday" target_file"# + ); + + let (got_atime, got_mtime) = file_times(target); + let got = ( + DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap(), + DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap(), + ); + assert_eq!( + ( + now.checked_sub_days(Days::new(1)).unwrap(), + now.checked_sub_days(Days::new(6)).unwrap() + ), + got + ); + }, + ) +} + +#[test] +fn change_file_times_to_timestamp() { + Playground::setup("change_file_times_to_timestamp", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("target_file")]); + + let target = dirs.test().join("target_file"); + let timestamp = DateTime::from_timestamp(TIME_ONE.unix_seconds(), TIME_ONE.nanoseconds()) + .unwrap() + .to_rfc3339(); + + nu!(cwd: dirs.test(), format!("utouch --timestamp {} target_file", timestamp)); + + assert_eq!((TIME_ONE, TIME_ONE), file_times(target)); + }) +} + +#[test] +fn change_modified_time_of_dir_to_today() { + Playground::setup("change_dir_mtime", |dirs, sandbox| { + sandbox.mkdir("test_dir"); + let path = dirs.test().join("test_dir"); + + filetime::set_file_mtime(&path, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -m test_dir" + ); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = + DateTime::::from(path.metadata().unwrap().modified().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + }) +} + +#[test] +fn change_access_time_of_dir_to_today() { + Playground::setup("change_dir_atime", |dirs, sandbox| { + sandbox.mkdir("test_dir"); + let path = dirs.test().join("test_dir"); + + filetime::set_file_atime(&path, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -a test_dir" + ); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let atime_day = + DateTime::::from(path.metadata().unwrap().accessed().unwrap()).date_naive(); + + assert_eq!(today, atime_day); + }) +} + +#[test] +fn change_modified_and_access_time_of_dir_to_today() { + Playground::setup("change_dir_times", |dirs, sandbox| { + sandbox.mkdir("test_dir"); + let path = dirs.test().join("test_dir"); + + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -a -m test_dir" + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = DateTime::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::from(metadata.accessed().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + assert_eq!(today, atime_day); + }) +} + +// TODO when https://github.com/uutils/coreutils/issues/6629 is fixed, +// unignore this test +#[test] +#[ignore] +fn change_file_times_to_date() { + Playground::setup("change_file_times_to_date", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("target_file")]); + + let expected = Utc::now().checked_sub_signed(TimeDelta::hours(2)).unwrap(); + nu!(cwd: dirs.test(), "utouch -d '-2 hours' target_file"); + + let (got_atime, got_mtime) = file_times(dirs.test().join("target_file")); + let got_atime = + DateTime::from_timestamp(got_atime.seconds(), got_atime.nanoseconds()).unwrap(); + let got_mtime = + DateTime::from_timestamp(got_mtime.seconds(), got_mtime.nanoseconds()).unwrap(); + let threshold = TimeDelta::minutes(1); + assert!( + got_atime.signed_duration_since(expected).lt(&threshold) + && got_mtime.signed_duration_since(expected).lt(&threshold), + "Expected: {}. Got: atime={}, mtime={}", + expected, + got_atime, + got_mtime + ); + assert!(got_mtime.signed_duration_since(expected).lt(&threshold)); + }) +} + +#[test] +fn change_dir_three_dots_times() { + Playground::setup("change_dir_three_dots_times", |dirs, sandbox| { + sandbox.mkdir("test_dir..."); + let path = dirs.test().join("test_dir..."); + + filetime::set_file_times(&path, TIME_ONE, TIME_ONE).unwrap(); + + nu!( + cwd: dirs.test(), + "utouch test_dir..." + ); + + let metadata = path.metadata().unwrap(); + + // Check only the date since the time may not match exactly + let today = Local::now().date_naive(); + let mtime_day = DateTime::::from(metadata.modified().unwrap()).date_naive(); + let atime_day = DateTime::::from(metadata.accessed().unwrap()).date_naive(); + + assert_eq!(today, mtime_day); + assert_eq!(today, atime_day); + }) +} + +#[test] +fn change_dir_times_to_reference_dir() { + Playground::setup("change_dir_times_to_reference_dir", |dirs, sandbox| { + sandbox.mkdir("reference_dir"); + sandbox.mkdir("target_dir"); + + let reference = dirs.test().join("reference_dir"); + let target = dirs.test().join("target_dir"); + + // Change the times for reference + filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap(); + + // target should have today's date since it was just created, but reference should be different + assert_ne!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_ne!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + + nu!( + cwd: dirs.test(), + "utouch -r reference_dir target_dir" + ); + + assert_eq!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_eq!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + }) +} + +#[test] +fn change_dir_atime_to_reference() { + Playground::setup("change_dir_atime_to_reference", |dirs, sandbox| { + sandbox.mkdir("reference_dir"); + sandbox.mkdir("target_dir"); + + let reference = dirs.test().join("reference_dir"); + let target = dirs.test().join("target_dir"); + + // Change the times for reference + filetime::set_file_times(&reference, FileTime::from_unix_time(1337, 0), TIME_ONE).unwrap(); + + // target should have today's date since it was just created, but reference should be different + assert_ne!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_ne!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + + // Save target's current mtime to make sure it is preserved + let target_original_mtime = target.metadata().unwrap().modified().unwrap(); + + nu!( + cwd: dirs.test(), + "utouch -ar reference_dir target_dir" + ); + + assert_eq!( + reference.metadata().unwrap().accessed().unwrap(), + target.metadata().unwrap().accessed().unwrap() + ); + assert_ne!( + reference.metadata().unwrap().modified().unwrap(), + target.metadata().unwrap().modified().unwrap() + ); + assert_eq!( + target_original_mtime, + target.metadata().unwrap().modified().unwrap() + ); + }) +} + +#[test] +fn create_a_file_with_tilde() { + Playground::setup("utouch with tilde", |dirs, _| { + let actual = nu!(cwd: dirs.test(), "utouch '~tilde'"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(&[Path::new("~tilde")], dirs.test())); + + // pass variable + let actual = nu!(cwd: dirs.test(), "let f = '~tilde2'; utouch $f"); + assert!(actual.err.is_empty()); + assert!(files_exist_at(&[Path::new("~tilde2")], dirs.test())); + }) +} + +#[test] +fn respects_cwd() { + Playground::setup("utouch_respects_cwd", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "mkdir 'dir'; cd 'dir'; utouch 'i_will_be_created.txt'" + ); + + let path = dirs.test().join("dir/i_will_be_created.txt"); + assert!(path.exists()); + }) +} + +#[test] +fn reference_respects_cwd() { + Playground::setup("utouch_reference_respects_cwd", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "mkdir 'dir'; cd 'dir'; utouch 'ref.txt'; utouch --reference 'ref.txt' 'foo.txt'" + ); + + let path = dirs.test().join("dir/foo.txt"); + assert!(path.exists()); + }) +} + +#[test] +fn recognizes_stdout() { + Playground::setup("utouch_recognizes_stdout", |dirs, _sandbox| { + nu!(cwd: dirs.test(), "utouch -"); + assert!(!dirs.test().join("-").exists()); + }) +} + +#[test] +fn follow_symlinks() { + Playground::setup("touch_follows_symlinks", |dirs, sandbox| { + setup_symlink_fs(&dirs, sandbox); + + let missing = dirs.test().join("m"); + assert!(!missing.exists()); + + nu!( + cwd: dirs.test(), + " + touch fds + touch ds + touch fs + touch fms + " + ); + + // We created the missing symlink target + assert!(missing.exists()); + + // The timestamps for files and directories were changed from TIME_ONE + let file_times = symlink_times(&dirs.test().join("f")); + let dir_times = symlink_times(&dirs.test().join("d")); + let dir_file_times = symlink_times(&dirs.test().join("d/f")); + + assert_ne!(file_times, (TIME_ONE, TIME_ONE)); + assert_ne!(dir_times, (TIME_ONE, TIME_ONE)); + assert_ne!(dir_file_times, (TIME_ONE, TIME_ONE)); + + // For symlinks, they remain (mostly) the same + // We can't test accessed times, since to reach the target file, the symlink must be accessed! + let file_symlink_times = symlink_times(&dirs.test().join("fs")); + let dir_symlink_times = symlink_times(&dirs.test().join("ds")); + let dir_file_symlink_times = symlink_times(&dirs.test().join("fds")); + let file_missing_symlink_times = symlink_times(&dirs.test().join("fms")); + + assert_eq!(file_symlink_times.1, TIME_ONE); + assert_eq!(dir_symlink_times.1, TIME_ONE); + assert_eq!(dir_file_symlink_times.1, TIME_ONE); + assert_eq!(file_missing_symlink_times.1, TIME_ONE); + }) +} + +#[test] +fn no_follow_symlinks() { + Playground::setup("touch_touches_symlinks", |dirs, sandbox| { + setup_symlink_fs(&dirs, sandbox); + + let missing = dirs.test().join("m"); + assert!(!missing.exists()); + + nu!( + cwd: dirs.test(), + " + touch fds -s + touch ds -s + touch fs -s + touch fms -s + " + ); + + // We did not create the missing symlink target + assert!(!missing.exists()); + + // The timestamps for files and directories remain the same + let file_times = symlink_times(&dirs.test().join("f")); + let dir_times = symlink_times(&dirs.test().join("d")); + let dir_file_times = symlink_times(&dirs.test().join("d/f")); + + assert_eq!(file_times, (TIME_ONE, TIME_ONE)); + assert_eq!(dir_times, (TIME_ONE, TIME_ONE)); + assert_eq!(dir_file_times, (TIME_ONE, TIME_ONE)); + + // For symlinks, everything changed. (except their targets, and paths, and personality) + let file_symlink_times = symlink_times(&dirs.test().join("fs")); + let dir_symlink_times = symlink_times(&dirs.test().join("ds")); + let dir_file_symlink_times = symlink_times(&dirs.test().join("fds")); + let file_missing_symlink_times = symlink_times(&dirs.test().join("fms")); + + assert_ne!(file_symlink_times, (TIME_ONE, TIME_ONE)); + assert_ne!(dir_symlink_times, (TIME_ONE, TIME_ONE)); + assert_ne!(dir_file_symlink_times, (TIME_ONE, TIME_ONE)); + assert_ne!(file_missing_symlink_times, (TIME_ONE, TIME_ONE)); + }) +}