Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --format option #341

Merged
merged 1 commit into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 62 additions & 24 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use utils::colors;

use crate::{
commands::{compress::compress_files, decompress::decompress_file, list::list_archive_contents},
error::FinalError,
extension::{self, flatten_compression_formats, Extension, SUPPORTED_EXTENSIONS},
error::{Error, FinalError},
extension::{self, flatten_compression_formats, parse_format, Extension, SUPPORTED_EXTENSIONS},
info,
list::ListOptions,
utils::{
Expand Down Expand Up @@ -114,7 +114,13 @@ pub fn run(
}

// 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 output_path = EscapedPathDisplay::new(&output_path);
Expand All @@ -134,28 +140,42 @@ pub fn run(
// If first format is not archive, can't compress folder, or multiple files
// Index safety: empty formats should be checked above.
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 {
"You are trying to compress multiple files."
} else {
"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}'."))
.detail(first_detail_message)
.detail(format!(
"The compression format '{first_format}' does not accept multiple files.",
))
.detail("Formats that bundle files into an archive are .tar and .zip.")
.hint(format!("Try inserting '.tar' or '.zip' before '{first_format}'."))
.hint(format!("From: {output_path}"))
.hint(format!("To: {suggested_output_path}"));
.detail("Formats that bundle files into an archive are tar and zip.")
.hint(format!("Try inserting 'tar.' or 'zip.' before '{first_format}'."))
.hint(from_hint)
.hint(to_hint);

return Err(error.into());
}
Expand Down Expand Up @@ -228,10 +248,21 @@ pub fn run(
let mut output_paths = vec![];
let mut formats = vec![];

for path in files.iter() {
let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
output_paths.push(file_output_path);
formats.push(file_formats);
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() {
let (file_output_path, file_formats) = extension::separate_known_extensions_from_name(path);
output_paths.push(file_output_path);
formats.push(file_formats);
}
}

if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
Expand Down Expand Up @@ -292,13 +323,20 @@ pub fn run(
Subcommand::List { archives: files, tree } => {
let mut formats = vec![];

for path in files.iter() {
let file_formats = extension::extensions_from_path(path);
formats.push(file_formats);
}
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() {
let file_formats = extension::extensions_from_path(path);
formats.push(file_formats);
}

if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
return Ok(());
if let ControlFlow::Break(_) = check_mime_type(&files, &mut formats, question_policy)? {
return Ok(());
}
}

// Ensure we were not told to list the content of a non-archive compressed file
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub enum Error {
WalkdirError { reason: String },
/// Custom and unique errors are reported in this variant
Custom { reason: FinalError },
/// Invalid format passed to `--format`
InvalidFormat { reason: String },
}

/// Alias to std's Result with ouch's Error
Expand Down Expand Up @@ -135,6 +137,7 @@ impl fmt::Display for Error {
FinalError::with_title(error_title.to_string()).detail("Permission denied")
}
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(),
};

Expand Down
20 changes: 18 additions & 2 deletions src/extension.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Our representation of all the supported compression formats.

use std::{fmt, path::Path};
use std::{ffi::OsStr, fmt, path::Path};

use bstr::ByteSlice;

use self::CompressionFormat::*;
use crate::warning;
use crate::{error::Error, warning};

/// A wrapper around `CompressionFormat` that allows combinations like `tgz`
#[derive(Debug, Clone, Eq)]
Expand Down Expand Up @@ -138,6 +138,22 @@ pub fn split_extension<'a>(name: &mut &'a [u8]) -> Option<&'a [u8]> {
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.
///
/// Returns both the remaining path and the list of extension objects
Expand Down
6 changes: 5 additions & 1 deletion src/opts.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{ffi::OsString, path::PathBuf};

use clap::{Parser, ValueHint};

Expand Down Expand Up @@ -37,6 +37,10 @@ pub struct Opts {
#[arg(short = 'g', long, global = true)]
pub gitignore: bool,

/// Specify the format of the archive
#[arg(short, long, global = true)]
pub format: Option<OsString>,

/// Ouch and claps subcommands
#[command(subcommand)]
pub cmd: Subcommand,
Expand Down