From e24c9ce931c836399556a56805f8c9177dd5bda5 Mon Sep 17 00:00:00 2001 From: Spyros Roum Date: Tue, 19 Oct 2021 14:42:07 +0300 Subject: [PATCH] Replace oof with clap --- Cargo.lock | 148 +++++++++++++++++- Cargo.toml | 2 +- src/archive/tar.rs | 10 +- src/archive/zip.rs | 10 +- src/cli.rs | 229 +++++++-------------------- src/commands.rs | 43 +++-- src/error.rs | 10 +- src/lib.rs | 46 ------ src/main.rs | 11 +- src/oof/error.rs | 55 ------- src/oof/flags.rs | 109 ------------- src/oof/mod.rs | 383 --------------------------------------------- src/oof/util.rs | 16 -- src/utils.rs | 17 +- 14 files changed, 244 insertions(+), 845 deletions(-) delete mode 100644 src/oof/error.rs delete mode 100644 src/oof/flags.rs delete mode 100644 src/oof/mod.rs delete mode 100644 src/oof/util.rs diff --git a/Cargo.lock b/Cargo.lock index a800ba3..68b82c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 071672a..c42ac8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/archive/tar.rs b/src/archive/tar.rs index a5fa6f0..96f93e6 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -10,11 +10,15 @@ use tar; use walkdir::WalkDir; use crate::{ - info, oof, + info, utils::{self, Bytes}, }; -pub fn unpack_archive(reader: Box, output_folder: &Path, flags: &oof::Flags) -> crate::Result> { +pub fn unpack_archive( + reader: Box, + output_folder: &Path, + skip_questions_positively: Option, +) -> crate::Result> { let mut archive = tar::Archive::new(reader); let mut files_unpacked = vec![]; @@ -22,7 +26,7 @@ pub fn unpack_archive(reader: Box, 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; } diff --git a/src/archive/zip.rs b/src/archive/zip.rs index be47ea8..eb2187e 100644 --- a/src/archive/zip.rs +++ b/src/archive/zip.rs @@ -10,14 +10,18 @@ use walkdir::WalkDir; use zip::{self, read::ZipFile, ZipArchive}; use crate::{ - info, oof, + info, utils::{self, dir_is_empty, Bytes}, }; use self::utf8::get_invalid_utf8_paths; /// Unpacks the archive given by `archive` into the folder given by `into`. -pub fn unpack_archive(mut archive: ZipArchive, into: &Path, flags: &oof::Flags) -> crate::Result> +pub fn unpack_archive( + mut archive: ZipArchive, + into: &Path, + skip_questions_positively: Option, +) -> crate::Result> 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; } diff --git a/src/cli.rs b/src/cli.rs index d507775..ed880ae 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,81 +1,75 @@ -//! 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::{crate_authors, crate_description, crate_name, crate_version, 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(name = crate_name!(), version = crate_version!(), author = crate_authors!(), about = crate_description!())] +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, - 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, - output_folder: Option, + + /// Decompress files in a directory other than the current + #[clap(short, long, value_hint = ValueHint::DirPath)] + output: Option, }, - 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 { - // 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)> { + 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) -> 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) -> crate::Result { @@ -94,120 +88,3 @@ fn canonicalize(path: impl AsRef) -> crate::Result { fn canonicalize_files(files: &[impl AsRef]) -> crate::Result> { files.iter().map(canonicalize).collect() } - -pub fn parse_args_from(mut args: Vec) -> crate::Result { - 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 = 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>(text: &str) -> Vec { - let args = text.split_whitespace(); - args.map(OsString::from).map(T::from).collect() - } - - fn test_cli(args: &str) -> crate::Result { - 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(), - } - ); - } -} diff --git a/src/commands.rs b/src/commands.rs index de2722d..7960804 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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::to_utf, utils::{self, dir_is_empty}, Error, @@ -37,9 +37,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) -> 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); @@ -93,7 +93,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(()); } @@ -125,7 +125,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() { @@ -143,7 +143,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![]; @@ -175,21 +175,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, - formats: Vec, - output_file: fs::File, - _flags: &oof::Flags, -) -> crate::Result<()> { +fn compress_files(files: Vec, formats: Vec, output_file: fs::File) -> crate::Result<()> { let file_writer = BufWriter::with_capacity(BUFFER_CAPACITY, output_file); if let [Tar | Tgz | Zip] = *formats.as_slice() { @@ -295,7 +288,7 @@ fn decompress_file( formats: Vec, output_folder: Option<&Path>, file_name: &Path, - flags: &oof::Flags, + skip_questions_positively: Option, ) -> crate::Result<()> { // TODO: improve error message let reader = fs::File::open(&input_file_path)?; @@ -317,7 +310,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 uncompressed archive in '{}'.", to_utf(output_folder)); return Ok(()); } @@ -355,27 +348,27 @@ fn decompress_file( info!("Successfully uncompressed archive in '{}'.", to_utf(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 uncompressed archive in '{}'.", to_utf(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 uncompressed archive in '{}'.", to_utf(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 uncompressed archive in '{}'.", to_utf(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 uncompressed archive in '{}'.", to_utf(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 uncompressed archive in '{}'.", to_utf(output_folder)); } Zip => { @@ -390,7 +383,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 uncompressed archive in '{}'.", to_utf(output_folder)); } diff --git a/src/error.rs b/src/error.rs index 2916089..a8dd654 100644 --- a/src/error.rs +++ b/src/error.rs @@ -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 for Error { Self::WalkdirError { reason: err.to_string() } } } - -impl From for Error { - fn from(err: oof::OofError) -> Self { - Self::OofError(err) - } -} diff --git a/src/lib.rs b/src/lib.rs index 3c3830d..9a1a795 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}{reset} Decompresses files. - - {green}ouch compress {magenta} 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); -} diff --git a/src/main.rs b/src/main.rs index 57a5c60..9fc88ec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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) } diff --git a/src/oof/error.rs b/src/oof/error.rs deleted file mode 100644 index 299601a..0000000 --- a/src/oof/error.rs +++ /dev/null @@ -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), - } - } -} diff --git a/src/oof/flags.rs b/src/oof/flags.rs deleted file mode 100644 index 231ec5c..0000000 --- a/src/oof/flags.rs +++ /dev/null @@ -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, - 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 { - self.argument_flags.remove(flag_name) - } -} - -#[derive(Debug)] -pub enum FlagType { - None, - Short, - Long, -} - -impl FlagType { - pub fn from(text: impl AsRef) -> 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 - } - } -} diff --git a/src/oof/mod.rs b/src/oof/mod.rs deleted file mode 100644 index 788113b..0000000 --- a/src/oof/mod.rs +++ /dev/null @@ -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` (or `BTreeSet<&str>`). -pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec, subcommands: I) -> Option<&'a II> -where - I: IntoIterator, - II: AsRef, - T: AsRef, -{ - 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, flags_info: &[Flag]) -> Result<(Vec, Flags), OofError> { - let mut short_flags_info = BTreeMap::::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::>(); - - // 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(args: &[T], texts: &[U]) -> bool -where - T: AsRef, - U: AsRef, -{ - 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 { - let args = text.split_whitespace(); - args.map(OsString::from).collect() - } - - fn setup_args_scenario(arg_str: &str) -> Result<(Vec, 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) - }}; -} diff --git a/src/oof/util.rs b/src/oof/util.rs deleted file mode 100644 index 0e75350..0000000 --- a/src/oof/util.rs +++ /dev/null @@ -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"); - } -} diff --git a/src/utils.rs b/src/utils.rs index 71e0337..6e282fa 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,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 { @@ -35,17 +35,12 @@ pub fn cd_into_same_dir_as(filename: &Path) -> crate::Result { Ok(previous_location) } -pub fn user_wants_to_overwrite(path: &Path, flags: &oof::Flags) -> crate::Result { - 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.") - } - (true, _) => return Ok(true), - (_, true) => return Ok(false), - _ => {} +pub fn user_wants_to_overwrite(path: &Path, skip_questions_positively: Option) -> crate::Result { + match skip_questions_positively { + Some(true) => Ok(true), + Some(false) => Ok(false), + None => Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))), } - - Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")).ask(Some(&to_utf(path))) } pub fn to_utf(os_str: impl AsRef) -> String {