Skip to content

Commit a19566a

Browse files
committed
feat: support extracting multiple archives in one call
Support extracting multiple archives in one call by specifying them via `-F` or `--file`. Example: ``` 3cpio -x -F ARCHIVE1 -F ARCHIVE2 -F ARCHIVE3 ```
1 parent 82c70cd commit a19566a

File tree

3 files changed

+77
-31
lines changed

3 files changed

+77
-31
lines changed

man/3cpio.1.adoc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Benjamin Drung
2222
*3cpio* {*-t*|*--list*} [*-v*|*--debug*] [*-P* _LIST_] _ARCHIVE_ [_pattern_...]
2323

2424
*3cpio* {*-x*|*--extract*} [*-v*|*--debug*] [*-C* _DIR_] [*-P* _LIST_] [*-p*] [*-s* _NAME_]
25-
[*--to-stdout*] [*--force*] _ARCHIVE_ [_pattern_...]
25+
[*--to-stdout*] [*--force*] [*-F*] _ARCHIVE_ [*-F* _ARCHIVE_...] [_pattern_...]
2626

2727
*3cpio* {*-V*|*--version*}
2828

@@ -74,8 +74,10 @@ Following compression formats are supported: bzip2, gzip, lz4, lzma, lzop, xz, z
7474
matching at least one of those __pattern__s.
7575
These __pattern__s are shell wildcard patterns (see *glob*(7)).
7676

77-
*-x*, *--extract* _ARCHIVE_ [_pattern_...]::
77+
*-x*, *--extract* [-F] _ARCHIVE_ [-F _ARCHIVE_] [_pattern_...]::
7878
Extract cpio archives.
79+
Multiple archives can be specified with *-F*.
80+
The option *-F* can be omitted for the first archive.
7981
If one or more __pattern__s are supplied, extract only files
8082
matching at least one of those __pattern__s.
8183
These __pattern__s are shell wildcard patterns (see *glob*(7)).

