this repo has no description
1pub mod backends;
2pub mod progress;
3pub mod test_utils;
4pub mod utils;
5
6use backends::*;
7use clap::{Parser, Subcommand};
8use is_terminal::IsTerminal;
9use std::io;
10use std::path::{Path, PathBuf};
11use std::{io, vec};
12use utils::*;
13
14/// A compression multi-tool
15#[derive(Parser, Debug)]
16#[command(author, version, about, long_about = None)]
17struct CmprssArgs {
18 /// Format
19 #[command(subcommand)]
20 format: Option<Format>,
21
22 // Base arguments for the non-subcommand behavior
23 #[clap(flatten)]
24 pub base_args: CommonArgs,
25}
26#[derive(Subcommand, Debug)]
27enum Format {
28 /// tar archive format
29 Tar(TarArgs),
30
31 /// gzip compression
32 #[clap(visible_alias = "gz")]
33 Gzip(GzipArgs),
34
35 /// xz compression
36 Xz(XzArgs),
37
38 /// bzip2 compression
39 #[clap(visible_alias = "bz2")]
40 Bzip2(Bzip2Args),
41
42 /// zip archive format
43 Zip(ZipArgs),
44
45 /// zstd compression
46 #[clap(visible_alias = "zst")]
47 Zstd(ZstdArgs),
48
49 /// lz4 compression
50 Lz4(Lz4Args),
51}
52
53/// Get the input filename or return a default file
54/// This file will be used to generate the output filename
55fn get_input_filename(input: &CmprssInput) -> Result<&Path, io::Error> {
56 match input {
57 CmprssInput::Path(paths) => match paths.first() {
58 Some(path) => Ok(path),
59 None => Err(io::Error::new(
60 io::ErrorKind::Other,
61 "error: no input specified",
62 )),
63 },
64 CmprssInput::Pipe(_) => Ok(Path::new("archive")),
65 CmprssInput::Reader(_) => Ok(Path::new("piped_data")),
66 }
67}
68
69#[derive(Debug, PartialEq, Clone, Copy)]
70enum Action {
71 Compress,
72 Extract,
73 Unknown,
74}
75
76/// Defines a single compress/extract action to take.
77#[derive(Debug)]
78struct Job {
79 compressor: Box<dyn Compressor>,
80 input: CmprssInput,
81 output: CmprssOutput,
82 action: Action,
83}
84
85/// Get a compressor from a filename
86fn get_compressor_from_filename(filename: &Path) -> Option<Box<dyn Compressor>> {
87 // Prioritize checking for multi-level formats first
88 if let Some(filename_str) = filename.to_str() {
89 let parts: Vec<&str> = filename_str.split('.').collect();
90 // A potential multi-level format like "archive.tar.gz" will have at least 3 parts
91 if parts.len() >= 3 {
92 // Get all available single compressors for matching extensions
93 let single_compressors: Vec<Box<dyn Compressor>> = vec![
94 Box::<Tar>::default(),
95 Box::<Gzip>::default(),
96 Box::<Xz>::default(),
97 Box::<Bzip2>::default(),
98 Box::<Zip>::default(),
99 Box::<Zstd>::default(),
100 Box::<Lz4>::default(),
101 ];
102
103 // Get extensions in reverse order (from right to left, e.g., "gz", then "tar")
104 let mut extensions: Vec<String> = Vec::new();
105 for i in 1..parts.len() {
106 // Iterate from the last extension backwards
107 // Stop before including the base filename part if it's just "filename.gz" (parts.len() would be 2)
108 // This loop is for parts.len() >= 3, ensuring we look at actual extensions
109 if parts.len() - i > 0 {
110 // Ensure we don't go out of bounds for the base filename part
111 extensions.push(parts[parts.len() - i].to_string());
112 } else {
113 break; // Should not happen if parts.len() >=3 and i starts at 1
114 }
115 }
116
117 let mut compressor_types: Vec<String> = Vec::new();
118 for ext_part in &extensions {
119 // e.g., ext_part is "gz", then "tar"
120 let mut found_match = false;
121 for sc in &single_compressors {
122 if sc.extension() == ext_part || sc.name() == ext_part {
123 compressor_types.push(sc.name().to_string());
124 found_match = true;
125 break;
126 }
127 }
128 if !found_match {
129 // If any extension part is not recognized, this is not a valid multi-level chain we know.
130 // Clear types and break, so we can fall back to simple single extension check.
131 compressor_types.clear();
132 break;
133 }
134 }
135
136 // If we successfully identified a chain of known compressor types:
137 // compressor_types would be e.g. ["gzip", "tar"] (outermost to innermost)
138 if !compressor_types.is_empty() {
139 // MultiLevelCompressor::from_names expects innermost to outermost.
140 compressor_types.reverse(); // e.g., ["tar", "gzip"]
141 return Some(create_multi_level_compressor(&compressor_types));
142 }
143 // If compressor_types is empty here, it means the multi-level parse failed (e.g. "file.foo.bar" with unknown foo/bar)
144 // or an unknown extension was found in the chain. We'll fall through to single extension check.
145 }
146 }
147
148 // Fallback: If not a recognized multi-level format, or if fewer than 3 parts (e.g. "file.gz"),
149 // try matching a single known compressor extension.
150 let single_compressors: Vec<Box<dyn Compressor>> = vec![
151 Box::<Tar>::default(),
152 Box::<Gzip>::default(),
153 Box::<Xz>::default(),
154 Box::<Bzip2>::default(),
155 Box::<Zip>::default(),
156 Box::<Zstd>::default(),
157 Box::<Lz4>::default(),
158 ];
159
160 // Check if file extension matches any known format
161 // This is now a fallback.
162 // Ensure this doesn't misinterpret "foo.tar.gz" as just "gz" if multi-level check failed for some reason
163 // A more robust check here might be to see if filename *only* ends with .ext and not .something_else.ext
164 // For now, the standard check is:
165 if let Some(filename_str) = filename.to_str() {
166 for sc in single_compressors {
167 // A simple "ends_with" can be problematic for "file.tar.gz" vs "file.gz"
168 // We need to be more specific. The extension should be exactly the compressor's extension.
169 let expected_extension = format!(".{}", sc.extension());
170 if filename_str.ends_with(&expected_extension) {
171 // Further check: ensure it's not something like ".tar.gz" being matched by ".gz"
172 // if we want to be super sure, but the multi-level check should catch .tar.gz first.
173 // A simple way: if it ends with ".tar.gz", Gzip (gz) should not match here IF Tar (tar) also exists.
174 // The current structure relies on multi-level being caught first.
175 // If multi-level parsing failed, then we check single extensions.
176 // Example: "archive.gz" -> Gzip
177 // Example: "archive.tar" -> Tar
178 // Example: "archive.unknown.gz" -> Multi-level fails, then Gzip matches.
179 return Some(sc);
180 }
181 }
182 }
183 None
184}
185
186/// Create a MultiLevelCompressor from a list of compressor types
187fn create_multi_level_compressor(compressor_types: &[String]) -> Box<dyn Compressor> {
188 // Create a MultiLevelCompressor from the list of compressor types
189 match MultiLevelCompressor::from_names(compressor_types) {
190 Ok(multi) => Box::new(multi),
191 Err(_) => {
192 // Fallback to the first compressor if there's an error
193 match compressor_types[0].as_str() {
194 "tar" => Box::<Tar>::default(),
195 "gzip" | "gz" => Box::<Gzip>::default(),
196 "xz" => Box::<Xz>::default(),
197 "bzip2" | "bz2" => Box::<Bzip2>::default(),
198 "zip" => Box::<Zip>::default(),
199 "zstd" | "zst" => Box::<Zstd>::default(),
200 "lz4" => Box::<Lz4>::default(),
201 _ => Box::<Tar>::default(), // Default to tar if unknown
202 }
203 }
204 }
205}
206
207/// Convert an input path into a Path
208fn get_path(input: &str) -> Option<PathBuf> {
209 let path = PathBuf::from(input);
210 if !path.try_exists().unwrap_or(false) {
211 return None;
212 }
213 Some(path)
214}
215
216/// Guess compressor/action from the two filenames
217/// The compressor may already be given
218fn guess_from_filenames(
219 input: &[PathBuf],
220 output: &Path,
221 compressor: Option<Box<dyn Compressor>>,
222) -> (Option<Box<dyn Compressor>>, Action) {
223 if input.len() != 1 {
224 if let Some(guessed_compressor) = get_compressor_from_filename(output) {
225 return (Some(guessed_compressor), Action::Compress);
226 }
227
228 // Check if output is a directory - this is likely an extraction
229 if output.is_dir() {
230 // Try to determine compressor from the input file's extension(s)
231 if let Some(input_path) = input.first() {
232 if let Some(guessed_compressor) = get_compressor_from_filename(input_path) {
233 return (Some(guessed_compressor), Action::Extract);
234 }
235 }
236 }
237
238 // In theory we could be extracting multiple files to a directory
239 // We'll fail somewhere else if that's not the case
240 return (compressor, Action::Extract);
241 }
242 let input = input.first().unwrap();
243
244 let guessed_compressor = get_compressor_from_filename(output);
245 let guessed_extractor = get_compressor_from_filename(input);
246 let guessed_compressor_name = if let Some(c) = &guessed_compressor {
247 c.name()
248 } else {
249 ""
250 };
251 let guessed_extractor_name = if let Some(e) = &guessed_extractor {
252 e.name()
253 } else {
254 ""
255 };
256
257 if let Some(c) = &compressor {
258 if guessed_compressor_name == c.name() {
259 return (compressor, Action::Compress);
260 } else if guessed_extractor_name == c.name() {
261 return (compressor, Action::Extract);
262 } else {
263 // Default to compressing
264 return (compressor, Action::Compress);
265 }
266 }
267
268 match (guessed_compressor, guessed_extractor) {
269 (None, None) => (None, Action::Unknown),
270 (Some(c), None) => (Some(c), Action::Compress),
271 (None, Some(e)) => (Some(e), Action::Extract),
272 (Some(c), Some(e)) => {
273 if c.name() == e.name() {
274 // Same format for input and output, can't decide
275 if output.is_dir() {
276 // If output is a directory, we're probably extracting
277 return (Some(e), Action::Extract);
278 }
279 return (Some(c), Action::Unknown);
280 }
281
282 // Compare the input and output extensions to see if one has an extra extension
283 let input_file = input.file_name().unwrap().to_str().unwrap();
284 let input_ext = input.extension().unwrap_or_default();
285 let output_file = output.file_name().unwrap().to_str().unwrap();
286 let output_ext = output.extension().unwrap_or_default();
287 let guessed_output = input_file.to_string() + "." + output_ext.to_str().unwrap();
288 let guessed_input = output_file.to_string() + "." + input_ext.to_str().unwrap();
289
290 if guessed_output == output_file {
291 (Some(c), Action::Compress)
292 } else if guessed_input == input_file {
293 (Some(e), Action::Extract)
294 } else if output.is_dir() {
295 // If output is a directory, we're probably extracting
296 (Some(e), Action::Extract)
297 } else {
298 (None, Action::Unknown)
299 }
300 }
301 }
302}
303
304/// Parse the common args and determine the details of the job requested
305fn get_job(
306 compressor: Option<Box<dyn Compressor>>,
307 common_args: &CommonArgs,
308) -> Result<Job, io::Error> {
309 let mut compressor = compressor;
310 let mut action = {
311 if common_args.compress {
312 Action::Compress
313 } else if common_args.extract || common_args.decompress {
314 Action::Extract
315 } else {
316 Action::Unknown
317 }
318 };
319
320 let mut inputs = Vec::new();
321 if let Some(in_file) = &common_args.input {
322 match get_path(in_file) {
323 Some(path) => inputs.push(path),
324 None => {
325 return Err(io::Error::new(
326 io::ErrorKind::Other,
327 "Specified input path does not exist",
328 ));
329 }
330 }
331 }
332
333 let mut output = match &common_args.output {
334 Some(output) => {
335 let path = Path::new(output);
336 if path.try_exists()? && !path.is_dir() {
337 // Output path exists, bail out
338 return Err(io::Error::new(
339 io::ErrorKind::Other,
340 "Specified output path already exists",
341 ));
342 }
343 Some(path)
344 }
345 None => None,
346 };
347
348 // Process the io_list, check if there is an output first
349 let mut io_list = common_args.io_list.clone();
350 if output.is_none() {
351 if let Some(possible_output) = common_args.io_list.last() {
352 let path = Path::new(possible_output);
353 if !path.try_exists()? {
354 // Use the given path if it doesn't exist
355 output = Some(path);
356 io_list.pop();
357 } else if path.is_dir() {
358 match action {
359 Action::Compress => {
360 // A directory can potentially be a target output location or
361 // an input, for now assume it is an input.
362 }
363 Action::Extract => {
364 // Can extract to a directory, and it wouldn't make any sense as an input
365 output = Some(path);
366 io_list.pop();
367 }
368 _ => {
369 // TODO: don't know if this is an input or output, assume we're compressing this directory
370 // This does cause problems for inferencing "cat archive.tar | cmprss tar ."
371 // Probably need to add some special casing
372 }
373 };
374 } else {
375 // TODO: check for scenarios where we want to append to an existing archive
376 }
377 }
378 }
379
380 // Validate the specified inputs
381 // Everything in the io_list should be an input
382 for input in &io_list {
383 if let Some(path) = get_path(input) {
384 inputs.push(path);
385 } else {
386 return Err(io::Error::new(
387 io::ErrorKind::Other,
388 "Specified input path does not exist",
389 ));
390 }
391 }
392
393 // Fallback to stdin/stdout if we're missing files
394 let cmprss_input = match inputs.is_empty() {
395 true => {
396 if !std::io::stdin().is_terminal()
397 && !&common_args.ignore_pipes
398 && !&common_args.ignore_stdin
399 {
400 CmprssInput::Pipe(std::io::stdin())
401 } else {
402 return Err(io::Error::new(io::ErrorKind::Other, "No specified input"));
403 }
404 }
405 false => CmprssInput::Path(inputs),
406 };
407
408 let cmprss_output = match output {
409 Some(path) => CmprssOutput::Path(path.to_path_buf()),
410 None => {
411 if !std::io::stdout().is_terminal()
412 && !&common_args.ignore_pipes
413 && !&common_args.ignore_stdout
414 {
415 CmprssOutput::Pipe(std::io::stdout())
416 } else {
417 match action {
418 Action::Compress => {
419 if compressor.is_none() {
420 return Err(io::Error::new(
421 io::ErrorKind::Other,
422 "Must specify a compressor",
423 ));
424 }
425 CmprssOutput::Path(PathBuf::from(
426 compressor
427 .as_ref()
428 .unwrap()
429 .default_compressed_filename(get_input_filename(&cmprss_input)?),
430 ))
431 }
432 Action::Extract => {
433 if compressor.is_none() {
434 compressor =
435 get_compressor_from_filename(get_input_filename(&cmprss_input)?);
436 if compressor.is_none() {
437 return Err(io::Error::new(
438 io::ErrorKind::Other,
439 "Must specify a compressor",
440 ));
441 }
442 }
443 CmprssOutput::Path(PathBuf::from(
444 compressor
445 .as_ref()
446 .unwrap()
447 .default_extracted_filename(get_input_filename(&cmprss_input)?),
448 ))
449 }
450 Action::Unknown => {
451 if compressor.is_none() {
452 // Can still work if the input is an archive
453 compressor =
454 get_compressor_from_filename(get_input_filename(&cmprss_input)?);
455 if compressor.is_none() {
456 return Err(io::Error::new(
457 io::ErrorKind::Other,
458 "Must specify a compressor",
459 ));
460 }
461 action = Action::Extract;
462 CmprssOutput::Path(PathBuf::from(
463 compressor
464 .as_ref()
465 .unwrap()
466 .default_extracted_filename(get_input_filename(&cmprss_input)?),
467 ))
468 } else {
469 // We know the compressor, does the input have the same extension?
470 if let Some(compressor_from_input) =
471 get_compressor_from_filename(get_input_filename(&cmprss_input)?)
472 {
473 if compressor.as_ref().unwrap().name()
474 == compressor_from_input.name()
475 {
476 action = Action::Extract;
477 CmprssOutput::Path(PathBuf::from(
478 compressor.as_ref().unwrap().default_extracted_filename(
479 get_input_filename(&cmprss_input)?,
480 ),
481 ))
482 } else {
483 action = Action::Compress;
484 CmprssOutput::Path(PathBuf::from(
485 compressor.as_ref().unwrap().default_compressed_filename(
486 get_input_filename(&cmprss_input)?,
487 ),
488 ))
489 }
490 } else {
491 action = Action::Compress;
492 CmprssOutput::Path(PathBuf::from(
493 compressor.as_ref().unwrap().default_compressed_filename(
494 get_input_filename(&cmprss_input)?,
495 ),
496 ))
497 }
498 }
499 }
500 }
501 }
502 }
503 };
504
505 // If we don't have the compressor/action, we can attempt to infer
506 if compressor.is_none() || action == Action::Unknown {
507 match action {
508 Action::Compress => {
509 // Look at the output name
510 if let CmprssOutput::Path(path) = &cmprss_output {
511 compressor = get_compressor_from_filename(path);
512 }
513 }
514 Action::Extract => {
515 // Look at the input name
516 if let CmprssInput::Path(paths) = &cmprss_input {
517 if paths.len() != 1 {
518 // When extracting, we expect a single input file
519 return Err(io::Error::new(
520 io::ErrorKind::Other,
521 "Expected a single archive to extract",
522 ));
523 }
524 compressor = get_compressor_from_filename(paths.first().unwrap());
525
526 // If we still couldn't guess the compressor, try harder with multi-level extraction
527 if compressor.is_none() && paths.len() == 1 {
528 if let Some(filename_str) = paths.first().unwrap().to_str() {
529 // Try to parse multi-level formats (e.g., tar.gz)
530 let parts: Vec<&str> = filename_str.split('.').collect();
531 if parts.len() >= 3 {
532 // Get all available compressors
533 let compressors: Vec<Box<dyn Compressor>> = vec![
534 Box::<Tar>::default(),
535 Box::<Gzip>::default(),
536 Box::<Xz>::default(),
537 Box::<Bzip2>::default(),
538 Box::<Zip>::default(),
539 Box::<Zstd>::default(),
540 Box::<Lz4>::default(),
541 ];
542
543 // Get extensions in reverse order (from right to left)
544 let mut extensions: Vec<String> = Vec::new();
545 for i in 1..parts.len() {
546 extensions.push(parts[parts.len() - i].to_string());
547 }
548
549 // Try to find a compressor for each extension
550 let mut compressor_types: Vec<String> = Vec::new();
551 for ext in &extensions {
552 for compressor in &compressors {
553 if compressor.extension() == ext || compressor.name() == ext
554 {
555 compressor_types.push(compressor.name().to_string());
556 break;
557 }
558 }
559 }
560
561 // If we found compressor types, create a MultiLevelCompressor
562 if !compressor_types.is_empty() {
563 compressor =
564 Some(create_multi_level_compressor(&compressor_types));
565 }
566 }
567 }
568 }
569 }
570 }
571 Action::Unknown => match (&cmprss_input, &cmprss_output) {
572 (CmprssInput::Path(paths), CmprssOutput::Path(path)) => {
573 // Special case: if output is a directory, assume we're extracting
574 if path.is_dir() && paths.len() == 1 {
575 // For extraction to directory, try to determine compressor from input file
576 compressor = get_compressor_from_filename(paths.first().unwrap());
577 action = Action::Extract;
578
579 // If no compressor was found, try harder with multi-level detection
580 if compressor.is_none() {
581 if let Some(filename_str) = paths.first().unwrap().to_str() {
582 // Try to parse multi-level formats (e.g., tar.gz)
583 let parts: Vec<&str> = filename_str.split('.').collect();
584 if parts.len() >= 3 {
585 // Get all available compressors
586 let compressors: Vec<Box<dyn Compressor>> = vec![
587 Box::<Tar>::default(),
588 Box::<Gzip>::default(),
589 Box::<Xz>::default(),
590 Box::<Bzip2>::default(),
591 Box::<Zip>::default(),
592 Box::<Zstd>::default(),
593 Box::<Lz4>::default(),
594 ];
595
596 // Get extensions in reverse order (from right to left)
597 let mut extensions: Vec<String> = Vec::new();
598 for i in 1..parts.len() {
599 extensions.push(parts[parts.len() - i].to_string());
600 }
601
602 // Try to find a compressor for each extension
603 let mut compressor_types: Vec<String> = Vec::new();
604 for ext in &extensions {
605 for compressor in &compressors {
606 if compressor.extension() == ext
607 || compressor.name() == ext
608 {
609 compressor_types
610 .push(compressor.name().to_string());
611 break;
612 }
613 }
614 }
615
616 // If we found compressor types, create a MultiLevelCompressor
617 if !compressor_types.is_empty() {
618 compressor =
619 Some(create_multi_level_compressor(&compressor_types));
620 }
621 }
622 }
623
624 // If we still couldn't determine compressor, fail with a clear message
625 if compressor.is_none() {
626 return Err(io::Error::new(
627 io::ErrorKind::Other,
628 format!(
629 "Couldn't determine how to extract {:?}",
630 paths.first().unwrap()
631 ),
632 ));
633 }
634 }
635 } else {
636 let (guessed_compressor, guessed_action) =
637 guess_from_filenames(paths, path, compressor);
638 compressor = guessed_compressor;
639 action = guessed_action;
640 }
641 }
642 (CmprssInput::Path(paths), CmprssOutput::Pipe(_)) => {
643 if compressor.is_none() {
644 if paths.len() != 1 {
645 return Err(io::Error::new(
646 io::ErrorKind::Other,
647 "Expected a single input file for piping to stdout",
648 ));
649 }
650 compressor = get_compressor_from_filename(paths.first().unwrap());
651 if compressor.is_some() {
652 action = Action::Extract;
653 } else {
654 return Err(io::Error::new(
655 io::ErrorKind::Other,
656 "Can't guess compressor to use",
657 ));
658 }
659 } else if let Some(c) = get_compressor_from_filename(paths.first().unwrap()) {
660 if compressor.as_ref().unwrap().name() == c.name() {
661 action = Action::Extract;
662 } else {
663 action = Action::Compress;
664 }
665 } else {
666 action = Action::Compress;
667 }
668 }
669 (CmprssInput::Pipe(_), CmprssOutput::Path(path)) => {
670 if compressor.is_none() {
671 compressor = get_compressor_from_filename(path);
672 if compressor.is_some() {
673 action = Action::Compress;
674 } else {
675 return Err(io::Error::new(
676 io::ErrorKind::Other,
677 "Can't guess compressor to use",
678 ));
679 }
680 } else if compressor.as_ref().unwrap().name()
681 == get_compressor_from_filename(path).unwrap().name()
682 {
683 action = Action::Compress;
684 } else {
685 action = Action::Extract;
686 }
687 }
688 (CmprssInput::Pipe(_), CmprssOutput::Pipe(_)) => {
689 action = Action::Compress;
690 }
691 // Handle all Writer output cases
692 (_, CmprssOutput::Writer(_)) => {
693 // Writer outputs are only supported in multi-level compression
694 // In main.rs we'll assume compression
695 action = Action::Compress;
696 }
697 // Handle all Reader input cases
698 (&CmprssInput::Reader(_), _) => {
699 // For Reader input, we'll assume extraction
700 action = Action::Extract;
701 }
702 },
703 }
704 }
705
706 if compressor.is_none() {
707 return Err(io::Error::new(
708 io::ErrorKind::Other,
709 "Could not determine compressor to use",
710 ));
711 }
712 if action == Action::Unknown {
713 return Err(io::Error::new(
714 io::ErrorKind::Other,
715 "Could not determine action to take",
716 ));
717 }
718
719 Ok(Job {
720 compressor: compressor.unwrap(),
721 input: cmprss_input,
722 output: cmprss_output,
723 action,
724 })
725}
726
727fn command(compressor: Option<Box<dyn Compressor>>, args: &CommonArgs) -> Result<(), io::Error> {
728 let job = get_job(compressor, args)?;
729
730 match job.action {
731 Action::Compress => job.compressor.compress(job.input, job.output)?,
732 Action::Extract => job.compressor.extract(job.input, job.output)?,
733 _ => {
734 return Err(io::Error::new(
735 io::ErrorKind::Other,
736 "Unknown action requested",
737 ));
738 }
739 };
740
741 Ok(())
742}
743
744fn main() {
745 let args = CmprssArgs::parse();
746 match args.format {
747 Some(Format::Tar(a)) => command(Some(Box::new(Tar::new(&a))), &a.common_args),
748 Some(Format::Gzip(a)) => command(Some(Box::new(Gzip::new(&a))), &a.common_args),
749 Some(Format::Xz(a)) => command(Some(Box::new(Xz::new(&a))), &a.common_args),
750 Some(Format::Bzip2(a)) => command(Some(Box::new(Bzip2::new(&a))), &a.common_args),
751 Some(Format::Zip(a)) => command(Some(Box::new(Zip::new(&a))), &a.common_args),
752 Some(Format::Zstd(a)) => command(Some(Box::new(Zstd::new(&a))), &a.common_args),
753 Some(Format::Lz4(a)) => command(Some(Box::new(Lz4::new(&a))), &a.common_args),
754 _ => command(None, &args.base_args),
755 }
756 .unwrap_or_else(|e| {
757 eprintln!("ERROR(cmprss): {}", e);
758 std::process::exit(1);
759 });
760}