Merge pull request #341 from figsoda/format

add --format option
This commit is contained in:
João Marcos Bezerra 2023-01-08 23:57:13 -03:00 committed by GitHub
commit 38e19536af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 88 additions and 27 deletions

View File

@ -14,8 +14,8 @@ use utils::colors;
use crate::{ use crate::{
commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents}, commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
error::FinalError, error::{Error, FinalError},
extension::{self, flatten_compression_formats, Extension, SUPPORTED_EXTENSIONS}, extension::{self, flatten_compression_formats, parse_format, Extension, SUPPORTED_EXTENSIONS},
info, info,
list::ListOptions, list::ListOptions,
utils::{ utils::{
@ -114,7 +114,13 @@ pub fn run(
} }
// Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma] // Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
let formats = extension::extensions_from_path(&output_path); let (formats_from_flag, formats) = match args.format {
Some(formats) => {
let parsed_formats = parse_format(&formats)?;
(Some(formats), parsed_formats)
}
None => (None, extension::extensions_from_path(&output_path)),
};
let first_format = formats.first().ok_or_else(|| { let first_format = formats.first().ok_or_else(|| {
let output_path = EscapedPathDisplay::new(&output_path); let output_path = EscapedPathDisplay::new(&output_path);
@ -134,28 +140,42 @@ pub fn run(
// If first format is not archive, can't compress folder, or multiple files // If first format is not archive, can't compress folder, or multiple files
// Index safety: empty formats should be checked above. // Index safety: empty formats should be checked above.
if !first_format.is_archive() && (is_some_input_a_folder || is_multiple_inputs) { if !first_format.is_archive() && (is_some_input_a_folder || is_multiple_inputs) {
// This piece of code creates a suggestion for compressing multiple files
// It says:
// Change from file.bz.xz
// To file.tar.bz.xz
let suggested_output_path = build_archive_file_suggestion(&output_path, ".tar")
.expect("output path should contain a compression format");
let output_path = EscapedPathDisplay::new(&output_path);
let first_detail_message = if is_multiple_inputs { let first_detail_message = if is_multiple_inputs {
"You are trying to compress multiple files." "You are trying to compress multiple files."
} else { } else {
"You are trying to compress a folder." "You are trying to compress a folder."
}; };
let (from_hint, to_hint) = if let Some(formats) = formats_from_flag {
let formats = formats.to_string_lossy();
(
format!("From: --format {formats}"),
format!("To: --format tar.{formats}"),
)
} else {
// This piece of code creates a suggestion for compressing multiple files
// It says:
// Change from file.bz.xz
// To file.tar.bz.xz
let suggested_output_path = build_archive_file_suggestion(&output_path, ".tar")
.expect("output path should contain a compression format");
(
format!("From: {}", EscapedPathDisplay::new(&output_path)),
format!("To: {suggested_output_path}"),
)
};
let output_path = EscapedPathDisplay::new(&output_path);
let error = FinalError::with_title(format!("Cannot compress to '{output_path}'.")) let error = FinalError::with_title(format!("Cannot compress to '{output_path}'."))
.detail(first_detail_message) .detail(first_detail_message)
.detail(format!( .detail(format!(
"The compression format '{first_format}' does not accept multiple files.", "The compression format '{first_format}' does not accept multiple files.",
)) ))
.detail("Formats that bundle files into an archive are .tar and .zip.") .detail("Formats that bundle files into an archive are tar and zip.")
.hint(format!("Try inserting '.tar' or '.zip' before '{first_format}'.")) .hint(format!("Try inserting 'tar.' or 'zip.' before '{first_format}'."))
.hint(format!("From: {output_path}")) .hint(from_hint)
.hint(format!("To: {suggested_output_path}")); .hint(to_hint);
return Err(error.into()); return Err(error.into());
} }
@ -228,11 +248,22 @@ pub fn run(
let mut output_paths = vec![]; let mut output_paths = vec![];
let mut formats = vec![]; let mut formats = vec![];
if let Some(format) = args.format {
let format = parse_format(&format)?;
for path in files.iter() {
let file_name = path.file_name().ok_or_else(|| Error::NotFound {
error_title: format!("{} does not have a file name", EscapedPathDisplay::new(path)),
})?;
output_paths.push(file_name.as_ref());
formats.push(format.clone());
}
} else {
for path in files.iter() { for path in files.iter() {
let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path); let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
output_paths.push(file_output_path); output_paths.push(file_output_path);
formats.push(file_formats); formats.push(file_formats);
} }
}
if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? { if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
return Ok(()); return Ok(());
@ -292,6 +323,12 @@ pub fn run(
Subcommand::List { archives: files, tree } => { Subcommand::List { archives: files, tree } => {
let mut formats = vec![]; let mut formats = vec![];
if let Some(format) = args.format {
let format = parse_format(&format)?;
for _ in 0..files.len() {
formats.push(format.clone());
}
} else {
for path in files.iter() { for path in files.iter() {
let file_formats = extension::extensions_from_path(path); let file_formats = extension::extensions_from_path(path);
formats.push(file_formats); formats.push(file_formats);
@ -300,6 +337,7 @@ pub fn run(
if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? { if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
return Ok(()); return Ok(());
} }
}
// Ensure we were not told to list the content of a non-archive compressed file // Ensure we were not told to list the content of a non-archive compressed file
check_for_non_archive_formats(&files, &formats)?; check_for_non_archive_formats(&files, &formats)?;

View File

@ -32,6 +32,8 @@ pub enum Error {
WalkdirError { reason: String }, WalkdirError { reason: String },
/// Custom and unique errors are reported in this variant /// Custom and unique errors are reported in this variant
Custom { reason: FinalError }, Custom { reason: FinalError },
/// Invalid format passed to `--format`
InvalidFormat { reason: String },
} }
/// Alias to std's Result with ouch's Error /// Alias to std's Result with ouch's Error
@ -135,6 +137,7 @@ impl fmt::Display for Error {
FinalError::with_title(error_title.to_string()).detail("Permission denied") FinalError::with_title(error_title.to_string()).detail("Permission denied")
} }
Error::UnsupportedZipArchive(reason) => FinalError::with_title("Unsupported zip archive").detail(*reason), Error::UnsupportedZipArchive(reason) => FinalError::with_title("Unsupported zip archive").detail(*reason),
Error::InvalidFormat { reason } => FinalError::with_title("Invalid archive format").detail(reason.clone()),
Error::Custom { reason } => reason.clone(), Error::Custom { reason } => reason.clone(),
}; };

View File

@ -1,11 +1,11 @@
//! Our representation of all the supported compression formats. //! Our representation of all the supported compression formats.
use std::{fmt, path::Path}; use std::{ffi::OsStr, fmt, path::Path};
use bstr::ByteSlice; use bstr::ByteSlice;
use self::CompressionFormat::*; use self::CompressionFormat::*;
use crate::warning; use crate::{error::Error, warning};
/// A wrapper around `CompressionFormat` that allows combinations like `tgz` /// A wrapper around `CompressionFormat` that allows combinations like `tgz`
#[derive(Debug, Clone, Eq)] #[derive(Debug, Clone, Eq)]
@ -138,6 +138,22 @@ pub fn split_extension<'a>(name: &mut &'a [u8]) -> Option<&'a [u8]> {
Some(ext) Some(ext)
} }
pub fn parse_format(fmt: &OsStr) -> crate::Result<Vec<Extension>> {
let fmt = <[u8] as ByteSlice>::from_os_str(fmt).ok_or_else(|| Error::InvalidFormat {
reason: "Invalid UTF-8".into(),
})?;
let mut extensions = Vec::new();
for extension in fmt.split_str(b".") {
let extension = to_extension(extension).ok_or_else(|| Error::InvalidFormat {
reason: format!("Unsupported extension: {}", extension.to_str_lossy()),
})?;
extensions.push(extension);
}
Ok(extensions)
}
/// Extracts extensions from a path. /// Extracts extensions from a path.
/// ///
/// Returns both the remaining path and the list of extension objects /// Returns both the remaining path and the list of extension objects

View File

@ -1,4 +1,4 @@
use std::path::PathBuf; use std::{ffi::OsString, path::PathBuf};
use clap::{Parser, ValueHint}; use clap::{Parser, ValueHint};
@ -37,6 +37,10 @@ pub struct Opts {
#[arg(short = 'g', long, global = true)] #[arg(short = 'g', long, global = true)]
pub gitignore: bool, pub gitignore: bool,
/// Specify the format of the archive
#[arg(short, long, global = true)]
pub format: Option<OsString>,
/// Ouch and claps subcommands /// Ouch and claps subcommands
#[command(subcommand)] #[command(subcommand)]
pub cmd: Subcommand, pub cmd: Subcommand,