tar: add -t/--list operation

Implement the list operation. Non-verbose (-t) prints one entry path
per line. Verbose (-tv) prints permissions, owner/group, size,
modification time, and path — matching GNU tar's ls-l style output.

Adds conflict group so -c, -x, and -t are mutually exclusive.

Pulls in chrono for timestamp formatting in verbose output. chrono is
already in the workspace dependency set and will also be needed for
date parsing when --newer=DATE is implemented.

Point uucore/uutests at the coreutils git repo while we're iterating
quickly on basic functionality.
This commit is contained in:
Jeff Bailey
2026-04-03 10:13:30 +02:00
committed by Daniel Hofstetter
parent f85dddd20e
commit 06042058f9
7 changed files with 259 additions and 114 deletions
Generated
+54 -106
View File
@@ -73,7 +73,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
dependencies = [
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -84,7 +84,7 @@ checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -161,7 +161,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.0",
"rand_core",
]
[[package]]
@@ -287,16 +287,6 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctor"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "424e0138278faeb2b401f174ad17e715c829512d74f3d1e81eb43365c2e0590e"
dependencies = [
"ctor-proc-macro",
"dtor 0.1.0",
]
[[package]]
name = "ctor"
version = "0.8.0"
@@ -304,7 +294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
dependencies = [
"ctor-proc-macro",
"dtor 0.3.0",
"dtor",
]
[[package]]
@@ -365,16 +355,7 @@ dependencies = [
"cfg-if",
"libc",
"socket2",
"windows-sys",
]
[[package]]
name = "dtor"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934"
dependencies = [
"dtor-proc-macro",
"windows-sys 0.60.2",
]
[[package]]
@@ -392,6 +373,12 @@ version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
[[package]]
name = "dunce"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -405,7 +392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -423,7 +410,7 @@ dependencies = [
"cfg-if",
"libc",
"libredox",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -526,7 +513,7 @@ dependencies = [
"js-sys",
"libc",
"r-efi",
"rand_core 0.10.0",
"rand_core",
"wasip2",
"wasip3",
"wasm-bindgen",
@@ -662,7 +649,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde_core",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -788,9 +775,9 @@ dependencies = [
[[package]]
name = "nix"
version = "0.30.1"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags",
"cfg-if",
@@ -925,15 +912,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -978,16 +956,6 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.0"
@@ -996,26 +964,7 @@ checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20",
"getrandom 0.4.1",
"rand_core 0.10.0",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.4",
"rand_core",
]
[[package]]
@@ -1093,7 +1042,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -1216,7 +1165,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -1261,12 +1210,12 @@ dependencies = [
"clap",
"clap_complete",
"clap_mangen",
"ctor 0.8.0",
"ctor",
"libc",
"phf",
"phf_codegen",
"pretty_assertions",
"rand 0.10.0",
"rand",
"regex",
"rlimit",
"tar",
@@ -1289,7 +1238,7 @@ dependencies = [
"getrandom 0.4.1",
"once_cell",
"rustix",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -1299,7 +1248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
dependencies = [
"rustix",
"windows-sys",
"windows-sys 0.60.2",
]
[[package]]
@@ -1452,6 +1401,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
name = "uu_tar"
version = "0.0.1"
dependencies = [
"chrono",
"clap",
"regex",
"tar",
@@ -1460,12 +1410,12 @@ dependencies = [
[[package]]
name = "uucore"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8038531f506a34ab4612b93f97d5f40759768cd34a83fd2af041b84fcbde474"
version = "0.8.0"
source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e"
dependencies = [
"clap",
"dns-lookup",
"dunce",
"fluent",
"fluent-bundle",
"fluent-syntax",
@@ -1479,13 +1429,14 @@ dependencies = [
"unic-langid",
"uucore_procs",
"wild",
"winapi-util",
"windows-sys 0.61.2",
]
[[package]]
name = "uucore_procs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f63e2d5083ff0983193a33e2d57fd271c7e3e3e7df8e46e8f471865647b2cbc"
version = "0.8.0"
source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e"
dependencies = [
"proc-macro2",
"quote",
@@ -1493,15 +1444,14 @@ dependencies = [
[[package]]
name = "uutests"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b8a1598ddd20230a5df59d642e2e5741b0b90038bde8684c984515f947fda5"
version = "0.8.0"
source = "git+https://github.com/uutils/coreutils#f6451c7d20bc65640e92ecdc368470577d7c9e2e"
dependencies = [
"ctor 0.6.3",
"ctor",
"libc",
"nix",
"pretty_assertions",
"rand 0.9.2",
"rand",
"regex",
"rlimit",
"tempfile",
@@ -1621,6 +1571,15 @@ dependencies = [
"glob",
]
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "windows-core"
version = "0.62.2"
@@ -1689,6 +1648,15 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
@@ -1864,26 +1832,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]]
name = "zerocopy"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
+2 -2
View File
@@ -43,8 +43,8 @@ phf = "0.13.0"
phf_codegen = "0.13.0"
rand = { version = "0.10.0" }
regex = "1.10.4"
uucore = "0.7.0"
uutests = "0.7.0"
uucore = { git = "https://github.com/uutils/coreutils" }
uutests = { git = "https://github.com/uutils/coreutils" }
tar = "0.4"
tempfile = "3.10.1"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
+2 -1
View File
@@ -13,10 +13,11 @@ categories = ["command-line-utilities"]
[dependencies]
uucore = { workspace = true }
uucore = { workspace = true, features = ["fs"] }
clap = { workspace = true }
regex = { workspace = true }
tar = { workspace = true }
chrono = { workspace = true }
[lib]
path = "src/tar.rs"
+91
View File
@@ -0,0 +1,91 @@
// This file is part of the uutils tar package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::errors::TarError;
use chrono::{TimeZone, Utc};
use std::fs::File;
use std::path::Path;
use tar::Archive;
use uucore::error::UResult;
use uucore::fs::display_permissions_unix;
/// List the contents of a tar archive, printing one entry per line.
pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
let file: File =
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
let mut archive = Archive::new(file);
for entry_result in archive
.entries()
.map_err(|e| TarError::InvalidArchive(format!("Failed to read archive entries: {e}")))?
{
let entry = entry_result
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry: {e}")))?;
if verbose {
// Collect all header fields into owned values before borrowing entry for the path,
// since both header() and path() require a borrow of entry.
let (mode, entry_type, owner, group, size, mtime) = {
let header = entry.header();
(
header.mode().unwrap_or(0),
header.entry_type(),
header
.username()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header
.groupname()
.ok()
.flatten()
.unwrap_or_default()
.to_owned(),
header.size().unwrap_or(0),
header.mtime().unwrap_or(0),
)
};
let path = entry
.path()
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?;
let type_char = match entry_type {
tar::EntryType::Directory => 'd',
tar::EntryType::Symlink => 'l',
tar::EntryType::Char => 'c',
tar::EntryType::Block => 'b',
tar::EntryType::Fifo => 'p',
_ => '-',
};
// Tar headers store the type separately from the mode bits, so we get the
// 9-character rwx string from uucore and prepend our own type character.
let perm_str = display_permissions_unix(mode, false);
let permissions = format!("{type_char}{perm_str}");
// TODO: GNU tar displays mtime in the user's local timezone; we
// currently format in UTC. Convert to local time for compatibility.
let dt: chrono::DateTime<Utc> = Utc
.timestamp_opt(mtime as i64, 0)
.single()
.unwrap_or_else(Utc::now);
let date_str = dt.format("%Y-%m-%d %H:%M");
println!(
"{permissions} {owner}/{group} {size:>8} {date_str} {}",
path.display()
);
} else {
let path = entry
.path()
.map_err(|e| TarError::InvalidArchive(format!("Failed to read entry path: {e}")))?;
println!("{}", path.display());
}
}
Ok(())
}
+1
View File
@@ -5,3 +5,4 @@
pub mod create;
pub mod extract;
pub mod list;
+14 -5
View File
@@ -12,7 +12,7 @@ use uucore::error::UResult;
use uucore::format_usage;
const ABOUT: &str = "an archiving utility";
const USAGE: &str = "tar key [FILE...]\n tar {-c|-x} [-v] -f ARCHIVE [FILE...]";
const USAGE: &str = "tar key [FILE...]\n tar {-c|-t|-x} [-v] -f ARCHIVE [FILE...]";
/// Determines whether a string looks like a POSIX tar keystring.
///
@@ -162,10 +162,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
return operations::create::create_archive(archive_path, &files, verbose);
}
// Handle list operation
if matches.get_flag("list") {
let archive_path = matches.get_one::<PathBuf>("file").ok_or_else(|| {
uucore::error::USimpleError::new(64, "option requires an argument -- 'f'")
})?;
return operations::list::list_archive(archive_path, verbose);
}
// If no operation specified, show error
Err(uucore::error::USimpleError::new(
2,
"You must specify one of the '-c' or '-x' options",
"You must specify one of the '-c', '-x', or '-t' options",
))
}
@@ -179,14 +188,14 @@ pub fn uu_app() -> Command {
.disable_help_flag(true)
.args([
// Main operation modes
arg!(-c --create "Create a new archive").conflicts_with("extract"),
arg!(-c --create "Create a new archive").conflicts_with_all(["extract", "list"]),
// arg!(-d --diff "Find differences between archive and file system").alias("compare"),
// arg!(-r --append "Append files to end of archive"),
// arg!(-t --list "List contents of archive"),
arg!(-t --list "List contents of archive").conflicts_with_all(["create", "extract"]),
// arg!(-u --update "Only append files newer than copy in archive"),
arg!(-x --extract "Extract files from archive")
.alias("get")
.conflicts_with("create"),
.conflicts_with_all(["create", "list"]),
// Archive file
arg!(-f --file <ARCHIVE> "Use archive file or device ARCHIVE")
.value_parser(clap::value_parser!(PathBuf)),
+95
View File
@@ -657,3 +657,98 @@ fn test_posix_b_matches_dash_prefix_failure() {
.code_is(64)
.stderr_contains("unexpected argument '-b'");
}
// List operation tests
#[test]
fn test_list_single_file() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file.txt", "test content");
ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds();
new_ucmd!()
.args(&["-tf", "archive.tar"])
.current_dir(at.as_string())
.succeeds()
.stdout_contains("file.txt");
}
#[test]
fn test_list_multiple_files() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file1.txt", "content1");
at.write("file2.txt", "content2");
at.write("file3.txt", "content3");
ucmd.args(&["-cf", "archive.tar", "file1.txt", "file2.txt", "file3.txt"])
.succeeds();
new_ucmd!()
.args(&["-tf", "archive.tar"])
.current_dir(at.as_string())
.succeeds()
.stdout_contains("file1.txt")
.stdout_contains("file2.txt")
.stdout_contains("file3.txt");
}
#[test]
fn test_list_directory() {
let (at, mut ucmd) = at_and_ucmd!();
at.mkdir("testdir");
at.write("testdir/file1.txt", "content1");
at.mkdir("testdir/subdir");
at.write("testdir/subdir/file2.txt", "content2");
ucmd.args(&["-cf", "archive.tar", "testdir"]).succeeds();
new_ucmd!()
.args(&["-tf", "archive.tar"])
.current_dir(at.as_string())
.succeeds()
.stdout_contains("testdir")
.stdout_contains("testdir/file1.txt")
.stdout_contains("testdir/subdir")
.stdout_contains("testdir/subdir/file2.txt");
}
#[test]
fn test_list_verbose() {
let (at, mut ucmd) = at_and_ucmd!();
at.write("file.txt", "content");
ucmd.args(&["-cf", "archive.tar", "file.txt"]).succeeds();
new_ucmd!()
.args(&["-tvf", "archive.tar"])
.current_dir(at.as_string())
.succeeds()
.stdout_contains("file.txt")
.stdout_contains("7 "); // verbose output includes file size; absent from plain -t listing
}
#[test]
fn test_list_nonexistent_archive() {
new_ucmd!()
.args(&["-tf", "nonexistent.tar"])
.fails()
.code_is(2)
.stderr_contains("nonexistent.tar");
}
#[test]
fn test_list_conflicts_with_create() {
new_ucmd!()
.args(&["-ctf", "archive.tar", "file.txt"])
.fails()
.code_is(2)
.stderr_contains("cannot be used with");
}
#[test]
fn test_list_conflicts_with_extract() {
new_ucmd!()
.args(&["-xtf", "archive.tar"])
.fails()
.code_is(2)
.stderr_contains("cannot be used with");
}