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
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect -A clippy::result_large_err` to check that
you're using the standard code style
- `cargo test --workspace` to check that all tests pass
- `cargo run -- crates/nu-std/tests/run.nu` to run the tests for the
standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
This commit is contained in:
WindSoilder 2023-05-21 00:48:57 +08:00 committed by GitHub
parent 9b139330f8
commit 5a34671343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 113 additions and 5 deletions

View File

@ -57,6 +57,10 @@ impl Command for Cp {
"show successful copies in addition to failed copies (default:false)", "show successful copies in addition to failed copies (default:false)",
Some('v'), 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 // TODO: add back in additional features
// .switch("force", "suppress error when no file", Some('f')) // .switch("force", "suppress error when no file", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i')) .switch("interactive", "ask user to confirm action", Some('i'))
@ -88,6 +92,7 @@ impl Command for Cp {
let verbose = call.has_flag("verbose"); let verbose = call.has_flag("verbose");
let interactive = call.has_flag("interactive"); let interactive = call.has_flag("interactive");
let progress = call.has_flag("progress"); let progress = call.has_flag("progress");
let update_mode = call.has_flag("update");
let current_dir_path = current_dir(engine_state, stack)?; let current_dir_path = current_dir(engine_state, stack)?;
let source = current_dir_path.join(src.item.as_str()); let source = current_dir_path.join(src.item.as_str());
@ -177,6 +182,12 @@ impl Command for Cp {
if src.is_file() { if src.is_file() {
let dst = let dst =
canonicalize_with(dst.as_path(), &current_dir_path).unwrap_or(dst); canonicalize_with(dst.as_path(), &current_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 res = if src == dst {
let message = format!( let message = format!(
"src {source:?} and dst {destination:?} are identical(not copied)" "src {source:?} and dst {destination:?} are identical(not copied)"
@ -361,6 +372,11 @@ impl Command for Cp {
example: "cp *.txt dir_a", example: "cp *.txt dir_a",
result: None, result: None,
}, },
Example {
description: "Copy only if source file is newer than target file",
example: "cp -u a b",
result: None,
},
] ]
} }
} }

View File

@ -53,6 +53,11 @@ impl Command for Mv {
) )
.switch("force", "overwrite the destination.", Some('f')) .switch("force", "overwrite the destination.", Some('f'))
.switch("interactive", "ask user to confirm action", Some('i')) .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) .category(Category::FileSystem)
} }
@ -75,6 +80,7 @@ impl Command for Mv {
let verbose = call.has_flag("verbose"); let verbose = call.has_flag("verbose");
let interactive = call.has_flag("interactive"); let interactive = call.has_flag("interactive");
let force = call.has_flag("force"); let force = call.has_flag("force");
let update_mode = call.has_flag("update");
let ctrlc = engine_state.ctrlc.clone(); let ctrlc = engine_state.ctrlc.clone();
@ -191,6 +197,7 @@ impl Command for Mv {
span: spanned_destination.span, span: spanned_destination.span,
}, },
interactive, interactive,
update_mode,
); );
if let Err(error) = result { if let Err(error) = result {
Some(Value::Error { Some(Value::Error {
@ -244,6 +251,7 @@ fn move_file(
spanned_from: Spanned<PathBuf>, spanned_from: Spanned<PathBuf>,
spanned_to: Spanned<PathBuf>, spanned_to: Spanned<PathBuf>,
interactive: bool, interactive: bool,
update_mode: bool,
) -> Result<bool, ShellError> { ) -> Result<bool, ShellError> {
let Spanned { let Spanned {
item: from, item: from,
@ -305,11 +313,15 @@ fn move_file(
} }
} }
if update_mode && super::util::is_older(&from, &to) {
Ok(false)
} else {
match move_item(&from, from_span, &to) { match move_item(&from, from_span, &to) {
Ok(()) => Ok(true), Ok(()) => Ok(true),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
}
fn move_item(from: &Path, from_span: Span, to: &Path) -> Result<(), ShellError> { fn move_item(from: &Path, from_span: Span, to: &Path) -> Result<(), ShellError> {
// We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy // We first try a rename, which is a quick operation. If that doesn't work, we'll try a copy

View File

@ -132,3 +132,31 @@ fn get_interactive_confirmation(prompt: String) -> Result<bool, Box<dyn Error>>
Ok(false) 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
}
}

View File

@ -1,7 +1,7 @@
use nu_test_support::fs::file_contents; use nu_test_support::fs::file_contents;
use nu_test_support::fs::{ use nu_test_support::fs::{
files_exist_at, AbsoluteFile, files_exist_at, AbsoluteFile,
Stub::{EmptyFile, FileWithPermission}, Stub::{EmptyFile, FileWithContent, FileWithPermission},
}; };
use nu_test_support::nu; use nu_test_support::nu;
use nu_test_support::playground::Playground; 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");
});
}

View File

@ -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::nu;
use nu_test_support::playground::Playground; 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)); 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");
});
}