Skip to content

Commit 2b27aad

Browse files
committed
tar: Implement double verbose option
Implement double verbose (-vv) option to create and extract operations. This output is in same format as already implemented in list.rs so move that to common and reuse it.
1 parent 840b16b commit 2b27aad

7 files changed

Lines changed: 149 additions & 69 deletions

File tree

src/uu/tar/src/display.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// This file is part of the uutils tar package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use chrono::{TimeZone, Utc};
7+
use std::io::Write;
8+
use std::path::Path;
9+
use uucore::fs::display_permissions_unix;
10+
11+
/// Print a verbose (ls -l style) line for an entry in a tar archive
12+
pub fn print_entry_verbose<W: Write>(
13+
mut out: W,
14+
header: &tar::Header,
15+
path: &Path,
16+
) -> std::io::Result<()> {
17+
let mode = header.mode().unwrap_or(0);
18+
let entry_type = header.entry_type();
19+
let owner = header
20+
.username()
21+
.ok()
22+
.flatten()
23+
.map(|s| s.to_owned())
24+
.unwrap_or_else(|| header.uid().unwrap_or(0).to_string());
25+
let group = header
26+
.groupname()
27+
.ok()
28+
.flatten()
29+
.map(|s| s.to_owned())
30+
.unwrap_or_else(|| header.gid().unwrap_or(0).to_string());
31+
let size = header.size().unwrap_or(0);
32+
let mtime = header.mtime().unwrap_or(0);
33+
34+
let type_char = match entry_type {
35+
tar::EntryType::Directory => 'd',
36+
tar::EntryType::Symlink => 'l',
37+
tar::EntryType::Char => 'c',
38+
tar::EntryType::Block => 'b',
39+
tar::EntryType::Fifo => 'p',
40+
_ => '-',
41+
};
42+
// Tar headers store the type separately from the mode bits, so we get the
43+
// 9-character rwx string from uucore and prepend our own type character.
44+
let perm_str = display_permissions_unix(mode, false);
45+
let permissions = format!("{type_char}{perm_str}");
46+
47+
// TODO: GNU tar displays mtime in the user's local timezone; we
48+
// currently format in UTC. Convert to local time for compatibility.
49+
let dt: chrono::DateTime<Utc> = Utc
50+
.timestamp_opt(mtime as i64, 0)
51+
.single()
52+
.unwrap_or_else(Utc::now);
53+
let date_str = dt.format("%Y-%m-%d %H:%M");
54+
55+
// TODO: use path.has_trailing_sep() when stable
56+
let path_str = path.display().to_string();
57+
let suffix = if entry_type.is_dir() && !path.ends_with(std::path::MAIN_SEPARATOR_STR) {
58+
std::path::MAIN_SEPARATOR_STR
59+
} else {
60+
""
61+
};
62+
63+
writeln!(
64+
out,
65+
"{permissions} {owner}/{group} {size:>8} {date_str} {path_str}{suffix}"
66+
)
67+
}

