diff --git a/src/cli.rs b/src/cli.rs index bef64db61..811346ed1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,7 +10,7 @@ use clap::Parser; use fs_err as fs; use once_cell::sync::OnceCell; -use crate::{Opts, QuestionPolicy, Subcommand}; +use crate::{error::FinalError, Opts, QuestionPolicy, Subcommand}; /// Whether to enable accessible output (removes info output and reduces other /// output, removes visual markers like '[' and ']'). @@ -26,6 +26,13 @@ impl Opts { pub fn parse_args() -> crate::Result<(Self, QuestionPolicy)> { let mut opts = Self::parse(); + if opts.format.as_ref().map(String::is_empty).unwrap_or(false) { + let error = FinalError::with_title("Given --format flag is empty") + .hint("Try passing a supported extension, like --format tar.gz"); + + return Err(error.into()); + } + ACCESSIBLE.set(opts.accessible).unwrap(); let (Subcommand::Compress { files, .. } diff --git a/src/commands.rs b/src/commands.rs index 5de074308..c7b18102e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -46,7 +46,8 @@ fn represents_several_files(files: &[PathBuf]) -> bool { pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> { match args.cmd { Subcommand::Compress { mut files, output: output_path } => { - // If the output_path file exists and is the same as some of the input files, warn the user and skip those inputs (in order to avoid compression recursion) + // If the output_path file exists and is the same as some of the input files, warn the + // user and skip those inputs (in order to avoid compression recursion) if output_path.exists() { clean_input_files_if_needed(&mut files, &fs::canonicalize(&output_path)?); } @@ -56,7 +57,15 @@ pub fn run(args: Opts, question_policy: QuestionPolicy) -> crate::Result<()> { } // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] - let mut formats = extension::extensions_from_path(&output_path); + let mut formats = if let Some(format) = args.format { + extension::from_format_text(&format).ok_or_else(|| { + FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) + .detail("Supplied compression format is not valid") + .hint("Check --help for all supported formats") + }) + } else { + Ok(extension::extensions_from_path(&output_path)) + }?; if formats.is_empty() { let error = FinalError::with_title(format!("Cannot compress to '{}'.", to_utf(&output_path))) diff --git a/src/extension.rs b/src/extension.rs index e8807d00e..744e6ed67 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -102,8 +102,54 @@ impl fmt::Display for CompressionFormat { } } -// use crate::extension::CompressionFormat::*; -// +/// Returns the list of compression formats that correspond to the given extension text. +/// +/// # Examples: +/// - `"tar" => Some(&[Tar])` +/// - `"tgz" => Some(&[Tar, Gzip])` +/// +/// Note that the text given as input should not contain any dots, otherwise, None will be returned. +pub fn compression_formats_from_text(extension: &str) -> Option<&'static [CompressionFormat]> { + match extension { + "tar" => Some(&[Tar]), + "tgz" => Some(&[Tar, Gzip]), + "tbz" | "tbz2" => Some(&[Tar, Bzip]), + "tlz4" => Some(&[Tar, Lz4]), + "txz" | "tlzma" => Some(&[Tar, Lzma]), + "tsz" => Some(&[Tar, Snappy]), + "tzst" => Some(&[Tar, Zstd]), + "zip" => Some(&[Zip]), + "bz" | "bz2" => Some(&[Bzip]), + "gz" => Some(&[Gzip]), + "lz4" => Some(&[Lz4]), + "xz" | "lzma" => Some(&[Lzma]), + "sz" => Some(&[Snappy]), + "zst" => Some(&[Zstd]), + _ => None, + } +} + +/// Grab a user given format, and parse it. +/// +/// # Examples: +/// - `"tar.gz" => Extension { ... [Tar, Gz] }` +/// - `".tar.gz" => Extension { ... [Tar, Gz] }` +pub fn from_format_text(format: &str) -> Option> { + let mut extensions = vec![]; + + // Ignore all dots, use filter to ignore dots at first and last char leaving empty pieces + let extension_pieces = format.split('.').filter(|piece| !piece.is_empty()); + for piece in extension_pieces { + let extension = match compression_formats_from_text(piece) { + Some(formats) => Extension::new(formats, piece), + None => return None, + }; + extensions.push(extension); + } + + extensions.reverse(); + Some(extensions) +} /// Extracts extensions from a path, /// return both the remaining path and the list of extension objects @@ -120,25 +166,10 @@ pub fn separate_known_extensions_from_name(mut path: &Path) -> (&Path, Vec &[Tar], - "tgz" => &[Tar, Gzip], - "tbz" | "tbz2" => &[Tar, Bzip], - "tlz4" => &[Tar, Lz4], - "txz" | "tlzma" => &[Tar, Lzma], - "tsz" => &[Tar, Snappy], - "tzst" => &[Tar, Zstd], - "zip" => &[Zip], - "bz" | "bz2" => &[Bzip], - "gz" => &[Gzip], - "lz4" => &[Lz4], - "xz" | "lzma" => &[Lzma], - "sz" => &[Snappy], - "zst" => &[Zstd], - _ => break, + let extension = match compression_formats_from_text(extension) { + Some(formats) => Extension::new(formats, extension), + None => break, }; - - let extension = Extension::new(formats, extension); extensions.push(extension); // Update for the next iteration diff --git a/src/opts.rs b/src/opts.rs index ad082518a..95e5b2767 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -20,6 +20,10 @@ pub struct Opts { pub no: bool, /// Activate accessibility mode, reducing visual noise + #[clap(short = 'f', long)] + pub format: Option, + + /// Ignore autodetection, specifying #[clap(short = 'A', long, env = "ACCESSIBLE")] pub accessible: bool,