Skip to content

Commit 0b98dbf

Browse files
committed
feat: support extracting multiple files in one call
1 parent 8fdcfed commit 0b98dbf

File tree

3 files changed

+63
-31
lines changed

3 files changed

+63
-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+
Short('f') => {
184+
archives.push(parser.value()?.string()?);
185+
}
186+
Value(val) if archives.is_empty() => {
187+
archives.push(val.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 -f 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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,29 @@ fn test_examine_single_cpio_raw() -> Result<(), Box<dyn Error>> {
390390
Ok(())
391391
}
392392

393+
#[test]
394+
fn test_extract_multiple() -> Result<(), Box<dyn Error>> {
395+
let tempdir = TempDir::new()?;
396+
let mut cmd = get_command();
397+
cmd.arg("-x")
398+
.arg("-C")
399+
.arg(&tempdir.path)
400+
.arg("-v")
401+
.arg("-f")
402+
.arg("tests/single.cpio")
403+
.arg("-f")
404+
.arg("tests/shell.cpio");
405+
406+
println!("tempdir = {:?}", tempdir.path);
407+
cmd.output()?
408+
.assert_stderr(".\npath\npath/file\n.\nusr\nusr/bin\nusr/bin/sh\n")
409+
.assert_success()
410+
.assert_stdout("");
411+
assert!(tempdir.path.join("path/file").exists());
412+
assert!(tempdir.path.join("usr/bin/sh").exists());
413+
Ok(())
414+
}
415+
393416
#[test]
394417
fn test_extract_to_stdout() -> Result<(), Box<dyn Error>> {
395418
let mut cmd = get_command();

0 commit comments

Comments
 (0)