diff --git a/crates/nu-command/src/filesystem/ucp.rs b/crates/nu-command/src/filesystem/ucp.rs index 56eea910ad..786137b35c 100644 --- a/crates/nu-command/src/filesystem/ucp.rs +++ b/crates/nu-command/src/filesystem/ucp.rs @@ -48,6 +48,8 @@ impl Command for UCp { ) .switch("progress", "display a progress bar", Some('p')) .switch("no-clobber", "do not overwrite an existing file", Some('n')) + .switch("link", "hard-link files instead of copying", Some('l')) + .switch("symbolic-link", "make symbolic links instead of copying", Some('s')) .named( "preserve", SyntaxShape::List(Box::new(SyntaxShape::String)), @@ -89,6 +91,21 @@ impl Command for UCp { example: "cp -u myfile newfile", result: None, }, + Example { + description: "", + example: "cp -s a b", + result: None, + }, + Example { + description: "", + example: "cp -l a b", + result: None, + }, + Example { + description: "", + example: "cp -r -l a b", + result: None, + }, Example { description: "Copy file preserving mode and timestamps attributes", example: "cp --preserve [ mode timestamps ] myfile newfile", @@ -115,10 +132,19 @@ impl Command for UCp { _input: PipelineData, ) -> Result { let interactive = call.has_flag(engine_state, stack, "interactive")?; - let (update, copy_mode) = if call.has_flag(engine_state, stack, "update")? { - (UpdateMode::ReplaceIfOlder, CopyMode::Update) + let update = if call.has_flag(engine_state, stack, "update")? { + UpdateMode::ReplaceIfOlder } else { - (UpdateMode::ReplaceAll, CopyMode::Copy) + UpdateMode::ReplaceAll + }; + let copy_mode = if call.has_flag(engine_state, stack, "link")? { + CopyMode::Link + } else if call.has_flag(engine_state, stack, "symbolic-link")? { + CopyMode::SymLink + } else if call.has_flag(engine_state, stack, "update")? { + CopyMode::Update + } else { + CopyMode::Copy }; let force = call.has_flag(engine_state, stack, "force")?; diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index 9b4cdce889..972141d4cf 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -46,6 +46,48 @@ fn copies_a_file_impl(progress: bool) { }); } +#[test] +fn copies_a_file_with_hardlink() { + copies_a_file_with_hardlink_impl(false); + copies_a_file_with_hardlink_impl(true); +} + +fn copies_a_file_with_hardlink_impl(progress: bool) { + Playground::setup("ucp_test_1-hardlink", |dirs, _| { + let test_file = dirs.formats().join("sample.ini"); + let progress_flag = if progress { "-p" } else { "" }; + + // Get the hash of the file content to check integrity after copy. + let first_hash = get_file_hash(test_file.display()); + + nu!( + cwd: dirs.root(), + "cp -l {} `{}` ucp_test_1-hardlink/sample.ini", + progress_flag, + test_file.display() + ); + + assert!(dirs.test().join("sample.ini").exists()); + + // Get the hash of the copied file content to check against first_hash. + let after_cp_hash = get_file_hash(dirs.test().join("sample.ini").display()); + assert_eq!(first_hash, after_cp_hash); + + // Modify file 1 by appending a line + nu!( + cwd: dirs.root(), + "echo 'new line' | save `{}` --append", + test_file.display() + ); + // We will compare the contents of file 1 and file 2. + // Since we're using a hardlink, the updated hashes should be the same. + let first_hash_modified = get_file_hash(test_file.display()); + assert_ne!(first_hash, first_hash_modified); + let after_cp_hash_modified = get_file_hash(dirs.test().join("sample.ini").display()); + assert_eq!(first_hash_modified, after_cp_hash_modified); + }); +} + #[test] fn copies_the_file_inside_directory_if_path_to_copy_is_directory() { copies_the_file_inside_directory_if_path_to_copy_is_directory_impl(false); @@ -729,6 +771,45 @@ fn test_cp_recurse() { }); } +#[test] +fn test_cp_recurse_with_hardlink_flag() { + Playground::setup("ucp_test_22-hardlink", |dirs, sandbox| { + // Create the relevant target directories + sandbox.mkdir(TEST_COPY_FROM_FOLDER); + sandbox.mkdir(TEST_COPY_TO_FOLDER_NEW); + let src = dirs + .fixtures + .join("cp") + .join(TEST_COPY_FROM_FOLDER) + .join(TEST_COPY_FROM_FOLDER_FILE); + + let src_hash = get_file_hash(src.display()); + // Start test + nu!( + cwd: dirs.root(), + "cp -r -l {} ucp_test_22-hardlink/{}", + TEST_COPY_FROM_FOLDER, + TEST_COPY_TO_FOLDER_NEW, + ); + let after_cp_hash = get_file_hash(dirs.test().join(TEST_COPY_TO_FOLDER_NEW_FILE).display()); + assert_eq!(src_hash, after_cp_hash); + + // Modify file 1 by appending a line + nu!( + cwd: dirs.root(), + "echo 'new line' | save `{}` --append", + src.display() + ); + // We will compare the contents of file 1 and file 2. + // Since we're using a hardlink, the updated hashes should be the same. + let src_hash_modified = get_file_hash(src.display()); + assert_ne!(src_hash, src_hash_modified); + let after_cp_hash_modified = + get_file_hash(dirs.test().join(TEST_COPY_TO_FOLDER_NEW_FILE).display()); + assert_eq!(src_hash_modified, after_cp_hash_modified); + }); +} + #[test] fn test_cp_with_dirs() { Playground::setup("ucp_test_23", |dirs, sandbox| {