src/main.rs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct Args {
2828
force: bool,
2929
list: bool,
3030
log_level: Level,
31-
archive: Option<String>,
31+
archives: Vec<String>,
3232
make_directories: bool,
3333
parts: Option<Ranges>,
3434
patterns: Vec<Pattern>,
@@ -58,7 +58,8 @@ fn print_help() {
5858
{executable} {{-c|--create}} [-v|--debug] [-C DIR] [--data-align BYTES] [ARCHIVE] < manifest
5959
{executable} {{-e|--examine}} [--raw] ARCHIVE
6060
{executable} {{-t|--list}} [-v|--debug] [-P LIST] ARCHIVE [pattern...]
61-
{executable} {{-x|--extract}} [-v|--debug] [-C DIR] [--make-directories] [-P LIST] [-p] [-s NAME] [--to-stdout] [--force] ARCHIVE [pattern...]
61+
{executable} {{-x|--extract}} [-v|--debug] [-C DIR] [--make-directories] [-P LIST] \
62+
[-p] [-s NAME] [--to-stdout] [--force] [-F] ARCHIVE [-F ARCHIVE...] [pattern...]
6263
6364
Optional arguments:
6465
--count Print the number of concatenated cpio archives.
@@ -103,7 +104,7 @@ fn parse_args() -> Result<Args, lexopt::Error> {
103104
let mut list = 0;
104105
let mut log_level = Level::Warning;
105106
let mut directory = ".".into();
106-
let mut archive = None;
107+
let mut archives = Vec::new();
107108
let mut make_directories = false;
108109
let mut patterns = Vec::new();
109110
let mut raw = false;
@@ -179,8 +180,11 @@ fn parse_args() -> Result<Args, lexopt::Error> {
179180
Short('x') | Long("extract") => {
180181
extract = 1;
181182
}
182-
Value(val) if archive.is_none() => {
183-
archive = Some(val.string()?);
183+
Value(val) if archives.is_empty() => {
184+
archives.push(val.string()?);
185+
}
186+
Short('F') | Long("file") => {
187+
archives.push(parser.value()?.string()?);
184188
}
185189
Value(val) => arguments.push(val.string()?),
186190
_ => return Err(arg.unexpected()),
@@ -210,9 +214,12 @@ fn parse_args() -> Result<Args, lexopt::Error> {
210214
}
211215
}
212216

213-
if create != 1 && archive.is_none() {
217+
if create != 1 && archives.is_empty() {
214218
return Err("missing argument ARCHIVE".into());
215219
}
220+
if extract != 1 && archives.len() > 1 {
221+
return Err("specifying multiple --file=ARCHIVE is only supported for --extract".into());
222+
}
216223

217224
Ok(Args {
218225
count: count == 1,
@@ -224,7 +231,7 @@ fn parse_args() -> Result<Args, lexopt::Error> {
224231
force,
225232
list: list == 1,
226233
log_level,
227-
archive,
234+
archives,
228235
make_directories,
229236
parts,
230237
patterns,
@@ -342,7 +349,7 @@ fn main() -> ExitCode {
342349

343350
if args.create {
344351
let mut archive = None;
345-
if let Some(path) = args.archive.as_ref() {
352+
if let Some(path) = args.archives.first() {
346353
archive = match File::create(path) {
347354
Ok(f) => Some(f),
348355
Err(e) => {
@@ -368,7 +375,7 @@ fn main() -> ExitCode {
368375
_ => {
369376
eprintln!(
370377
"{executable}: Error: Failed to create '{}': {error}",
371-
args.archive.unwrap_or("cpio on stdout".into()),
378+
args.archives.first().unwrap_or(&"cpio on stdout".into()),
372379
);
373380
return ExitCode::FAILURE;
374381
}
@@ -377,16 +384,17 @@ fn main() -> ExitCode {
377384
return ExitCode::SUCCESS;
378385
};
379386

380-
let archive = match File::open(args.archive.as_ref().unwrap()) {
381-
Ok(f) => f,
382-
Err(e) => {
383-
eprintln!(
384-
"{executable}: Error: Failed to open '{}': {e}",
385-
args.archive.unwrap(),
386-
);
387-
return ExitCode::FAILURE;
388-
}
389-
};
387+
let mut opened_archives = Vec::new();
388+
for path in &args.archives {
389+
let archive = match File::open(path) {
390+
Ok(f) => f,
391+
Err(e) => {
392+
eprintln!("{executable}: Error: Failed to open '{path}': {e}");
393+
return ExitCode::FAILURE;
394+
}
395+
};
396+
opened_archives.push((path.clone(), archive));
397+
}
390398

391399
if args.extract && !args.to_stdout {
392400
if let Err(e) = create_and_set_current_dir(&args.directory, args.force) {
@@ -402,16 +410,15 @@ fn main() -> ExitCode {
402410
return ExitCode::FAILURE;
403411
}
404412
};
405-
let (operation, result) = operate_on_archive(archive, &args, &cwd, &mut logger);
406-
if let Err(e) = result {
407-
match e.kind() {
408-
ErrorKind::BrokenPipe => {}
409-
_ => {
410-
eprintln!(
411-
"{executable}: Error: Failed to {operation} of '{}': {e}",
412-
args.archive.as_deref().unwrap(),
413-
);
414-
return ExitCode::FAILURE;
413+
for (path, archive) in opened_archives {
414+
let (operation, result) = operate_on_archive(archive, &args, &cwd, &mut logger);
415+
if let Err(e) = result {
416+
match e.kind() {
417+
ErrorKind::BrokenPipe => break,
418+
_ => {
419+
eprintln!("{executable}: Error: Failed to {operation} of '{path}': {e}");
420+
return ExitCode::FAILURE;
421+
}
415422
}
416423
}
417424
}

tests/cli.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ fn test_create_missing_path() -> Result<(), Box<dyn Error>> {
314314
Ok(())
315315
}
316316

317+
#[test]
318+
fn test_create_multiple() -> Result<(), Box<dyn Error>> {
319+
let mut cmd = get_command();
320+
cmd.args(["--create", "-F", "/tmp/first", "-F", "/tmp/second"]);
321+
cmd.output()?
322+
.assert_failure(2)
323+
.assert_stderr_contains(
324+
"Error: specifying multiple --file=ARCHIVE is only supported for --extract",
325+
)
326+
.assert_stdout("");
327+
Ok(())
328+
}
329+
317330
#[test]
318331
fn test_create_uncompressed_plus_zstd_on_stdout() -> Result<(), Box<dyn Error>> {
319332
let mut cmd = get_command();
@@ -428,6 +441,30 @@ fn test_examine_single_cpio_raw() -> Result<(), Box<dyn Error>> {
428441
Ok(())
429442
}
430443

444+
#[test]
445+
fn test_extract_multiple() -> Result<(), Box<dyn Error>> {
446+
let tempdir = TempDir::new()?;
447+
let mut cmd = get_command();
448+
cmd.arg("-x")
449+
.arg("-C")
450+
.arg(&tempdir.path)
451+
.arg("-v")
452+
.arg("-F")
453+
.arg("tests/single.cpio")
454+
.arg("--file")
455+
.arg("tests/shell.cpio");
456+
457+
println!("tempdir = {:?}", tempdir.path);
458+
cmd.output()?
459+
.assert_stderr(".\npath\npath/file\n.\nusr\nusr/bin\nusr/bin/ash\nusr/bin/sh\n")
460+
.assert_success()
461+
.assert_stdout("");
462+
assert!(tempdir.path.join("path/file").exists());
463+
assert!(tempdir.path.join("usr/bin/ash").exists());
464+
assert!(tempdir.path.join("usr/bin/sh").exists());
465+
Ok(())
466+
}
467+
431468
#[test]
432469
fn test_extract_to_stdout() -> Result<(), Box<dyn Error>> {
433470
let mut cmd = get_command();

0 commit comments

Comments
 (0)