diff --git a/Cargo.lock b/Cargo.lock index c95f0d1435..def92863a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3009,6 +3009,7 @@ dependencies = [ "uu_mkdir", "uu_mktemp", "uu_mv", + "uu_touch", "uu_uname", "uu_whoami", "uucore 0.0.25", @@ -3783,6 +3784,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.14" @@ -6361,7 +6373,7 @@ dependencies = [ "indicatif", "libc", "quick-error 2.0.1", - "uucore 0.0.26", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir", "xattr", ] @@ -6373,7 +6385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "040aa4584036b2f65e05387b0ea9ac468afce1db325743ce5f350689fd9ce4ae" dependencies = [ "clap", - "uucore 0.0.26", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -6385,7 +6397,7 @@ dependencies = [ "clap", "rand", "tempfile", - "uucore 0.0.26", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -6397,7 +6409,19 @@ dependencies = [ "clap", "fs_extra", "indicatif", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uu_touch" +version = "0.0.26" +dependencies = [ + "chrono", + "clap", + "filetime", + "parse_datetime", "uucore 0.0.26", + "windows-sys 0.48.0", ] [[package]] @@ -6408,7 +6432,7 @@ checksum = "5951832d73199636bde6c0d61cf960932b3c4450142c290375bc10c7abed6db5" dependencies = [ "clap", "platform-info", - "uucore 0.0.26", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -6419,7 +6443,7 @@ checksum = "e3b44166eb6335aeac42744ea368cc4c32d3f2287a4ff765a5ce44d927ab8bb4" dependencies = [ "clap", "libc", - "uucore 0.0.26", + "uucore 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "windows-sys 0.48.0", ] @@ -6435,7 +6459,22 @@ dependencies = [ "nix", "once_cell", "os_display", - "uucore_procs", + "uucore_procs 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", + "wild", +] + +[[package]] +name = "uucore" +version = "0.0.26" +dependencies = [ + "clap", + "glob", + "libc", + "nix", + "number_prefix", + "once_cell", + "os_display", + "uucore_procs 0.0.26", "wild", ] @@ -6453,7 +6492,7 @@ dependencies = [ "number_prefix", "once_cell", "os_display", - "uucore_procs", + "uucore_procs 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", "walkdir", "wild", "winapi-util", @@ -6461,6 +6500,15 @@ dependencies = [ "xattr", ] +[[package]] +name = "uucore_procs" +version = "0.0.26" +dependencies = [ + "proc-macro2", + "quote", + "uuhelp_parser 0.0.26", +] + [[package]] name = "uucore_procs" version = "0.0.26" @@ -6469,9 +6517,13 @@ checksum = "1a233a488da42f3ddb0aaa8a9f75a969e3f37e4de7e909d2d23f6aa3ee401d20" dependencies = [ "proc-macro2", "quote", - "uuhelp_parser", + "uuhelp_parser 0.0.26 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "uuhelp_parser" +version = "0.0.26" + [[package]] name = "uuhelp_parser" version = "0.0.26" diff --git a/Cargo.toml b/Cargo.toml index 7e5f436bbf..9c61d7cd17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,7 @@ uu_cp = "0.0.25" uu_mkdir = "0.0.25" uu_mktemp = "0.0.25" uu_mv = "0.0.25" +uu_touch = { path = "../coreutils/src/uu/touch" } uu_whoami = "0.0.25" uu_uname = "0.0.25" uucore = "0.0.25" diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 4d69ce089a..049a300e99 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -94,6 +94,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 a23f9c4ef4..1252da6ca1 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -225,6 +225,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { Rm, Save, Touch, + UTouch, Glob, Watch, }; @@ -293,6 +294,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { ToTsv, Touch, Upsert, + UTouch, Where, ToXml, ToYaml, 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..7bf712b1eb --- /dev/null +++ b/crates/nu-command/src/filesystem/utouch.rs @@ -0,0 +1,245 @@ +use std::io::ErrorKind; +use std::path::PathBuf; + +use chrono::{DateTime, FixedOffset}; +use filetime::FileTime; + +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, +}; +use uu_touch::error::TouchError; +use uu_touch::{ChangeTimes, InputFile, Options, Source}; + +#[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) ]) + .required( + "filename", + SyntaxShape::Filepath, + "The path of the file you want to create.", + ) + .named( + "reference", + SyntaxShape::Filepath, + "change the file or directory time to the time of the reference file/directory", + Some('r'), + ) + .named( + "timestamp", + SyntaxShape::DateTime, + "use the given time instead of the current time", + Some('t') + ) + .named( + "date", + SyntaxShape::String, + "use the given date instead of the current date", + Some('d') + ) + .switch( + "modified", + "change the modification time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used", + Some('m'), + ) + .switch( + "access", + "change the access time of the file or directory. If no timestamp, date or reference file/directory is given, the current time is used", + Some('a'), + ) + .switch( + "no-create", + "do not create the file if it does not exist", + Some('c'), + ) + .switch( + "no-dereference", + "affect each symbolic link instead of any referenced file (only for systems that can change the timestamps of a symlink)", + None + ) + .rest("rest", SyntaxShape::Filepath, "Additional files to create.") + .category(Category::FileSystem) + } + + fn usage(&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 target: Spanned = call.req(engine_state, stack, 0)?; + let rest: Vec> = call.rest(engine_state, stack, 1)?; + + 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 { + 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 files = vec![InputFile::Path(PathBuf::from(target.item))]; + let mut file_spans = vec![target.span]; + for file in rest { + files.push(InputFile::Path(PathBuf::from(file.item))); + file_spans.push(file.span); + } + + if let Err(err) = uu_touch::touch( + &files, + &Options { + no_create, + no_deref, + source, + date: date_str, + change_times, + strict: true, + }, + ) { + let nu_err = match err { + TouchError::InvalidDateFormat(date) => ShellError::IncorrectValue { + msg: format!("Invalid date: {}", date), + val_span: date_span.expect("utouch was given a date"), + call_span: call.head, + }, + TouchError::ReferenceFileInaccessible(reference_path, io_err) => { + let span = reference_span.expect("utouch was given a reference file"); + if io_err.kind() == ErrorKind::NotFound { + // todo merge main into this to say which file not found + ShellError::FileNotFound { + span, + file: reference_path.display().to_string(), + } + } else { + io_to_nu_err( + io_err, + format!("Failed to read metadata of {}", reference_path.display()), + span, + ) + } + } + _ => 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 modified time of files a, b and c to the current time but yesterday", + example: r#"utouch -m -d "yesterday" a b c"#, + result: None, + }, + Example { + description: r#"Changes the last modified time of file 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, + }, + ] + } +} + +fn io_to_nu_err(err: std::io::Error, msg: String, span: Span) -> ShellError { + ShellError::GenericError { + error: err.to_string(), + msg, + span: Some(span), + help: None, + inner: Vec::new(), + } +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 922e804405..60a0dd8dc4 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -124,6 +124,7 @@ mod update; mod upsert; mod url; mod use_; +mod utouch; mod where_; #[cfg(feature = "which-support")] mod which; diff --git a/crates/nu-command/tests/commands/utouch.rs b/crates/nu-command/tests/commands/utouch.rs new file mode 100644 index 0000000000..8665837149 --- /dev/null +++ b/crates/nu-command/tests/commands/utouch.rs @@ -0,0 +1,176 @@ +use chrono::{DateTime, Local}; +use nu_test_support::fs::Stub; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[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")]); + + nu!( + cwd: dirs.test(), + "utouch -m file.txt" + ); + + let path = dirs.test().join("file.txt"); + + // Check only the date since the time may not match exactly + let date = Local::now().date_naive(); + let actual_date_time: DateTime = + DateTime::from(path.metadata().unwrap().modified().unwrap()); + let actual_date = actual_date_time.date_naive(); + + assert_eq!(date, actual_date); + }) +} + +#[test] +fn change_access_time_of_file_to_today() { + Playground::setup("change_time_test_18", |dirs, sandbox| { + sandbox.with_files(&[Stub::EmptyFile("file.txt")]); + + nu!( + cwd: dirs.test(), + "utouch -a file.txt" + ); + + let path = dirs.test().join("file.txt"); + + // Check only the date since the time may not match exactly + let date = Local::now().date_naive(); + let actual_date_time: DateTime = + DateTime::from(path.metadata().unwrap().accessed().unwrap()); + let actual_date = actual_date_time.date_naive(); + + assert_eq!(date, actual_date); + }) +} + +#[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")]); + + nu!( + cwd: dirs.test(), + "utouch -a -m file.txt" + ); + + let metadata = dirs.test().join("file.txt").metadata().unwrap(); + + // Check only the date since the time may not match exactly + let date = Local::now().date_naive(); + let adate_time: DateTime = DateTime::from(metadata.accessed().unwrap()); + let adate = adate_time.date_naive(); + let mdate_time: DateTime = DateTime::from(metadata.modified().unwrap()); + let mdate = mdate_time.date_naive(); + + assert_eq!(date, adate); + assert_eq!(date, mdate); + }) +} + +#[test] +fn not_create_file_if_it_not_exists() { + Playground::setup("change_time_test_28", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch -c file.txt" + ); + + let path = dirs.test().join("file.txt"); + + assert!(!path.exists()); + + nu!( + cwd: dirs.test(), + "utouch -c file.txt" + ); + + let path = dirs.test().join("file.txt"); + + assert!(!path.exists()); + }) +} + +#[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 creates_with_date() { + Playground::setup("create_test_1", |dirs, _sandbox| { + nu!( + cwd: dirs.test(), + "utouch 'file....'" + ); + + let path = dirs.test().join("file...."); + assert!(path.exists()); + }) +}