Merge pull request #108 from SpyrosRoum/migrate-to-clap

Migrate from `oof` to `clap`!
This commit is contained in:
João Marcos Bezerra 2021-10-20 14:31:37 -03:00 committed by GitHub
commit a46fa1fb38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 279 additions and 859 deletions

148
Cargo.lock generated
View File

@ -83,6 +83,37 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"unicase",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
@ -128,6 +159,21 @@ dependencies = [
"wasi",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -137,6 +183,16 @@ dependencies = [
"libc",
]
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "infer"
version = "0.5.0"
@ -155,6 +211,12 @@ dependencies = [
"libc",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.103"
@ -183,6 +245,12 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@ -199,18 +267,27 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "os_str_bytes"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799"
dependencies = [
"memchr",
]
[[package]]
name = "ouch"
version = "0.2.0"
dependencies = [
"atty",
"bzip2",
"clap",
"flate2",
"infer",
"libc",
"once_cell",
"rand",
"strsim",
"tar",
"tempfile",
"walkdir",
@ -231,6 +308,30 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3ca011bd0129ff4ae15cd04c4eef202cadf6c51c21e47aba319b4e0501db741"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.30"
@ -358,6 +459,24 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.30"
@ -378,6 +497,27 @@ dependencies = [
"syn",
]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
@ -396,6 +536,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.2"

View File

@ -13,10 +13,10 @@ description = "A command-line utility for easily compressing and decompressing f
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "=3.0.0-beta.5" # Keep it pinned while in beta!
atty = "0.2.14"
once_cell = "1.8.0"
walkdir = "2.3.2"
strsim = "0.10.0"
bzip2 = "0.4.3"
libc = "0.2.103"
tar = "0.4.37"

View File

@ -10,11 +10,15 @@ use tar;
use walkdir::WalkDir;
use crate::{
info, oof,
info,
utils::{self, Bytes},
};
pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> {
pub fn unpack_archive(
reader: Box<dyn Read>,
output_folder: &Path,
skip_questions_positively: Option<bool>,
) -> crate::Result<Vec<PathBuf>> {
let mut archive = tar::Archive::new(reader);
let mut files_unpacked = vec![];
@ -22,7 +26,7 @@ pub fn unpack_archive(reader: Box<dyn Read>, output_folder: &Path, flags: &oof::
let mut file = file?;
let file_path = output_folder.join(file.path()?);
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, flags)? {
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? {
continue;
}

View File

@ -10,14 +10,18 @@ use walkdir::WalkDir;
use zip::{self, read::ZipFile, ZipArchive};
use crate::{
info, oof,
utils::{self, dir_is_empty, strip_cur_dir, Bytes},
info,
utils::{self, dir_is_empty,strip_cur_dir, Bytes},
};
use self::utf8::get_invalid_utf8_paths;
/// Unpacks the archive given by `archive` into the folder given by `into`.
pub fn unpack_archive<R>(mut archive: ZipArchive<R>, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>>
pub fn unpack_archive<R>(
mut archive: ZipArchive<R>,
into: &Path,
skip_questions_positively: Option<bool>,
) -> crate::Result<Vec<PathBuf>>
where
R: Read + Seek,
{
@ -30,7 +34,7 @@ where
};
let file_path = into.join(file_path);
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, flags)? {
if file_path.exists() && !utils::user_wants_to_overwrite(&file_path, skip_questions_positively)? {
continue;
}

View File

@ -1,81 +1,74 @@
//! CLI argparser configuration, command detection and input treatment.
//!
//! NOTE: the argparser implementation itself is not in this file.
//! CLI arg parser configuration, command detection and input treatment.
use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
vec::Vec,
};
use strsim::normalized_damerau_levenshtein;
use clap::{Parser, ValueHint};
use crate::{arg_flag, error::FinalError, flag, oof, Error};
use crate::Error;
#[derive(PartialEq, Eq, Debug)]
pub enum Command {
/// Files to be compressed
#[derive(Parser, Debug)]
#[clap(version, about)]
pub struct Opts {
/// Skip overwrite questions positively.
#[clap(short, long, conflicts_with = "no")]
pub yes: bool,
/// Skip overwrite questions negatively.
#[clap(short, long)]
pub no: bool,
#[clap(subcommand)]
pub cmd: Subcommand,
}
#[derive(Parser, PartialEq, Eq, Debug)]
pub enum Subcommand {
/// Compress files. Alias: c
#[clap(alias = "c")]
Compress {
/// Files to be compressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
output_path: PathBuf,
/// The resulting file. Its extensions specify how the files will be compressed and they need to be supported
#[clap(required = true, value_hint = ValueHint::FilePath)]
output: PathBuf,
},
/// Files to be decompressed and their extensions
/// Compress files. Alias: d
#[clap(alias = "d")]
Decompress {
/// Files to be decompressed
#[clap(required = true, min_values = 1)]
files: Vec<PathBuf>,
output_folder: Option<PathBuf>,
/// Decompress files in a directory other than the current
#[clap(short, long, value_hint = ValueHint::DirPath)]
output: Option<PathBuf>,
},
ShowHelp,
ShowVersion,
}
/// Calls parse_args_and_flags_from using argv (std::env::args_os)
///
/// This function is also responsible for treating and checking the command-line input,
/// such as calling [`canonicalize`](std::fs::canonicalize), checking if it the given files exists, etc.
pub fn parse_args() -> crate::Result<ParsedArgs> {
// From argv, but ignoring empty arguments
let args = env::args_os().skip(1).filter(|arg| !arg.is_empty()).collect();
let mut parsed_args = parse_args_from(args)?;
impl Opts {
/// A helper method that calls `clap::Parser::parse` and then translates relative paths to absolute.
/// Also determines if the user wants to skip questions or not
pub fn parse_args() -> crate::Result<(Self, Option<bool>)> {
let mut opts: Self = Self::parse();
// If has a list of files, canonicalize them, reporting error if they do not exist
match &mut parsed_args.command {
Command::Compress { files, .. } | Command::Decompress { files, .. } => {
*files = canonicalize_files(files)?;
}
_ => {}
let (Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. }) = &mut opts.cmd;
*files = canonicalize_files(files)?;
let skip_questions_positively = if opts.yes {
Some(true)
} else if opts.no {
Some(false)
} else {
None
};
Ok((opts, skip_questions_positively))
}
if parsed_args.flags.is_present("yes") && parsed_args.flags.is_present("no") {
return Err(Error::Custom {
reason: FinalError::with_title("Conflicted flags detected.")
.detail("You can't use both --yes and --no at the same time.")
.hint("Use --yes if you want to positively skip overwrite questions")
.hint("Use --no if you want to negatively skip overwrite questions")
.hint("Don't use either if you want to be asked each time"),
});
}
Ok(parsed_args)
}
#[derive(Debug)]
pub struct ParsedArgs {
pub command: Command,
pub flags: oof::Flags,
}
/// Checks if the first argument is a typo for the `compress` subcommand.
/// Returns true if the arg is probably a typo or false otherwise.
fn is_typo(path: impl AsRef<Path>) -> bool {
if path.as_ref().exists() {
// If the file exists then we won't check for a typo
return false;
}
let path = path.as_ref().to_string_lossy();
// We'll consider it a typo if the word is somewhat 'close' to "compress"
normalized_damerau_levenshtein("compress", &path) > 0.625
}
fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
@ -94,120 +87,3 @@ fn canonicalize(path: impl AsRef<Path>) -> crate::Result<PathBuf> {
fn canonicalize_files(files: &[impl AsRef<Path>]) -> crate::Result<Vec<PathBuf>> {
files.iter().map(canonicalize).collect()
}
pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() {
return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() });
}
if oof::matches_any_arg(&args, &["--version"]) {
return Ok(ParsedArgs { command: Command::ShowVersion, flags: oof::Flags::default() });
}
let subcommands = &["c", "compress", "d", "decompress"];
let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")];
let parsed_args = match oof::pop_subcommand(&mut args, subcommands) {
Some(&"c") | Some(&"compress") => {
// `ouch compress` subcommand
let (args, flags) = oof::filter_flags(args, &flags_info)?;
let mut files: Vec<PathBuf> = args.into_iter().map(PathBuf::from).collect();
if files.len() < 2 {
return Err(Error::MissingArgumentsForCompression);
}
// Safety: we checked that args.len() >= 2
let output_path = files.pop().unwrap();
let command = Command::Compress { files, output_path };
ParsedArgs { command, flags }
}
Some(&"d") | Some(&"decompress") => {
flags_info.push(arg_flag!('o', "output"));
if let Some(first_arg) = args.first() {
if is_typo(first_arg) {
return Err(Error::CompressionTypo);
}
} else {
return Err(Error::MissingArgumentsForDecompression);
}
// Parse flags
let (files, flags) = oof::filter_flags(args, &flags_info)?;
let files = files.into_iter().map(PathBuf::from).collect();
let output_folder = flags.arg("output").map(PathBuf::from);
// TODO: ensure all files are decompressible
let command = Command::Decompress { files, output_folder };
ParsedArgs { command, flags }
}
// Defaults to help when there is no subcommand
None => {
return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() });
}
_ => unreachable!("You should match each subcommand passed."),
};
Ok(parsed_args)
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_args<T: From<OsString>>(text: &str) -> Vec<T> {
let args = text.split_whitespace();
args.map(OsString::from).map(T::from).collect()
}
fn test_cli(args: &str) -> crate::Result<ParsedArgs> {
let args = gen_args(args);
parse_args_from(args)
}
#[test]
fn test_cli_commands() {
assert_eq!(test_cli("").unwrap().command, Command::ShowHelp);
assert_eq!(test_cli("--help").unwrap().command, Command::ShowHelp);
assert_eq!(test_cli("--version").unwrap().command, Command::ShowVersion);
assert_eq!(test_cli("--version").unwrap().flags, oof::Flags::default());
assert_eq!(
test_cli("decompress foo.zip bar.zip").unwrap().command,
Command::Decompress { files: gen_args("foo.zip bar.zip"), output_folder: None }
);
assert_eq!(
test_cli("d foo.zip bar.zip").unwrap().command,
Command::Decompress { files: gen_args("foo.zip bar.zip"), output_folder: None }
);
assert_eq!(
test_cli("compress foo bar baz.zip").unwrap().command,
Command::Compress { files: gen_args("foo bar"), output_path: "baz.zip".into() }
);
assert_eq!(
test_cli("c foo bar baz.zip").unwrap().command,
Command::Compress { files: gen_args("foo bar"), output_path: "baz.zip".into() }
);
assert_eq!(test_cli("compress").unwrap_err(), Error::MissingArgumentsForCompression);
// assert_eq!(test_cli("decompress").unwrap_err(), Error::MissingArgumentsForCompression); // TODO
}
#[test]
fn test_cli_flags() {
// --help and --version flags are considered commands that are ran over anything else
assert_eq!(test_cli("--help").unwrap().flags, oof::Flags::default());
assert_eq!(test_cli("--version").unwrap().flags, oof::Flags::default());
assert_eq!(
test_cli("decompress foo --yes bar --output folder").unwrap().flags,
oof::Flags {
boolean_flags: vec!["yes"].into_iter().collect(),
argument_flags: vec![("output", OsString::from("folder"))].into_iter().collect(),
}
);
}
}

View File

@ -12,13 +12,13 @@ use utils::colors;
use crate::{
archive,
cli::Command,
cli::{Opts, Subcommand},
error::FinalError,
extension::{
self,
CompressionFormat::{self, *},
},
info, oof,
info,
utils::nice_directory_display,
utils::to_utf,
utils::{self, dir_is_empty},
@ -38,9 +38,9 @@ fn represents_several_files(files: &[PathBuf]) -> bool {
files.iter().any(is_non_empty_dir) || files.len() > 1
}
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
match command {
Command::Compress { files, output_path } => {
pub fn run(args: Opts, skip_questions_positively: Option<bool>) -> crate::Result<()> {
match args.cmd {
Subcommand::Compress { files, output: output_path } => {
// Formats from path extension, like "file.tar.gz.xz" -> vec![Tar, Gzip, Lzma]
let mut formats = extension::extensions_from_path(&output_path);
@ -94,7 +94,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
return Err(Error::with_reason(reason));
}
if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, flags)? {
if output_path.exists() && !utils::user_wants_to_overwrite(&output_path, skip_questions_positively)? {
// User does not want to overwrite this file
return Ok(());
}
@ -126,7 +126,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
drop(drain_iter); // Remove the extensions from `formats`
}
}
let compress_result = compress_files(files, formats, output_file, flags);
let compress_result = compress_files(files, formats, output_file);
// If any error occurred, delete incomplete file
if compress_result.is_err() {
@ -144,7 +144,7 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
compress_result?;
}
Command::Decompress { files, output_folder } => {
Subcommand::Decompress { files, output: output_folder } => {
let mut output_paths = vec![];
let mut formats = vec![];
@ -176,21 +176,14 @@ pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
let output_folder = output_folder.as_ref().map(|path| path.as_ref());
for ((input_path, formats), file_name) in files.iter().zip(formats).zip(output_paths) {
decompress_file(input_path, formats, output_folder, file_name, flags)?;
decompress_file(input_path, formats, output_folder, file_name, skip_questions_positively)?;
}
}
Command::ShowHelp => crate::help_command(),
Command::ShowVersion => crate::version_command(),
}
Ok(())
}
fn compress_files(
files: Vec<PathBuf>,
formats: Vec<CompressionFormat>,
output_file: fs::File,
_flags: &oof::Flags,
) -> crate::Result<()> {
fn compress_files(files: Vec<PathBuf>, formats: Vec<CompressionFormat>, output_file: fs::File) -> crate::Result<()> {
let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file);
if let [Tar | Tgz | Zip] = *formats.as_slice() {
@ -296,7 +289,7 @@ fn decompress_file(
formats: Vec<extension::CompressionFormat>,
output_folder: Option<&Path>,
file_name: &Path,
flags: &oof::Flags,
skip_questions_positively: Option<bool>,
) -> crate::Result<()> {
// TODO: improve error message
let reader = fs::File::open(&input_file_path)?;
@ -318,7 +311,7 @@ fn decompress_file(
if let [Zip] = *formats.as_slice() {
utils::create_dir_if_non_existent(output_folder)?;
let zip_archive = zip::ZipArchive::new(reader)?;
let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?;
let _files = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
return Ok(());
}
@ -356,27 +349,27 @@ fn decompress_file(
info!("Successfully decompressed archive in {}.", nice_directory_display(output_path));
}
Tar => {
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}
Tgz => {
let reader = chain_reader_decoder(&Gzip, reader)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}
Tbz => {
let reader = chain_reader_decoder(&Bzip, reader)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}
Tlzma => {
let reader = chain_reader_decoder(&Lzma, reader)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}
Tzst => {
let reader = chain_reader_decoder(&Zstd, reader)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, flags)?;
let _ = crate::archive::tar::unpack_archive(reader, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}
Zip => {
@ -391,7 +384,7 @@ fn decompress_file(
io::copy(&mut reader, &mut vec)?;
let zip_archive = zip::ZipArchive::new(io::Cursor::new(vec))?;
let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, flags)?;
let _ = crate::archive::zip::unpack_archive(zip_archive, output_folder, skip_questions_positively)?;
info!("Successfully decompressed archive in {}.", nice_directory_display(output_folder));
}

View File

@ -11,7 +11,7 @@ use std::{
path::{Path, PathBuf},
};
use crate::{oof, utils::colors::*};
use crate::utils::colors::*;
#[derive(Debug, PartialEq)]
pub enum Error {
@ -24,7 +24,6 @@ pub enum Error {
PermissionDenied,
UnsupportedZipArchive(&'static str),
InternalError,
OofError(oof::OofError),
CompressingRootFolder,
MissingArgumentsForCompression,
MissingArgumentsForDecompression,
@ -127,7 +126,6 @@ impl fmt::Display for Error {
.detail("Please help us improve by reporting the issue at:")
.detail(format!(" {}https://github.com/vrmiguel/ouch/issues ", *CYAN))
}
Error::OofError(err) => FinalError::with_title(err),
Error::IoError { reason } => FinalError::with_title(reason),
Error::CompressionTypo => {
FinalError::with_title("Possible typo detected")
@ -179,9 +177,3 @@ impl From<walkdir::Error> for Error {
Self::WalkdirError { reason: err.to_string() }
}
}
impl From<oof::OofError> for Error {
fn from(err: oof::OofError) -> Self {
Self::OofError(err)
}
}

View File

@ -7,7 +7,6 @@
// Public modules
pub mod cli;
pub mod commands;
pub mod oof;
// Private modules
pub mod archive;
@ -21,48 +20,3 @@ pub use error::{Error, Result};
/// The status code ouch has when an error is encountered
pub const EXIT_FAILURE: i32 = libc::EXIT_FAILURE;
const VERSION: &str = env!("CARGO_PKG_VERSION");
fn help_command() {
use utils::colors::*;
println!(
"\
{cyan}ouch{reset} - Obvious Unified Compression files Helper
{cyan}USAGE:{reset}
{green}ouch decompress {magenta}<files...>{reset} Decompresses files.
{green}ouch compress {magenta}<files...> OUTPUT.EXT{reset} Compresses files into {magenta}OUTPUT.EXT{reset},
where {magenta}EXT{reset} must be a supported format.
{cyan}ALIASES:{reset}
{green}d decompress {reset}
{green}c compress {reset}
{cyan}FLAGS:{reset}
{yellow}-h{white}, {yellow}--help{reset} Display this help information.
{yellow}-y{white}, {yellow}--yes{reset} Skip overwrite questions.
{yellow}-n{white}, {yellow}--no{reset} Skip overwrite questions.
{yellow}--version{reset} Display version information.
{cyan}SPECIFIC FLAGS:{reset}
{yellow}-o{reset}, {yellow}--output{reset} FOLDER_PATH When decompressing, to decompress files to
another folder.
Visit https://github.com/ouch-org/ouch for more usage examples.",
magenta = *MAGENTA,
white = *WHITE,
green = *GREEN,
yellow = *YELLOW,
reset = *RESET,
cyan = *CYAN
);
}
#[inline]
fn version_command() {
use utils::colors::*;
println!("{green}ouch{reset} {}", crate::VERSION, green = *GREEN, reset = *RESET);
}

View File

@ -1,7 +1,4 @@
use ouch::{
cli::{parse_args, ParsedArgs},
commands, Result,
};
use ouch::{cli::Opts, commands, Result};
fn main() {
if let Err(err) = run() {
@ -10,7 +7,7 @@ fn main() {
}
}
fn run() -> crate::Result<()> {
let ParsedArgs { command, flags } = parse_args()?;
commands::run(command, &flags)
fn run() -> Result<()> {
let (args, skip_questions_positively) = Opts::parse_args()?;
commands::run(args, skip_questions_positively)
}

View File

@ -1,55 +0,0 @@
//! Errors related to argparsing.
use std::{error, ffi::OsString, fmt};
use super::Flag;
#[derive(Debug, PartialEq)]
pub enum OofError {
FlagValueConflict {
flag: Flag,
previous_value: OsString,
new_value: OsString,
},
/// User supplied a flag containing invalid Unicode
InvalidUnicode(OsString),
/// User supplied an unrecognized short flag
UnknownShortFlag(char),
UnknownLongFlag(String),
MisplacedShortArgFlagError(char),
MissingValueToFlag(Flag),
DuplicatedFlag(Flag),
}
impl error::Error for OofError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
impl fmt::Display for OofError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO: implement proper debug messages
match self {
OofError::FlagValueConflict { flag, previous_value, new_value } => {
write!(
f,
"CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.",
flag.long, previous_value, new_value
)
}
OofError::InvalidUnicode(flag) => write!(f, "{:?} is not valid Unicode.", flag),
OofError::UnknownShortFlag(ch) => write!(f, "Unknown argument '-{}'", ch),
OofError::MisplacedShortArgFlagError(ch) => {
write!(
f,
"Invalid placement of `-{}`.\nOnly the last letter in a sequence of short flags can take values.",
ch
)
}
OofError::MissingValueToFlag(flag) => write!(f, "Flag {} takes value but none was supplied.", flag),
OofError::DuplicatedFlag(flag) => write!(f, "Duplicated usage of {}.", flag),
OofError::UnknownLongFlag(flag) => write!(f, "Unknown argument '--{}'", flag),
}
}
}

View File

@ -1,109 +0,0 @@
use std::{
collections::{HashMap, HashSet},
ffi::{OsStr, OsString},
};
/// Shallow type, created to indicate a `Flag` that accepts a argument.
///
/// ArgFlag::long(), is actually a Flag::long(), but sets a internal attribute.
///
/// Examples in here pls
#[derive(Debug)]
pub struct ArgFlag;
impl ArgFlag {
pub fn long(name: &'static str) -> Flag {
Flag { long: name, short: None, takes_value: true }
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Flag {
// Also the name
pub long: &'static str,
pub short: Option<char>,
pub takes_value: bool,
}
impl std::fmt::Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.short {
Some(short_flag) => write!(f, "-{}/--{}", short_flag, self.long),
None => write!(f, "--{}", self.long),
}
}
}
impl Flag {
pub fn long(name: &'static str) -> Self {
Self { long: name, short: None, takes_value: false }
}
pub fn short(mut self, short_flag_char: char) -> Self {
self.short = Some(short_flag_char);
self
}
}
#[derive(Default, PartialEq, Eq, Debug)]
pub struct Flags {
pub boolean_flags: HashSet<&'static str>,
pub argument_flags: HashMap<&'static str, OsString>,
}
impl Flags {
pub fn new() -> Self {
Self::default()
}
pub fn is_present(&self, flag_name: &str) -> bool {
self.boolean_flags.contains(flag_name) || self.argument_flags.contains_key(flag_name)
}
pub fn arg(&self, flag_name: &str) -> Option<&OsString> {
self.argument_flags.get(flag_name)
}
pub fn take_arg(&mut self, flag_name: &str) -> Option<OsString> {
self.argument_flags.remove(flag_name)
}
}
#[derive(Debug)]
pub enum FlagType {
None,
Short,
Long,
}
impl FlagType {
pub fn from(text: impl AsRef<OsStr>) -> Self {
let text = text.as_ref();
let mut iter;
#[cfg(target_family = "unix")]
{
use std::os::unix::ffi::OsStrExt;
iter = text.as_bytes().iter();
}
#[cfg(target_family = "windows")]
{
use std::os::windows::ffi::OsStrExt;
iter = text.encode_wide();
}
// 45 is the code for a hyphen
// Typed as 45_u16 for Windows
// Typed as 45_u8 for Unix
if let Some(45) = iter.next() {
if let Some(45) = iter.next() {
Self::Long
} else {
Self::Short
}
} else {
Self::None
}
}
}

View File

@ -1,383 +0,0 @@
//! Ouch's argparsing crate.
//!
//! The usage of this crate is heavily based on boolean_flags and
//! argument_flags, there should be an more _obvious_ naming.
mod error;
mod flags;
pub mod util;
use std::{
collections::BTreeMap,
ffi::{OsStr, OsString},
};
pub use error::OofError;
pub use flags::{ArgFlag, Flag, FlagType, Flags};
use util::trim_double_hyphen;
/// Pop leading application `subcommand`, if valid.
///
/// `args` can be a Vec of `OsString` or `OsStr`
/// `subcommands` is any container that can yield `&str` through `AsRef`, can be `Vec<&str>` or
/// a GREAT `BTreeSet<String>` (or `BTreeSet<&str>`).
pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec<T>, subcommands: I) -> Option<&'a II>
where
I: IntoIterator<Item = &'a II>,
II: AsRef<str>,
T: AsRef<OsStr>,
{
if args.is_empty() {
return None;
}
for subcommand in subcommands.into_iter() {
if subcommand.as_ref() == args[0].as_ref() {
args.remove(0);
return Some(subcommand);
}
}
None
}
/// Detect flags from args and filter from args.
///
/// Each flag received via flags_info should must have unique long and short identifiers.
///
/// # Panics (Developer errors)
/// - If there are duplicated short flag identifiers.
/// - If there are duplicated long flag identifiers.
///
/// Both conditions cause panic because your program's flags specification is meant to have unique
/// flags. There shouldn't be two "--verbose" flags, for example.
/// Caller should guarantee it, fortunately, this can almost always be caught while prototyping in
/// debug mode, test your CLI flags once, if it works once, you're good,
///
/// # Errors (User errors)
/// - Argument flag comes at last arg, so there's no way to provide an argument.
/// - Or if it doesn't comes at last, but the rest are just flags, no possible and valid arg.
/// - Short flags with multiple letters in the same arg contain a argument flag that does not come
/// as the last one in the list (example "-oahc", where 'o', 'a', or 'h' is a argument flag, but do
/// not comes at last, so it is impossible for them to receive the required argument.
/// - User passes same flag twice (short or long, boolean or arg).
///
/// ...
pub fn filter_flags(args: Vec<OsString>, flags_info: &[Flag]) -> Result<(Vec<OsString>, Flags), OofError> {
let mut short_flags_info = BTreeMap::<char, &Flag>::new();
let mut long_flags_info = BTreeMap::<&'static str, &Flag>::new();
for flag in flags_info.iter() {
// Panics if duplicated/conflicts
assert!(!long_flags_info.contains_key(flag.long), "DEV ERROR: duplicated long flag '{}'.", flag.long);
long_flags_info.insert(flag.long, flag);
if let Some(short) = flag.short {
// Panics if duplicated/conflicts
assert!(!short_flags_info.contains_key(&short), "DEV ERROR: duplicated short flag '-{}'.", short);
short_flags_info.insert(short, flag);
}
}
// Consume args, filter out flags, and add back to args new vec
let mut iter = args.into_iter();
let mut new_args = vec![];
let mut result_flags = Flags::new();
while let Some(arg) = iter.next() {
let flag_type = FlagType::from(&arg);
// If it isn't a flag, retrieve to `args` and skip this iteration
if let FlagType::None = flag_type {
new_args.push(arg);
continue;
}
// If it is a flag, now we try to interpret it as valid utf-8
let flag = match arg.to_str() {
Some(arg) => arg,
None => return Err(OofError::InvalidUnicode(arg)),
};
// Only one hyphen in the flag
// A short flag can be of form "-", "-abcd", "-h", "-v", etc
if let FlagType::Short = flag_type {
assert_eq!(flag.chars().next(), Some('-'));
// TODO
// TODO: what should happen if the flag is empty?????
// if flags.chars().skip(1).next().is_none() {
// panic!("User error: flag is empty???");
// }
// Skip hyphen and get all letters
let letters = flag.chars().skip(1).collect::<Vec<char>>();
// For each letter in the short arg, except the last one
for (i, letter) in letters.iter().copied().enumerate() {
// Safety: this loop only runs when len >= 1, so this subtraction is safe
let is_last_letter = i == letters.len() - 1;
let flag_info = *short_flags_info.get(&letter).ok_or(OofError::UnknownShortFlag(letter))?;
if !is_last_letter && flag_info.takes_value {
return Err(OofError::MisplacedShortArgFlagError(letter));
// Because "-AB argument" only works if B takes values, not A.
// That is, the short flag that takes values needs to come at the end
// of this piece of text
}
let flag_name: &'static str = flag_info.long;
if flag_info.takes_value {
// If it was already inserted
if result_flags.argument_flags.contains_key(flag_name) {
return Err(OofError::DuplicatedFlag(flag_info.clone()));
}
// pop the next one
let flag_argument = iter.next().ok_or_else(|| OofError::MissingValueToFlag(flag_info.clone()))?;
// Otherwise, insert it.
result_flags.argument_flags.insert(flag_name, flag_argument);
} else {
// If it was already inserted
if result_flags.boolean_flags.contains(flag_name) {
return Err(OofError::DuplicatedFlag(flag_info.clone()));
}
// Otherwise, insert it
result_flags.boolean_flags.insert(flag_name);
}
}
}
if let FlagType::Long = flag_type {
let flag = trim_double_hyphen(flag);
let flag_info = *long_flags_info.get(flag).ok_or_else(|| OofError::UnknownLongFlag(String::from(flag)))?;
let flag_name = flag_info.long;
if flag_info.takes_value {
// If it was already inserted
if result_flags.argument_flags.contains_key(&flag_name) {
return Err(OofError::DuplicatedFlag(flag_info.clone()));
}
let flag_argument = iter.next().ok_or_else(|| OofError::MissingValueToFlag(flag_info.clone()))?;
result_flags.argument_flags.insert(flag_name, flag_argument);
} else {
// If it was already inserted
if result_flags.boolean_flags.contains(&flag_name) {
return Err(OofError::DuplicatedFlag(flag_info.clone()));
}
// Otherwise, insert it
result_flags.boolean_flags.insert(flag_name);
}
// // TODO
// TODO: what should happen if the flag is empty?????
// if flag.is_empty() {
// panic!("Is this an error?");
// }
}
}
Ok((new_args, result_flags))
}
/// Says if any text matches any arg
pub fn matches_any_arg<T, U>(args: &[T], texts: &[U]) -> bool
where
T: AsRef<OsStr>,
U: AsRef<str>,
{
texts.iter().any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref()))
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_args(text: &str) -> Vec<OsString> {
let args = text.split_whitespace();
args.map(OsString::from).collect()
}
fn setup_args_scenario(arg_str: &str) -> Result<(Vec<OsString>, Flags), OofError> {
let flags_info =
[ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('v'), Flag::long("help").short('h')];
let args = gen_args(arg_str);
filter_flags(args, &flags_info)
}
#[test]
fn test_unknown_flags() {
let result = setup_args_scenario("ouch a.zip -s b.tar.gz c.tar").unwrap_err();
assert!(matches!(result, OofError::UnknownShortFlag(flag) if flag == 's'));
let unknown_long_flag = "foobar".to_string();
let result = setup_args_scenario("ouch a.zip --foobar b.tar.gz c.tar").unwrap_err();
assert!(matches!(result, OofError::UnknownLongFlag(flag) if flag == unknown_long_flag));
}
#[test]
fn test_incomplete_flags() {
let incomplete_flag = ArgFlag::long("output_file").short('o');
let result = setup_args_scenario("ouch a.zip b.tar.gz c.tar -o").unwrap_err();
assert!(matches!(result, OofError::MissingValueToFlag(flag) if flag == incomplete_flag));
}
#[test]
fn test_duplicated_flags() {
let duplicated_flag = ArgFlag::long("output_file").short('o');
let result = setup_args_scenario("ouch a.zip b.tar.gz c.tar -o -o -o").unwrap_err();
assert!(matches!(result, OofError::DuplicatedFlag(flag) if flag == duplicated_flag));
}
#[test]
fn test_misplaced_flag() {
let misplaced_flag = ArgFlag::long("output_file").short('o');
let result = setup_args_scenario("ouch -ov a.zip b.tar.gz c.tar").unwrap_err();
assert!(matches!(result, OofError::MisplacedShortArgFlagError(flag) if flag == misplaced_flag.short.unwrap()));
}
// #[test]
// fn test_invalid_unicode_flag() {
// use std::os::unix::prelude::OsStringExt;
// // `invalid_unicode_flag` has to contain a leading hyphen to be considered a flag.
// let invalid_unicode_flag = OsString::from_vec(vec![45, 0, 0, 0, 255, 255, 255, 255]);
// let result = filter_flags(vec![invalid_unicode_flag.clone()], &[]).unwrap_err();
// assert!(matches!(result, OofError::InvalidUnicode(flag) if flag == invalid_unicode_flag));
// }
// asdasdsa
#[test]
fn test_filter_flags() {
let flags_info =
[ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('v'), Flag::long("help").short('h')];
let args = gen_args("ouch a.zip -v b.tar.gz --output_file new_folder c.tar");
let (args, mut flags) = filter_flags(args, &flags_info).unwrap();
assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar"));
assert!(flags.is_present("output_file"));
assert_eq!(Some(&OsString::from("new_folder")), flags.arg("output_file"));
assert_eq!(Some(OsString::from("new_folder")), flags.take_arg("output_file"));
assert!(!flags.is_present("output_file"));
}
#[test]
fn test_pop_subcommand() {
let subcommands = &["commit", "add", "push", "remote"];
let mut args = gen_args("add a b c");
let result = pop_subcommand(&mut args, subcommands);
assert_eq!(result, Some(&"add"));
assert_eq!(args[0], "a");
// Check when no subcommand matches
let mut args = gen_args("a b c");
let result = pop_subcommand(&mut args, subcommands);
assert_eq!(result, None);
assert_eq!(args[0], "a");
}
// #[test]
// fn test_flag_info_macros() {
// let flags_info = [
// arg_flag!('o', "output_file"),
// arg_flag!("delay"),
// flag!('v', "verbose"),
// flag!('h', "help"),
// flag!("version"),
// ];
// let expected = [
// ArgFlag::long("output_file").short('o'),
// ArgFlag::long("delay"),
// Flag::long("verbose").short('v'),
// Flag::long("help").short('h'),
// Flag::long("version"),
// ];
// assert_eq!(flags_info, expected);
// }
#[test]
// TODO: remove should_panic and use proper error handling inside of filter_args
#[should_panic]
fn test_flag_info_with_long_flag_conflict() {
let flags_info = [ArgFlag::long("verbose").short('a'), Flag::long("verbose").short('b')];
// Should panic here
let result = filter_flags(vec![], &flags_info);
assert!(matches!(result, Err(OofError::FlagValueConflict { .. })));
}
#[test]
// TODO: remove should_panic and use proper error handling inside of filter_args
#[should_panic]
fn test_flag_info_with_short_flag_conflict() {
let flags_info = [ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('o')];
// Should panic here
filter_flags(vec![], &flags_info).unwrap_err();
}
#[test]
fn test_matches_any_arg_function() {
let args = gen_args("program a -h b");
assert!(matches_any_arg(&args, &["--help", "-h"]));
let args = gen_args("program a b --help");
assert!(matches_any_arg(&args, &["--help", "-h"]));
let args = gen_args("--version program a b");
assert!(matches_any_arg(&args, &["--version", "-v"]));
let args = gen_args("program -v a --version b");
assert!(matches_any_arg(&args, &["--version", "-v"]));
// Cases without it
let args = gen_args("program a b c");
assert!(!matches_any_arg(&args, &["--help", "-h"]));
let args = gen_args("program a --version -v b c");
assert!(!matches_any_arg(&args, &["--help", "-h"]));
}
}
/// Create a flag with long flag (?).
#[macro_export]
macro_rules! flag {
($short:expr, $long:expr) => {{
oof::Flag::long($long).short($short)
}};
($long:expr) => {{
oof::Flag::long($long)
}};
}
/// Create a flag with long flag (?), receives argument (?).
#[macro_export]
macro_rules! arg_flag {
($short:expr, $long:expr) => {{
oof::ArgFlag::long($long).short($short)
}};
($long:expr) => {{
oof::ArgFlag::long($long)
}};
}

View File

@ -1,16 +0,0 @@
/// Util function to skip the two leading long flag hyphens.
pub fn trim_double_hyphen(flag_text: &str) -> &str {
flag_text.get(2..).unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::trim_double_hyphen;
#[test]
fn _trim_double_hyphen() {
assert_eq!(trim_double_hyphen("--flag"), "flag");
assert_eq!(trim_double_hyphen("--verbose"), "verbose");
assert_eq!(trim_double_hyphen("--help"), "help");
}
}

View File

@ -6,7 +6,7 @@ use std::{
path::{Path, PathBuf},
};
use crate::{dialogs::Confirmation, info, oof};
use crate::{dialogs::Confirmation, info};
/// Checks if the given path represents an empty directory.
pub fn dir_is_empty(dir_path: &Path) -> bool {
@ -43,18 +43,17 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result<PathBuf> {
Ok(previous_location)
}
pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result<bool> {
match (flags.is_present("yes"), flags.is_present("no")) {
(true, true) => {
unreachable!("This should've been cutted out in the ~/src/cli.rs filter flags function.")
pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option<bool>) -> crate::Result<bool> {
match skip_questions_positively {
Some(true) => Ok(true),
Some(false) => Ok(false),
None => {
let path = to_utf(strip_cur_dir(path));
let path = Some(path.as_str());
let placeholder = Some("FILE");
Confirmation::new("Do you want to overwrite 'FILE'?", placeholder).ask(path)
}
(true, _) => return Ok(true),
(_, true) => return Ok(false),
_ => {}
}
let path = strip_cur_dir(path);
Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path)))
}
pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {

View File

@ -7,7 +7,10 @@ use std::{
time::Duration,
};
use ouch::{cli::Command, commands::run, oof};
use ouch::{
cli::{Opts, Subcommand},
commands::run,
};
use rand::{rngs::SmallRng, RngCore, SeedableRng};
use tempfile::NamedTempFile;
use utils::*;
@ -172,11 +175,15 @@ fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
// Add the suffix "results"
extraction_output_folder.push("extraction_results");
let command = Command::Decompress {
files: vec![archive_path.to_owned()],
output_folder: Some(extraction_output_folder.clone()),
let command = Opts {
yes: false,
no: false,
cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()],
output: Some(extraction_output_folder.clone()),
},
};
run(command, &oof::Flags::default()).expect("Failed to extract");
run(command, None).expect("Failed to extract");
fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
}

View File

@ -7,7 +7,10 @@ use std::{
path::{Path, PathBuf},
};
use ouch::{cli::Command, commands::run, oof};
use ouch::{
cli::{Opts, Subcommand},
commands::run,
};
pub fn create_empty_dir(at: &Path, filename: &str) -> PathBuf {
let dirname = Path::new(filename);
@ -22,8 +25,12 @@ pub fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) ->
let archive_path = String::from("archive.") + format;
let archive_path = at.join(archive_path);
let command = Command::Compress { files: paths_to_compress.to_vec(), output_path: archive_path.clone() };
run(command, &oof::Flags::default()).expect("Failed to compress test dummy files");
let command = Opts {
yes: false,
no: false,
cmd: Subcommand::Compress { files: paths_to_compress.to_vec(), output: archive_path.clone() },
};
run(command, None).expect("Failed to compress test dummy files");
archive_path
}
@ -40,11 +47,15 @@ pub fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
// Add the suffix "results"
extraction_output_folder.push("extraction_results");
let command = Command::Decompress {
files: vec![archive_path.to_owned()],
output_folder: Some(extraction_output_folder.clone()),
let command = Opts {
yes: false,
no: false,
cmd: Subcommand::Decompress {
files: vec![archive_path.to_owned()],
output: Some(extraction_output_folder.clone()),
},
};
run(command, &oof::Flags::default()).expect("Failed to extract");
run(command, None).expect("Failed to extract");
fs::read_dir(extraction_output_folder).unwrap().map(Result::unwrap).map(|entry| entry.path()).collect()
}