src/uu/tar/src/operations/create.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
use crate::display;
67
use crate::errors::TarError;
78
use std::collections::VecDeque;
89
use std::fs::{self, File};
@@ -18,15 +19,15 @@ use uucore::error::UResult;
1819
///
1920
/// * `archive_path` - Path where the tar archive should be created
2021
/// * `files` - Slice of file paths to add to the archive
21-
/// * `verbose` - Whether to print verbose output during creation
22+
/// * `verbose` - Verbosity level during creation
2223
///
2324
/// # Errors
2425
///
2526
/// Returns an error if:
2627
/// - The archive file cannot be created
2728
/// - Any input file cannot be read
2829
/// - Files cannot be added due to I/O or permission errors
29-
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UResult<()> {
30+
pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: u8) -> UResult<()> {
3031
// Create the output file
3132
let file = File::create(archive_path).map_err(|e| TarError::CannotCreateArchive {
3233
path: archive_path.to_path_buf(),
@@ -47,7 +48,17 @@ pub fn create_archive(archive_path: &Path, files: &[&Path], verbose: bool) -> UR
4748
.into());
4849
}
4950

50-
if verbose {
51+
if verbose >= 2 {
52+
for p in get_tree(path)? {
53+
let metadata = p.metadata().map_err(|e| TarError::CannotAddFile {
54+
path: p.clone(),
55+
source: e,
56+
})?;
57+
let mut header = tar::Header::new_gnu();
58+
header.set_metadata(&metadata);
59+
display::print_entry_verbose(&mut out, &header, &p).map_err(TarError::Io)?;
60+
}
61+
} else if verbose == 1 {
5162
let to_print = get_tree(path)?
5263
.iter()
5364
.map(|p| (p.is_dir(), p.display().to_string()))

src/uu/tar/src/operations/extract.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
use crate::display;
67
use crate::errors::TarError;
78
use std::fs::File;
89
use std::io::{self, BufWriter, Write};
@@ -15,15 +16,15 @@ use uucore::error::UResult;
1516
/// # Arguments
1617
///
1718
/// * `archive_path` - Path to the tar archive to extract
18-
/// * `verbose` - Whether to print verbose output during extraction
19+
/// * `verbose` - Verbosity level during extraction
1920
///
2021
/// # Errors
2122
///
2223
/// Returns an error if:
2324
/// - The archive file cannot be opened
2425
/// - The archive format is invalid
2526
/// - Files cannot be extracted due to I/O or permission errors
26-
pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
27+
pub fn extract_archive(archive_path: &Path, verbose: u8) -> UResult<()> {
2728
// Open the archive file
2829
let file = File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
2930

@@ -32,7 +33,7 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
3233
let mut out = BufWriter::new(io::stdout().lock());
3334

3435
// Extract to current directory
35-
if verbose {
36+
if verbose >= 1 {
3637
writeln!(out, "Extracting archive: {}", archive_path.display()).map_err(TarError::Io)?;
3738
}
3839

@@ -46,7 +47,9 @@ pub fn extract_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
4647
.map_err(TarError::CannotReadEntryPath)?
4748
.to_path_buf();
4849

49-
if verbose {
50+
if verbose >= 2 {
51+
display::print_entry_verbose(&mut out, entry.header(), &path).map_err(TarError::Io)?;
52+
} else if verbose == 1 {
5053
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
5154
}
5255

src/uu/tar/src/operations/list.rs

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,82 +3,28 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
use crate::display;
67
use crate::errors::TarError;
7-
use chrono::{TimeZone, Utc};
88
use std::fs::File;
99
use std::io::{self, BufWriter, Write};
1010
use std::path::Path;
1111
use tar::Archive;
1212
use uucore::error::UResult;
13-
use uucore::fs::display_permissions_unix;
1413

1514
/// List the contents of a tar archive, printing one entry per line.
16-
pub fn list_archive(archive_path: &Path, verbose: bool) -> UResult<()> {
15+
pub fn list_archive(archive_path: &Path, verbose: u8) -> UResult<()> {
1716
let file: File =
1817
File::open(archive_path).map_err(|e| TarError::from_io_error(e, archive_path))?;
1918
let mut archive = Archive::new(file);
2019
let mut out = BufWriter::new(io::stdout().lock());
2120

2221
for entry_result in archive.entries().map_err(TarError::CannotReadEntries)? {
2322
let entry = entry_result.map_err(TarError::CannotReadEntry)?;
23+
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;
2424

25-
if verbose {
26-
// Collect all header fields into owned values before borrowing entry for the path,
27-
// since both header() and path() require a borrow of entry.
28-
let (mode, entry_type, owner, group, size, mtime) = {
29-
let header = entry.header();
30-
(
31-
header.mode().unwrap_or(0),
32-
header.entry_type(),
33-
header
34-
.username()
35-
.ok()
36-
.flatten()
37-
.unwrap_or_default()
38-
.to_owned(),
39-
header
40-
.groupname()
41-
.ok()
42-
.flatten()
43-
.unwrap_or_default()
44-
.to_owned(),
45-
header.size().unwrap_or(0),
46-
header.mtime().unwrap_or(0),
47-
)
48-
};
49-
50-
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;
51-
52-
let type_char = match entry_type {
53-
tar::EntryType::Directory => 'd',
54-
tar::EntryType::Symlink => 'l',
55-
tar::EntryType::Char => 'c',
56-
tar::EntryType::Block => 'b',
57-
tar::EntryType::Fifo => 'p',
58-
_ => '-',
59-
};
60-
// Tar headers store the type separately from the mode bits, so we get the
61-
// 9-character rwx string from uucore and prepend our own type character.
62-
let perm_str = display_permissions_unix(mode, false);
63-
let permissions = format!("{type_char}{perm_str}");
64-
65-
// TODO: GNU tar displays mtime in the user's local timezone; we
66-
// currently format in UTC. Convert to local time for compatibility.
67-
let dt: chrono::DateTime<Utc> = Utc
68-
.timestamp_opt(mtime as i64, 0)
69-
.single()
70-
.unwrap_or_else(Utc::now);
71-
let date_str = dt.format("%Y-%m-%d %H:%M");
72-
73-
writeln!(
74-
out,
75-
"{permissions} {owner}/{group} {size:>8} {date_str} {}",
76-
path.display()
77-
)
78-
.map_err(TarError::Io)?;
25+
if verbose >= 1 {
26+
display::print_entry_verbose(&mut out, entry.header(), &path).map_err(TarError::Io)?;
7927
} else {
80-
let path = entry.path().map_err(TarError::CannotReadEntryPath)?;
81-
8228
writeln!(out, "{}", path.display()).map_err(TarError::Io)?;
8329
}
8430
}

src/uu/tar/src/tar.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
55

6+
mod display;
67
pub mod errors;
78
mod operations;
89

@@ -130,7 +131,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
130131
}
131132
};
132133

133-
let verbose = matches.get_flag("verbose");
134+
let verbose = matches.get_count("verbose");
134135

135136
// Handle extract operation
136137
if matches.get_flag("extract") {
@@ -204,7 +205,7 @@ pub fn uu_app() -> Command {
204205
// arg!(-j --bzip2 "Filter through bzip2"),
205206
// arg!(-J --xz "Filter through xz"),
206207
// Common options
207-
arg!(-v --verbose "Verbosely list files processed"),
208+
arg!(-v --verbose "Verbosely list files processed").action(ArgAction::Count),
208209
// arg!(-h --dereference "Follow symlinks"),
209210
// arg!(-p --"preserve-permissions" "Extract information about file permissions"),
210211
// arg!(-P --"absolute-names" "Don't strip leading '/' from file names"),
@@ -285,6 +286,13 @@ mod tests {
285286
assert_eq!(expand_posix_keystring(input), expected);
286287
}
287288

289+
#[test]
290+
fn test_expand_cvvf() {
291+
let input = osvec(&["tar", "cvvf", "archive.tar", "file.txt"]);
292+
let expected = osvec(&["tar", "-c", "-v", "-v", "-f", "archive.tar", "file.txt"]);
293+
assert_eq!(expand_posix_keystring(input), expected);
294+
}
295+
288296
#[test]
289297
fn test_expand_xf() {
290298
let input = osvec(&["tar", "xf", "archive.tar"]);

src/uu/tar/tests/test_cli.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ fn test_verbose_flag_parsing() {
2929
let result = app.try_get_matches_from(vec!["tar", "-cvf", "archive.tar", "file.txt"]);
3030
assert!(result.is_ok());
3131
let matches = result.unwrap();
32-
assert!(matches.get_flag("verbose"));
32+
assert_eq!(matches.get_count("verbose"), 1);
33+
assert!(matches.get_flag("create"));
34+
}
35+
36+
#[test]
37+
fn test_double_verbose_flag_parsing() {
38+
let app = uu_app();
39+
let result = app.try_get_matches_from(vec!["tar", "-cvvf", "archive.tar", "file.txt"]);
40+
assert!(result.is_ok());
41+
let matches = result.unwrap();
42+
assert_eq!(matches.get_count("verbose"), 2);
3343
assert!(matches.get_flag("create"));
3444
}

tests/by-util/test_tar.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
use std::path::{self, PathBuf};
77

8+
use regex::Regex;
89
use uutests::{at_and_ucmd, new_ucmd};
910

1011
/// Size of a single tar block in bytes (per POSIX specification).
@@ -145,6 +146,20 @@ fn test_create_verbose() {
145146
assert!(at.file_exists("archive.tar"));
146147
}
147148

149+
#[test]
150+
fn test_create_double_verbose() {
151+
let (at, mut ucmd) = at_and_ucmd!();
152+
at.write("file.txt", "content");
153+
at.mkdir("dir");
154+
155+
let file_regex = Regex::new(r"-.{9} .* 7 \d{4}-\d{2}-\d{2} \d{2}:\d{2} file.txt").unwrap();
156+
let dir_regex = Regex::new(r"d.{9} .* 0 \d{4}-\d{2}-\d{2} \d{2}:\d{2} dir/").unwrap();
157+
ucmd.args(&["-cvvf", "archive.tar", "file.txt", "dir"])
158+
.succeeds()
159+
.stdout_matches(&file_regex)
160+
.stdout_matches(&dir_regex);
161+
}
162+
148163
#[test]
149164
fn test_create_empty_archive_fails() {
150165
new_ucmd!()
@@ -239,6 +254,26 @@ fn test_extract_verbose() {
239254
assert!(at.file_exists("file3.txt"));
240255
}
241256

257+
#[test]
258+
fn test_extract_double_verbose() {
259+
let (at, mut ucmd) = at_and_ucmd!();
260+
at.write("file.txt", "content");
261+
at.mkdir("dir");
262+
ucmd.args(&["-cf", "archive.tar", "file.txt", "dir"])
263+
.succeeds();
264+
at.remove("file.txt");
265+
at.rmdir("dir");
266+
267+
let file_regex = Regex::new(r"-.{9} .* 7 \d{4}-\d{2}-\d{2} \d{2}:\d{2} file.txt").unwrap();
268+
let dir_regex = Regex::new(r"d.{9} .* 0 \d{4}-\d{2}-\d{2} \d{2}:\d{2} dir/").unwrap();
269+
new_ucmd!()
270+
.args(&["-xvvf", "archive.tar"])
271+
.current_dir(at.as_string())
272+
.succeeds()
273+
.stdout_matches(&file_regex)
274+
.stdout_matches(&dir_regex);
275+
}
276+
242277
#[test]
243278
fn test_extract_multiple_files() {
244279
let (at, mut ucmd) = at_and_ucmd!();

0 commit comments

Comments
 (0)