From cca749aace8d8ee4af108acffc404092ab43e82e Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:23:35 +0300 Subject: [PATCH 1/2] create: add --no-recursion With --no-recursion, a directory named on the command line is added as a directory entry only, without its contents, like GNU tar. Closes #188 --- benches/bench_tar.rs | 4 +++ src/uu/tar/src/operations/create.rs | 26 ++++++++++++++++---- src/uu/tar/src/operations/create_tests.rs | 4 +++ src/uu/tar/src/tar.rs | 4 +++ tests/by-util/test_tar.rs | 30 +++++++++++++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/benches/bench_tar.rs b/benches/bench_tar.rs index 638813c..555a1f8 100644 --- a/benches/bench_tar.rs +++ b/benches/bench_tar.rs @@ -48,6 +48,7 @@ fn build_archive(archive_path: &Path, source_dir: &Path) { &refs, true, false, + true, CompressionMode::None, ) .unwrap(); @@ -76,6 +77,7 @@ fn create_archive_10_files(bencher: divan::Bencher) { &refs, true, false, + true, CompressionMode::None, ) .unwrap(); @@ -101,6 +103,7 @@ fn create_archive_100_files(bencher: divan::Bencher) { &refs, true, false, + true, CompressionMode::None, ) .unwrap(); @@ -126,6 +129,7 @@ fn create_archive_directory(bencher: divan::Bencher) { &[sub.as_path()], true, false, + true, CompressionMode::None, ) .unwrap(); diff --git a/src/uu/tar/src/operations/create.rs b/src/uu/tar/src/operations/create.rs index 7714a43..7481ce7 100644 --- a/src/uu/tar/src/operations/create.rs +++ b/src/uu/tar/src/operations/create.rs @@ -22,6 +22,7 @@ use uucore::error::UResult; /// * `files` - Slice of file paths to add to the archive /// * `allow_absolute` - Allow absolute paths while creating archive /// * `verbose` - Whether to print verbose output during creation +/// * `recursive` - Whether to add the contents of directories, not only the directories themselves /// /// # Errors /// @@ -35,6 +36,7 @@ pub fn create_archive( files: &[&Path], allow_absolute: bool, verbose: bool, + recursive: bool, compression: CompressionMode, ) -> UResult<()> { let output = BufWriter::new(output); @@ -56,13 +58,22 @@ pub fn create_archive( } if verbose { - print_verbose_tree(&mut status_output, path)?; + print_verbose_tree(&mut status_output, path, recursive)?; } let normalized_name = get_normalized_path(path, allow_absolute)?; - // If it's a directory, recursively add all contents - if path.is_dir() { + if path.is_dir() && !recursive { + // --no-recursion: the directory entry only, named with a trailing + // slash like the top directory added by append_dir_all + builder + .append_dir(normalized_name.join(""), path) + .map_err(|e| TarError::CannotAddDirectory { + path: path.to_path_buf(), + source: e, + })?; + } else if path.is_dir() { + // If it's a directory, recursively add all contents builder.append_dir_all(normalized_name, path).map_err(|e| { TarError::CannotAddDirectory { path: path.to_path_buf(), @@ -90,8 +101,13 @@ pub fn create_archive( Ok(()) } -fn print_verbose_tree(status_output: &mut impl Write, path: &Path) -> UResult<()> { - let to_print = get_tree(path)? +fn print_verbose_tree(status_output: &mut impl Write, path: &Path, recursive: bool) -> UResult<()> { + let tree = if recursive { + get_tree(path)? + } else { + vec![path.to_path_buf()] + }; + let to_print = tree .iter() .map(|p| (p.is_dir(), p.display().to_string())) .map(|(is_dir, path)| { diff --git a/src/uu/tar/src/operations/create_tests.rs b/src/uu/tar/src/operations/create_tests.rs index dbbc71c..5d5f823 100644 --- a/src/uu/tar/src/operations/create_tests.rs +++ b/src/uu/tar/src/operations/create_tests.rs @@ -33,6 +33,7 @@ fn test_create_archive_flush_failed() { &[file_path.as_path()], false, false, + true, CompressionMode::None, ); assert!(res.is_err()); @@ -53,6 +54,7 @@ fn test_create_archive_gzip_flush_failed() { &[file_path.as_path()], false, false, + true, CompressionMode::Gzip, ); assert!(res.is_err()); @@ -70,6 +72,7 @@ fn test_create_archive_with_zstd() { &[Path::new("file.txt")], false, false, + true, CompressionMode::Zstd, ) .unwrap(); @@ -93,6 +96,7 @@ fn test_create_archive_missing_file_fails() { &[missing_path.as_path()], false, false, + true, CompressionMode::Zstd, ) .unwrap_err(); diff --git a/src/uu/tar/src/tar.rs b/src/uu/tar/src/tar.rs index 60ce5a5..73c8225 100644 --- a/src/uu/tar/src/tar.rs +++ b/src/uu/tar/src/tar.rs @@ -145,6 +145,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let verbose = matches.get_flag("verbose"); let allow_absolute = matches.get_flag("absolute-names"); + let recursive = !matches.get_flag("no-recursion"); let explicit_compression = if matches.get_flag("gzip") { Some(CompressionMode::Gzip) } else if matches.get_flag("zstd") { @@ -201,6 +202,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &files, allow_absolute, verbose, + recursive, compression, ) } @@ -216,6 +218,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { &files, allow_absolute, verbose, + recursive, compression, ) }; @@ -276,6 +279,7 @@ pub fn uu_app() -> Command { arg!(--zstd "Filter through zstd").conflicts_with("gzip"), // Common options arg!(-v --verbose "Verbosely list files processed"), + arg!(--"no-recursion" "Avoid descending automatically in directories"), // arg!(-h --dereference "Follow symlinks"), // arg!(-p --"preserve-permissions" "Extract information about file permissions"), // Help diff --git a/tests/by-util/test_tar.rs b/tests/by-util/test_tar.rs index 975f523..b766295 100644 --- a/tests/by-util/test_tar.rs +++ b/tests/by-util/test_tar.rs @@ -86,6 +86,36 @@ fn test_create_dir_verbose() { .stdout_contains(file2_path); } +#[test] +fn test_create_no_recursion() { + let (at, mut ucmd) = at_and_ucmd!(); + let separator = path::MAIN_SEPARATOR; + + at.mkdir("dir1"); + at.mkdir(format!("dir1{separator}dir2")); + at.write(&format!("dir1{separator}file1.txt"), "test content 1"); + at.write( + &format!("dir1{separator}dir2{separator}file2.txt"), + "test content 2", + ); + + ucmd.args(&[ + "--no-recursion", + "-cvf", + "archive.tar", + "dir1", + &format!("dir1{separator}file1.txt"), + ]) + .succeeds() + .stdout_is(format!("dir1{separator}\ndir1{separator}file1.txt\n")); + + new_ucmd!() + .arg("-tf") + .arg(at.plus("archive.tar")) + .succeeds() + .stdout_is("dir1/\ndir1/file1.txt\n"); +} + #[test] fn test_create_single_file() { let (at, mut ucmd) = at_and_ucmd!(); From 2299db6271a3458b6fe2ab4b47dff34726396d63 Mon Sep 17 00:00:00 2001 From: Yunus Emre Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:43:22 +0300 Subject: [PATCH 2/2] create: check is_dir once for the recursive and --no-recursion cases --- src/uu/tar/src/operations/create.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/uu/tar/src/operations/create.rs b/src/uu/tar/src/operations/create.rs index 7481ce7..35d7f3f 100644 --- a/src/uu/tar/src/operations/create.rs +++ b/src/uu/tar/src/operations/create.rs @@ -63,22 +63,18 @@ pub fn create_archive( let normalized_name = get_normalized_path(path, allow_absolute)?; - if path.is_dir() && !recursive { - // --no-recursion: the directory entry only, named with a trailing - // slash like the top directory added by append_dir_all - builder - .append_dir(normalized_name.join(""), path) - .map_err(|e| TarError::CannotAddDirectory { - path: path.to_path_buf(), - source: e, - })?; - } else if path.is_dir() { - // If it's a directory, recursively add all contents - builder.append_dir_all(normalized_name, path).map_err(|e| { - TarError::CannotAddDirectory { - path: path.to_path_buf(), - source: e, - } + if path.is_dir() { + if recursive { + // If it's a directory, recursively add all contents + builder.append_dir_all(normalized_name, path) + } else { + // --no-recursion: the directory entry only, named with a trailing + // slash like the top directory added by append_dir_all + builder.append_dir(normalized_name.join(""), path) + } + .map_err(|e| TarError::CannotAddDirectory { + path: path.to_path_buf(), + source: e, })?; } else { // For files, add them directly