From 5a346713432bb5972d06a10f91cb0c1cb30fdc1e Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Sun, 21 May 2023 00:48:57 +0800 Subject: [PATCH] add -u flag to cp, mv command (#9214) # Description Closes: #7853 I found that I want this feature too... So I take over it, sorry for that @VincenzoCarlino # User-Facing Changes # Tests + Formatting # After Submitting --- crates/nu-command/src/filesystem/cp.rs | 16 ++++++++++ crates/nu-command/src/filesystem/mv.rs | 18 +++++++++-- crates/nu-command/src/filesystem/util.rs | 28 +++++++++++++++++ crates/nu-command/tests/commands/cp.rs | 32 +++++++++++++++++++- crates/nu-command/tests/commands/move_/mv.rs | 24 ++++++++++++++- 5 files changed, 113 insertions(+), 5 deletions(-) diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs index 33a6036da..e3ad7dc58 100644 --- a/crates/nu-command/src/filesystem/cp.rs +++ b/crates/nu-command/src/filesystem/cp.rs @@ -57,6 +57,10 @@ impl Command for Cp { "show successful copies in addition to failed copies (default:false)", Some('v'), ) + .switch("update", + "copy only when the SOURCE file is newer than the destination file or when the destination file is missing", + Some('u') + ) // TODO: add back in additional features // .switch("force", "suppress error when no file", Some('f')) .switch("interactive", "ask user to confirm action", Some('i')) @@ -88,6 +92,7 @@ impl Command for Cp { let verbose = call.has_flag("verbose"); let interactive = call.has_flag("interactive"); let progress = call.has_flag("progress"); + let update_mode = call.has_flag("update"); let current_dir_path = current_dir(engine_state, stack)?; let source = current_dir_path.join(src.item.as_str()); @@ -177,6 +182,12 @@ impl Command for Cp { if src.is_file() { let dst = canonicalize_with(dst.as_path(), ¤t_dir_path).unwrap_or(dst); + + // ignore when source file is not newer than target file + if update_mode && super::util::is_older(&src, &dst) { + continue; + } + let res = if src == dst { let message = format!( "src {source:?} and dst {destination:?} are identical(not copied)" @@ -361,6 +372,11 @@ impl Command for Cp { example: "cp *.txt dir_a", result: None, }, + Example { + description: "Copy only if source file is newer than target file", + example: "cp -u a b", + result: None, + }, ] } } diff --git a/crates/nu-command/src/filesystem/mv.rs b/crates/nu-command/src/filesystem/mv.rs index bc68946dc..3e93c935d 100644 --- a/crates/nu-command/src/filesystem/mv.rs +++ b/crates/nu-command/src/filesystem/mv.rs @@ -53,6 +53,11 @@ impl Command for Mv { ) .switch("force", "overwrite the destination.", Some('f')) .switch("interactive", "ask user to confirm action", Some('i')) + .switch("update", + "move only when the SOURCE file is newer than the destination file(with -f) or when the destination file is missing", + Some('u') + ) + // TODO: add back in additional features .category(Category::FileSystem) } @@ -75,6 +80,7 @@ impl Command for Mv { let verbose = call.has_flag("verbose"); let interactive = call.has_flag("interactive"); let force = call.has_flag("force"); + let update_mode = call.has_flag("update"); let ctrlc = engine_state.ctrlc.clone(); @@ -191,6 +197,7 @@ impl Command for Mv { span: spanned_destination.span, }, interactive, + update_mode, ); if let Err(error) = result { Some(Value::Error { @@ -244,6 +251,7 @@ fn move_file( spanned_from: Spanned, spanned_to: Spanned, interactive: bool, + update_mode: bool, ) -> Result { let Spanned { item: from, @@ -305,9 +313,13 @@ fn move_file( } } - match move_item(&from, from_span, &to) { - Ok(()) => Ok(true), - Err(e) => Err(e), + if update_mode && super::util::is_older(&from, &to) { + Ok(false) + } else { + match move_item(&from, from_span, &to) { + Ok(()) => Ok(true), + Err(e) => Err(e), + } } } diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index eaba9d46a..14b1ada92 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -132,3 +132,31 @@ fn get_interactive_confirmation(prompt: String) -> Result> Ok(false) } } + +pub fn is_older(src: &Path, dst: &Path) -> bool { + if !dst.exists() { + return true; + } + #[cfg(unix)] + { + use std::os::unix::fs::MetadataExt; + let src_ctime = std::fs::metadata(src) + .map(|m| m.ctime()) + .unwrap_or(i64::MIN); + let dst_ctime = std::fs::metadata(dst) + .map(|m| m.ctime()) + .unwrap_or(i64::MAX); + src_ctime <= dst_ctime + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + let src_ctime = std::fs::metadata(src) + .map(|m| m.last_write_time()) + .unwrap_or(u64::MIN); + let dst_ctime = std::fs::metadata(dst) + .map(|m| m.last_write_time()) + .unwrap_or(u64::MAX); + src_ctime <= dst_ctime + } +} diff --git a/crates/nu-command/tests/commands/cp.rs b/crates/nu-command/tests/commands/cp.rs index 2c9f4f664..f7168e320 100644 --- a/crates/nu-command/tests/commands/cp.rs +++ b/crates/nu-command/tests/commands/cp.rs @@ -1,7 +1,7 @@ use nu_test_support::fs::file_contents; use nu_test_support::fs::{ files_exist_at, AbsoluteFile, - Stub::{EmptyFile, FileWithPermission}, + Stub::{EmptyFile, FileWithContent, FileWithPermission}, }; use nu_test_support::nu; use nu_test_support::playground::Playground; @@ -579,3 +579,33 @@ fn copy_file_with_read_permission_impl(progress: bool) { ); }); } + +#[test] +fn copy_file_with_update_flag() { + copy_file_with_update_flag_impl(false); + copy_file_with_update_flag_impl(true); +} + +fn copy_file_with_update_flag_impl(progress: bool) { + Playground::setup("cp_test_19", |_dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("valid.txt"), + FileWithContent("newer_valid.txt", "body"), + ]); + + let progress_flag = if progress { "-p" } else { "" }; + + let actual = nu!( + cwd: sandbox.cwd(), + "cp {} -u valid.txt newer_valid.txt; open newer_valid.txt", + progress_flag, + ); + assert!(actual.out.contains("body")); + + // create a file after assert to make sure that newest_valid.txt is newest + std::thread::sleep(std::time::Duration::from_secs(1)); + sandbox.with_files(vec![FileWithContent("newest_valid.txt", "newest_body")]); + let actual = nu!(cwd: sandbox.cwd(), "cp {} -u newest_valid.txt valid.txt; open valid.txt", progress_flag); + assert_eq!(actual.out, "newest_body"); + }); +} diff --git a/crates/nu-command/tests/commands/move_/mv.rs b/crates/nu-command/tests/commands/move_/mv.rs index 2e1414a59..9552b3bff 100644 --- a/crates/nu-command/tests/commands/move_/mv.rs +++ b/crates/nu-command/tests/commands/move_/mv.rs @@ -1,4 +1,4 @@ -use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; +use nu_test_support::fs::{files_exist_at, Stub::EmptyFile, Stub::FileWithContent}; use nu_test_support::nu; use nu_test_support::playground::Playground; @@ -464,3 +464,25 @@ fn mv_change_case_of_file() { assert!(files_in_test_directory.contains(&new_file_name)); }) } + +#[test] +fn mv_with_update_flag() { + Playground::setup("mv_with_update_flag", |_dirs, sandbox| { + sandbox.with_files(vec![ + EmptyFile("valid.txt"), + FileWithContent("newer_valid.txt", "body"), + ]); + + let actual = nu!( + cwd: sandbox.cwd(), + "mv -uf valid.txt newer_valid.txt; open newer_valid.txt", + ); + assert_eq!(actual.out, "body"); + + // create a file after assert to make sure that newest_valid.txt is newest + std::thread::sleep(std::time::Duration::from_secs(1)); + sandbox.with_files(vec![FileWithContent("newest_valid.txt", "newest_body")]); + let actual = nu!(cwd: sandbox.cwd(), "mv -uf newest_valid.txt valid.txt; open valid.txt"); + assert_eq!(actual.out, "newest_body"); + }); +}