From 9fb48fd348f14665198faf94b8fc80ec1a203e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 00:19:00 -0300 Subject: [PATCH 1/8] Create subproject `oof`, a thin argparsing lib And added it to the same workspace --- Cargo.toml | 9 ++++++--- oof/Cargo.toml | 9 +++++++++ oof/src/lib.rs | 7 +++++++ 3 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 oof/Cargo.toml create mode 100644 oof/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index dc1acf3..2b02c96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +4,12 @@ version = "0.1.4" authors = ["Vinícius Rodrigues Miguel ", "João M. Bezerra "] edition = "2018" readme = "README.md" -homepage = "https://github.com/vrmiguel/ouch" repository = "https://github.com/vrmiguel/ouch" license = "MIT" keywords = ["decompression", "compression", "zip", "tar", "gzip"] categories = ["command-line-utilities", "compression", "encoding"] description = "A command-line utility for easily compressing and decompressing files and directories." - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -24,8 +22,13 @@ bzip2 = "0.4.2" flate2 = "1.0.14" zip = "0.5.11" - [profile.release] lto = true codegen-units = 1 opt-level = 3 + +[workspace] +members = [ + ".", + "oof", +] diff --git a/oof/Cargo.toml b/oof/Cargo.toml new file mode 100644 index 0000000..c134bc3 --- /dev/null +++ b/oof/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "oof" +version = "0.1.0" +authors = ["João M. Bezerra "] +edition = "2018" +description = "Ouch's argparsing library" +repository = "https://github.com/vrmiguel/ouch" + +[dependencies] diff --git a/oof/src/lib.rs b/oof/src/lib.rs new file mode 100644 index 0000000..31e1bb2 --- /dev/null +++ b/oof/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} From c83b38a87432a85b0323c153c328030842919aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 01:01:32 -0300 Subject: [PATCH 2/8] Add `oof` complete base implementation. --- oof/src/error.rs | 35 ++++ oof/src/flags.rs | 39 +++++ oof/src/lib.rs | 409 ++++++++++++++++++++++++++++++++++++++++++++++- oof/src/util.rs | 13 ++ 4 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 oof/src/error.rs create mode 100644 oof/src/flags.rs create mode 100644 oof/src/util.rs diff --git a/oof/src/error.rs b/oof/src/error.rs new file mode 100644 index 0000000..abc8671 --- /dev/null +++ b/oof/src/error.rs @@ -0,0 +1,35 @@ +use std::{error, ffi::OsString, fmt}; + +use crate::Flag; + +#[derive(Debug)] +pub enum OofError { + FlagValueConflict { + flag: Flag, + previous_value: OsString, + new_value: OsString, + }, +} + +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 + ), + } + } +} diff --git a/oof/src/flags.rs b/oof/src/flags.rs new file mode 100644 index 0000000..38ff31f --- /dev/null +++ b/oof/src/flags.rs @@ -0,0 +1,39 @@ +use std::ffi::OsStr; + +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/oof/src/lib.rs b/oof/src/lib.rs index 31e1bb2..7dcea69 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -1,7 +1,406 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +//! 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, BTreeSet}, + ffi::{OsStr, OsString}, + str, +}; + +pub use error::OofError; +pub use flags::FlagType; +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 +} + +/// 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 +pub struct ArgFlag; + +#[allow(clippy::new_ret_no_self)] +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 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, Debug)] +pub struct Flags { + pub boolean_flags: BTreeSet<&'static str>, + pub argument_flags: BTreeMap<&'static str, OsString>, +} + +impl Flags { + pub fn new() -> Self { + Self::default() + } +} + +impl Flags { + 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) + } +} + +/// 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 as valid utf-8 + let flag: &str = arg + .to_str() + .unwrap_or_else(|| panic!("User error: The flag needs to be valid utf8")); + + // 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 + let is_last_letter = i == letters.len() - 1; + + let flag_info = short_flags_info.get(&letter).unwrap_or_else(|| { + panic!("User error: Unexpected/UNKNOWN flag `letter`, error") + }); + + if !is_last_letter && flag_info.takes_value { + panic!("User error: Only the last letter can refer to flag that takes values"); + // Because "-AB argument" only works if B takes values, not A. + // That is, the short flag that takes values need 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) { + panic!("User error: duplicated, found this flag TWICE!"); + } + + // pop the next one + let flag_argument = iter.next(); + flag_argument.unwrap_or_else(|| { + panic!( + "USer errror: argument flag `argument_flag` came at last, but it \ + requires an argument" + ) + }); + + // Otherwise, insert it (TODO: grab next one and add it) + // result_flags.argument_flags.insert(flag_info.long); + } else { + // If it was already inserted + if result_flags.boolean_flags.contains(flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + // 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) + .unwrap_or_else(|| panic!("User error: Unexpected/UNKNOWN flag '{}'", 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) { + panic!("User error: duplicated, found this flag TWICE!"); + } + + let flag_argument = iter.next().unwrap_or_else(|| { + panic!( + "USer errror: argument flag `argument_flag` came at last, but it requires \ + an argument" + ) + }); + result_flags.argument_flags.insert(flag_name, flag_argument); + } else { + // If it was already inserted + if result_flags.boolean_flags.contains(&flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + // 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)) +} + +#[cfg(test)] +mod tests { + use crate::*; + + fn gen_args(text: &str) -> Vec { + let args = text.split_whitespace(); + args.map(OsString::from).collect() + } + + // 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(); + } +} + +/// Create a flag with long flag (?). +#[macro_export] +macro_rules! flag { + ($short:expr, $long:expr) => { + Flag::long($long).short($short) + }; + + ($long:expr) => { + Flag::long($long) + }; +} + +/// Create a flag with long flag (?), receives argument (?). +#[macro_export] +macro_rules! arg_flag { + ($short:expr, $long:expr) => { + ArgFlag::long($long).short($short) + }; + + ($long:expr) => { + ArgFlag::long($long) + }; +} diff --git a/oof/src/util.rs b/oof/src/util.rs new file mode 100644 index 0000000..5f301aa --- /dev/null +++ b/oof/src/util.rs @@ -0,0 +1,13 @@ +/// Util function to skip the two leading long flag hyphens. +pub fn trim_double_hyphen(flag_text: &str) -> &str { + let mut chars = flag_text.chars(); + chars.nth(1); // Skipping 2 chars + chars.as_str() +} + +/// Util function to skip the single leading short flag hyphen. +pub fn trim_single_hyphen(flag_text: &str) -> &str { + let mut chars = flag_text.chars(); + chars.next(); // Skipping 1 char + chars.as_str() +} From 5b37a117f19a036ac7b39880192c801ccd576975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 01:58:22 -0300 Subject: [PATCH 3/8] Clippy lints --- src/decompressors/tar.rs | 10 +++++----- src/decompressors/zip.rs | 12 ++++++------ src/evaluator.rs | 10 +++++----- src/utils.rs | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 272b9b2..ef5086e 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -35,11 +35,11 @@ impl TarDecompressor { let mut file = file?; let file_path = PathBuf::from(into).join(file.path()?); - if file_path.exists() { - if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { - // The user does not want to overwrite the file - continue; - } + if file_path.exists() + && !utils::permission_for_overwriting(&file_path, flags, &confirm)? + { + // The user does not want to overwrite the file + continue; } file.unpack_in(into)?; diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 3c4a5a4..fb81341 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -11,7 +11,7 @@ use super::decompressor::{DecompressionResult, Decompressor}; use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; #[cfg(unix)] -fn __unix_set_permissions(file_path: &PathBuf, file: &ZipFile) { +fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { @@ -52,11 +52,11 @@ impl ZipDecompressor { }; let file_path = into.join(file_path); - if file_path.exists() { - if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { - // The user does not want to overwrite the file - continue; - } + if file_path.exists() + && !utils::permission_for_overwriting(&file_path, flags, &confirm)? + { + // The user does not want to overwrite the file + continue; } Self::check_for_comments(&file); diff --git a/src/evaluator.rs b/src/evaluator.rs index 105ed26..4775e26 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -162,11 +162,11 @@ impl Evaluator { // TODO: use -y and -n here let output_path = output.path.clone(); - if output_path.exists() { - if !utils::permission_for_overwriting(&output_path, flags, &confirm)? { - // The user does not want to overwrite the file - return Ok(()); - } + if output_path.exists() + && !utils::permission_for_overwriting(&output_path, flags, &confirm)? + { + // The user does not want to overwrite the file + return Ok(()); } let bytes = match first_compressor { diff --git a/src/utils.rs b/src/utils.rs index c131fce..4e8a565 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -59,7 +59,7 @@ pub(crate) fn get_destination_path(dest: &Option) -> &Path { } } -pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { +pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result { let previous_location = env::current_dir()?; let parent = if let Some(parent) = filename.parent() { @@ -73,7 +73,7 @@ pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result< } pub fn permission_for_overwriting( - path: &PathBuf, + path: &Path, flags: Flags, confirm: &Confirmation, ) -> crate::Result { @@ -83,6 +83,6 @@ pub fn permission_for_overwriting( Flags::None => {} } - let file_path_str = &*path.as_path().to_string_lossy(); - Ok(confirm.ask(Some(file_path_str))?) + let file_path_str = path.to_string_lossy(); + confirm.ask(Some(&file_path_str)) } From b2d064bbb34957e3bf160e45833055ccbe4cc8bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 02:08:57 -0300 Subject: [PATCH 4/8] Moving oof flags to dedicated flags.rs --- oof/src/flags.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++- oof/src/lib.rs | 72 ++---------------------------------------------- 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/oof/src/flags.rs b/oof/src/flags.rs index 38ff31f..172e769 100644 --- a/oof/src/flags.rs +++ b/oof/src/flags.rs @@ -1,4 +1,74 @@ -use std::ffi::OsStr; +use std::{ + collections::{BTreeMap, BTreeSet}, + 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 +pub struct ArgFlag; + +#[allow(clippy::new_ret_no_self)] +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 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, Debug)] +pub struct Flags { + pub boolean_flags: BTreeSet<&'static str>, + pub argument_flags: BTreeMap<&'static str, OsString>, +} + +impl Flags { + pub fn new() -> Self { + Self::default() + } +} + +impl Flags { + 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) + } +} pub enum FlagType { None, diff --git a/oof/src/lib.rs b/oof/src/lib.rs index 7dcea69..bff131b 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -8,13 +8,12 @@ mod flags; pub mod util; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::BTreeMap, ffi::{OsStr, OsString}, - str, }; pub use error::OofError; -pub use flags::FlagType; +pub use flags::{ArgFlag, Flag, FlagType, Flags}; use util::trim_double_hyphen; /// Pop leading application `subcommand`, if valid. @@ -42,73 +41,6 @@ where None } -/// 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 -pub struct ArgFlag; - -#[allow(clippy::new_ret_no_self)] -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 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, Debug)] -pub struct Flags { - pub boolean_flags: BTreeSet<&'static str>, - pub argument_flags: BTreeMap<&'static str, OsString>, -} - -impl Flags { - pub fn new() -> Self { - Self::default() - } -} - -impl Flags { - 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) - } -} - /// Detect flags from args and filter from args. /// /// Each flag received via flags_info should must have unique long and short identifiers. From 535d4fcc930a4f99aa2a90054d866fb2acb7908a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 02:20:39 -0300 Subject: [PATCH 5/8] Added oof::matches_any_arg, can detect --help etc. --- oof/src/lib.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/oof/src/lib.rs b/oof/src/lib.rs index bff131b..2fb5122 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -212,6 +212,17 @@ pub fn filter_flags( 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 crate::*; @@ -311,6 +322,28 @@ mod tests { // 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 (?). From 0c9131c307eaddd7105405f1c3c384d1d50d1e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 03:17:55 -0300 Subject: [PATCH 6/8] Start replacing `clap` with `oof` --- Cargo.toml | 3 + oof/src/flags.rs | 5 +- oof/src/lib.rs | 66 ++++---- oof/src/util.rs | 1 + src/cli.rs | 396 ++++++++++++++++++++++++++--------------------- src/error.rs | 23 ++- src/evaluator.rs | 29 +++- src/main.rs | 5 +- 8 files changed, 309 insertions(+), 219 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b02c96..725be2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ bzip2 = "0.4.2" flate2 = "1.0.14" zip = "0.5.11" +# Dependency from workspace +oof = { path = "./oof" } + [profile.release] lto = true codegen-units = 1 diff --git a/oof/src/flags.rs b/oof/src/flags.rs index 172e769..571713c 100644 --- a/oof/src/flags.rs +++ b/oof/src/flags.rs @@ -8,9 +8,9 @@ use std::{ /// ArgFlag::long(), is actually a Flag::long(), but sets a internal attribute. /// /// Examples in here pls +#[derive(Debug)] pub struct ArgFlag; -#[allow(clippy::new_ret_no_self)] impl ArgFlag { pub fn long(name: &'static str) -> Flag { Flag { @@ -44,7 +44,7 @@ impl Flag { } } -#[derive(Default, Debug)] +#[derive(Default, PartialEq, Eq, Debug)] pub struct Flags { pub boolean_flags: BTreeSet<&'static str>, pub argument_flags: BTreeMap<&'static str, OsString>, @@ -70,6 +70,7 @@ impl Flags { } } +#[derive(Debug)] pub enum FlagType { None, Short, diff --git a/oof/src/lib.rs b/oof/src/lib.rs index 2fb5122..327d53e 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -173,9 +173,9 @@ pub fn filter_flags( if let FlagType::Long = flag_type { let flag = trim_double_hyphen(flag); - let flag_info = long_flags_info - .get(flag) - .unwrap_or_else(|| panic!("User error: Unexpected/UNKNOWN flag '{}'", flag)); + let flag_info = long_flags_info.get(flag).unwrap_or_else(|| { + panic!("User error: Unexpected/UNKNOWN flag '{}'", flag); + }); let flag_name = flag_info.long; @@ -275,26 +275,26 @@ mod tests { 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"), - ]; + // #[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"), - ]; + // 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); - } + // assert_eq!(flags_info, expected); + // } #[test] // TODO: remove should_panic and use proper error handling inside of filter_args @@ -349,23 +349,23 @@ mod tests { /// Create a flag with long flag (?). #[macro_export] macro_rules! flag { - ($short:expr, $long:expr) => { - Flag::long($long).short($short) - }; + ($short:expr, $long:expr) => {{ + oof::Flag::long($long).short($short) + }}; - ($long:expr) => { - Flag::long($long) - }; + ($long:expr) => {{ + oof::Flag::long($long) + }}; } /// Create a flag with long flag (?), receives argument (?). #[macro_export] macro_rules! arg_flag { - ($short:expr, $long:expr) => { - ArgFlag::long($long).short($short) - }; + ($short:expr, $long:expr) => {{ + oof::ArgFlag::long($long).short($short) + }}; - ($long:expr) => { - ArgFlag::long($long) - }; + ($long:expr) => {{ + oof::ArgFlag::long($long) + }}; } diff --git a/oof/src/util.rs b/oof/src/util.rs index 5f301aa..dddfb3d 100644 --- a/oof/src/util.rs +++ b/oof/src/util.rs @@ -8,6 +8,7 @@ pub fn trim_double_hyphen(flag_text: &str) -> &str { /// Util function to skip the single leading short flag hyphen. pub fn trim_single_hyphen(flag_text: &str) -> &str { let mut chars = flag_text.chars(); + chars.next(); // Skipping 1 char chars.as_str() } diff --git a/src/cli.rs b/src/cli.rs index efab31e..cb31aba 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,212 +1,260 @@ use std::{ convert::TryFrom, + env, + ffi::OsString, fs, path::{Path, PathBuf}, vec::Vec, }; -use clap::{Arg, Values}; -use colored::Colorize; +use oof::{arg_flag, flag}; -use crate::{extension::Extension, file::File}; +// use clap::{Arg, Values}; +// use colored::Colorize; + +// use crate::{extension::Extension, file::File}; +use crate::file::File; #[derive(PartialEq, Eq, Debug)] -pub enum CommandKind { - Compression( - /// Files to be compressed - Vec, - ), - Decompression( - /// Files to be decompressed and their extensions - Vec, - ), +pub enum Command { + /// Files to be compressed + Compress { + files: Vec, + flags: oof::Flags, + }, + /// Files to be decompressed and their extensions + Decompress { + files: Vec, + output_folder: Option, + flags: oof::Flags, + }, + ShowHelp, + ShowVersion, } -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub enum Flags { - // No flags supplied - None, - // Flag -y, --yes supplied - AlwaysYes, - // Flag -n, --no supplied - AlwaysNo, -} +// #[derive(PartialEq, Eq, Debug)] +// pub struct Command { +// pub kind: CommandKind, +// pub output: Option, +// } -#[derive(PartialEq, Eq, Debug)] -pub struct Command { - pub kind: CommandKind, - pub output: Option, -} +// // pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { +// // clap::App::new("ouch") +// // .version("0.1.4") +// // .about("ouch is a unified compression & decompression utility") +// // .after_help( +// // "ouch infers what to based on the extensions of the input files and output file received. +// // Examples: `ouch -i movies.tar.gz classes.zip -o Videos/` in order to decompress files into a folder. +// // `ouch -i headers/ sources/ Makefile -o my-project.tar.gz` +// // `ouch -i image{1..50}.jpeg -o images.zip` +// // Please relate any issues or contribute at https://github.com/vrmiguel/ouch") +// // .author("Vinícius R. Miguel") +// // .help_message("Displays this message and exits") +// // .settings(&[ +// // clap::AppSettings::ColoredHelp, +// // clap::AppSettings::ArgRequiredElseHelp, +// // ]) +// // .arg( +// // Arg::with_name("input") +// // .required(true) +// // .multiple(true) +// // .long("input") +// // .short("i") +// // .help("The input files or directories.") +// // .takes_value(true), +// // ) +// // .arg( +// // Arg::with_name("output") +// // // --output/-o not required when output can be inferred from the input files +// // .required(false) +// // .multiple(false) +// // .long("output") +// // .short("o") +// // .help("The output directory or compressed file.") +// // .takes_value(true), +// // ) +// // .arg( +// // Arg::with_name("yes") +// // .required(false) +// // .multiple(false) +// // .long("yes") +// // .short("y") +// // .help("Says yes to all confirmation dialogs.") +// // .conflicts_with("no") +// // .takes_value(false), +// // ) +// // .arg( +// // Arg::with_name("no") +// // .required(false) +// // .multiple(false) +// // .long("no") +// // .short("n") +// // .help("Says no to all confirmation dialogs.") +// // .conflicts_with("yes") +// // .takes_value(false), +// // ) +// // } -pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { - clap::App::new("ouch") - .version("0.1.4") - .about("ouch is a unified compression & decompression utility") - .after_help( -"ouch infers what to based on the extensions of the input files and output file received. -Examples: `ouch -i movies.tar.gz classes.zip -o Videos/` in order to decompress files into a folder. - `ouch -i headers/ sources/ Makefile -o my-project.tar.gz` - `ouch -i image{1..50}.jpeg -o images.zip` -Please relate any issues or contribute at https://github.com/vrmiguel/ouch") - .author("Vinícius R. Miguel") - .help_message("Displays this message and exits") - .settings(&[ - clap::AppSettings::ColoredHelp, - clap::AppSettings::ArgRequiredElseHelp, - ]) - .arg( - Arg::with_name("input") - .required(true) - .multiple(true) - .long("input") - .short("i") - .help("The input files or directories.") - .takes_value(true), - ) - .arg( - Arg::with_name("output") - // --output/-o not required when output can be inferred from the input files - .required(false) - .multiple(false) - .long("output") - .short("o") - .help("The output directory or compressed file.") - .takes_value(true), - ) - .arg( - Arg::with_name("yes") - .required(false) - .multiple(false) - .long("yes") - .short("y") - .help("Says yes to all confirmation dialogs.") - .conflicts_with("no") - .takes_value(false), - ) - .arg( - Arg::with_name("no") - .required(false) - .multiple(false) - .long("no") - .short("n") - .help("Says no to all confirmation dialogs.") - .conflicts_with("yes") - .takes_value(false), - ) -} +// // pub fn get_matches() -> clap::ArgMatches<'static> { +// // clap_app().get_matches() +// // } -pub fn get_matches() -> clap::ArgMatches<'static> { - clap_app().get_matches() -} +// pub fn parse_matches(matches: clap::ArgMatches<'static>) -> { +// let flag = match (matches.is_present("yes"), matches.is_present("no")) { +// (true, true) => unreachable!(), +// (true, _) => Flags::AlwaysYes, +// (_, true) => Flags::AlwaysNo, +// (_, _) => Flags::None, +// }; -pub fn parse_matches(matches: clap::ArgMatches<'static>) -> crate::Result<(Command, Flags)> { - let flag = match (matches.is_present("yes"), matches.is_present("no")) { - (true, true) => unreachable!(), - (true, _) => Flags::AlwaysYes, - (_, true) => Flags::AlwaysNo, - (_, _) => Flags::None, - }; +// Ok((Command::try_from(matches)?, flag)) +// } - Ok((Command::try_from(matches)?, flag)) -} +// impl TryFrom> for Command { +// type Error = crate::Error; -impl TryFrom> for Command { - type Error = crate::Error; +// fn try_from(matches: clap::ArgMatches<'static>) -> crate::Result { +// let process_decompressible_input = |input_files: Values| { +// let input_files = +// input_files.map(|filename| (Path::new(filename), Extension::new(filename))); - fn try_from(matches: clap::ArgMatches<'static>) -> crate::Result { - let process_decompressible_input = |input_files: Values| { - let input_files = - input_files.map(|filename| (Path::new(filename), Extension::new(filename))); +// for file in input_files.clone() { +// match file { +// (filename, Ok(_)) => { +// let path = Path::new(filename); +// if !path.exists() { +// return Err(crate::Error::FileNotFound(filename.into())); +// } +// } +// (filename, Err(_)) => { +// return Err(crate::Error::InputsMustHaveBeenDecompressible( +// filename.into(), +// )); +// } +// } +// } - for file in input_files.clone() { - match file { - (filename, Ok(_)) => { - let path = Path::new(filename); - if !path.exists() { - return Err(crate::Error::FileNotFound(filename.into())); - } - } - (filename, Err(_)) => { - return Err(crate::Error::InputsMustHaveBeenDecompressible( - filename.into(), - )); - } - } - } +// Ok(input_files +// .map(|(filename, extension)| { +// (fs::canonicalize(filename).unwrap(), extension.unwrap()) +// }) +// .map(File::from) +// .collect::>()) +// }; - Ok(input_files - .map(|(filename, extension)| { - (fs::canonicalize(filename).unwrap(), extension.unwrap()) - }) - .map(File::from) - .collect::>()) - }; +// // Possibilities: +// // * Case 1: output not supplied, therefore try to infer output by checking if all input files are decompressible +// // * Case 2: output supplied - // Possibilities: - // * Case 1: output not supplied, therefore try to infer output by checking if all input files are decompressible - // * Case 2: output supplied +// let output_was_supplied = matches.is_present("output"); - let output_was_supplied = matches.is_present("output"); +// let input_files = matches.values_of("input").unwrap(); // Safe to unwrap since input is an obligatory argument - let input_files = matches.values_of("input").unwrap(); // Safe to unwrap since input is an obligatory argument +// if output_was_supplied { +// let output_file = matches.value_of("output").unwrap(); // Safe unwrap since we've established that output was supplied - if output_was_supplied { - let output_file = matches.value_of("output").unwrap(); // Safe unwrap since we've established that output was supplied +// let output_file_extension = Extension::new(output_file); - let output_file_extension = Extension::new(output_file); +// let output_is_compressible = output_file_extension.is_ok(); +// if output_is_compressible { +// // The supplied output is compressible, so we'll compress our inputs to it - let output_is_compressible = output_file_extension.is_ok(); - if output_is_compressible { - // The supplied output is compressible, so we'll compress our inputs to it +// let canonical_paths = input_files.clone().map(Path::new).map(fs::canonicalize); +// for (filename, canonical_path) in input_files.zip(canonical_paths.clone()) { +// if let Err(err) = canonical_path { +// let path = PathBuf::from(filename); +// if !path.exists() { +// return Err(crate::Error::FileNotFound(path)); +// } - let canonical_paths = input_files.clone().map(Path::new).map(fs::canonicalize); - for (filename, canonical_path) in input_files.zip(canonical_paths.clone()) { - if let Err(err) = canonical_path { - let path = PathBuf::from(filename); - if !path.exists() { - return Err(crate::Error::FileNotFound(path)); - } +// eprintln!("{} {}", "[ERROR]".red(), err); +// return Err(crate::Error::IoError); +// } +// } - eprintln!("{} {}", "[ERROR]".red(), err); - return Err(crate::Error::IoError); - } - } +// let input_files = canonical_paths.map(Result::unwrap).collect(); - let input_files = canonical_paths.map(Result::unwrap).collect(); +// Ok(Command { +// kind: CommandKind::Compression(input_files), +// output: Some(File { +// path: output_file.into(), +// contents_in_memory: None, +// extension: Some(output_file_extension.unwrap()), +// }), +// }) +// } else { +// // Output not supplied +// // Checking if input files are decompressible - Ok(Command { - kind: CommandKind::Compression(input_files), - output: Some(File { - path: output_file.into(), - contents_in_memory: None, - extension: Some(output_file_extension.unwrap()), - }), - }) - } else { - // Output not supplied - // Checking if input files are decompressible +// let input_files = process_decompressible_input(input_files)?; - let input_files = process_decompressible_input(input_files)?; +// Ok(Command { +// kind: CommandKind::Decompression(input_files), +// output: Some(File { +// path: output_file.into(), +// contents_in_memory: None, +// extension: None, +// }), +// }) +// } +// } else { +// // else: output file not supplied +// // Case 1: all input files are decompressible +// // Case 2: error +// let input_files = process_decompressible_input(input_files)?; - Ok(Command { - kind: CommandKind::Decompression(input_files), - output: Some(File { - path: output_file.into(), - contents_in_memory: None, - extension: None, - }), - }) - } - } else { - // else: output file not supplied - // Case 1: all input files are decompressible - // Case 2: error - let input_files = process_decompressible_input(input_files)?; +// Ok(Command { +// kind: CommandKind::Decompression(input_files), +// output: None, +// }) +// } +// } +// } - Ok(Command { - kind: CommandKind::Decompression(input_files), - output: None, +pub fn parse_args_and_flags() -> crate::Result { + let args: Vec = env::args_os().skip(1).collect(); + + if oof::matches_any_arg(&args, &["--help", "-h"]) { + return Ok(Command::ShowHelp); + } + + if oof::matches_any_arg(&args, &["--version"]) { + return Ok(Command::ShowHelp); + } + + let subcommands = &["compress"]; + + let mut flags_info = vec![ + flag!('y', "yes"), + flag!('n', "no"), + // flag!('v', "verbose"), + ]; + + match oof::pop_subcommand(&mut args, subcommands) { + Some(&"compress") => { + let (args, flags) = oof::filter_flags(args, &flags_info)?; + let files = args.into_iter().map(PathBuf::from).collect(); + + todo!("Adicionar output_file, que é o files.pop() do fim"); + Ok(Command::Compress { files, flags }) + } + // Defaults to decompression when there is no subcommand + None => { + // Specific flag + flags_info.push(arg_flag!('o', "output_file")); + + // Parse flags + let (args, flags) = oof::filter_flags(args, &flags_info)?; + + let files = args.into_iter().map(PathBuf::from).collect(); + let output_folder = flags.take_arg("output_folder").map(PathBuf::from); + + Ok(Command::Decompress { + files, + output_folder, + flags, }) } + _ => unreachable!("You should match each subcommand passed."), } } diff --git a/src/error.rs b/src/error.rs index 60f8316..5767370 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,7 +18,7 @@ pub enum Error { InputsMustHaveBeenDecompressible(PathBuf), InternalError, CompressingRootFolder, - WalkdirError + WalkdirError, } pub type Result = std::result::Result; @@ -48,7 +48,7 @@ impl fmt::Display for Error { write!(f, "{} ", "[ERROR]".red())?; write!(f, "file '{:?}' is not decompressible", file) } - Error::WalkdirError => { + Error::WalkdirError => { // Already printed in the From block write!(f, "") } @@ -61,8 +61,17 @@ impl fmt::Display for Error { write!(f, "{} ", "[ERROR]".red())?; let spacing = " "; writeln!(f, "It seems you're trying to compress the root folder.")?; - writeln!(f, "{}This is unadvisable since ouch does compressions in-memory.", spacing)?; - write!(f, "{}Use a more appropriate tool for this, such as {}.", spacing, "rsync".green()) + writeln!( + f, + "{}This is unadvisable since ouch does compressions in-memory.", + spacing + )?; + write!( + f, + "{}Use a more appropriate tool for this, such as {}.", + spacing, + "rsync".green() + ) } Error::InternalError => { write!(f, "{} ", "[ERROR]".red())?; @@ -108,3 +117,9 @@ impl From for Error { Self::WalkdirError } } + +impl From for Error { + fn from(err: oof::OofError) -> Self { + todo!("We need to implement this properly"); + } +} diff --git a/src/evaluator.rs b/src/evaluator.rs index 4775e26..a44c1b9 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; use crate::{ - cli::{Command, CommandKind, Flags}, + cli::Command, compressors::{ BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, @@ -230,8 +230,31 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command, flags: Flags) -> crate::Result<()> { - let output = command.output.clone(); + pub fn evaluate(command: Command) -> crate::Result<()> { + // Compress { + // files: Vec, + // flags: oof::Flags, + // }, + // /// Files to be decompressed and their extensions + // Decompress { + // files: Vec, + // output_folder: Option, + // flags: oof::Flags, + // }, + // ShowHelp, + // ShowVersion, + match command { + Command::Compress { files, flags } => {} + Command::Decompress { + files, + output_folder, + flags, + } => { + // for file in files { decompress } + } + Command::ShowHelp => todo!(), + Command::ShowVersion => todo!(), + } match command.kind { CommandKind::Compression(files_to_compress) => { diff --git a/src/main.rs b/src/main.rs index 4400359..15cc88d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ fn main() { } fn run() -> crate::Result<()> { - let matches = cli::get_matches(); - let (command, flags) = cli::parse_matches(matches)?; - Evaluator::evaluate(command, flags) + let command = cli::parse_args_and_flags()?; + Evaluator::evaluate(command) } From b45f38f5fad6f953edda3da3e6367c63c3909acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 23:18:25 -0300 Subject: [PATCH 7/8] Add utils::to_utf --- src/utils.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 4e8a565..98e260a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -83,6 +83,11 @@ pub fn permission_for_overwriting( Flags::None => {} } - let file_path_str = path.to_string_lossy(); + let file_path_str = to_utf(path); confirm.ask(Some(&file_path_str)) } + +pub fn to_utf(os_str: impl AsRef) -> String { + let text = format!("{:?}", os_str.as_ref()); + text.trim_matches('"').to_string() +} From 28901ec44efc184b64b56ef9953e20eb9d17a90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 23:23:51 -0300 Subject: [PATCH 8/8] Binary size decreased by 35% (-400KB) Huge refactor, removed totally `clap` to use our argparsing instead. 500+ modified. --- Cargo.toml | 1 - src/cli.rs | 369 +++++++++++++---------------- src/compressors/compressor.rs | 4 +- src/compressors/tar.rs | 3 +- src/decompressors/decompressor.rs | 4 +- src/decompressors/tar.rs | 6 +- src/decompressors/to_memory.rs | 14 +- src/decompressors/zip.rs | 14 +- src/error.rs | 14 +- src/evaluator.rs | 113 ++++----- src/extension.rs | 49 ++-- src/file.rs | 19 +- src/main.rs | 7 +- src/test.rs | 370 +++++++++++++++--------------- src/utils.rs | 31 +-- 15 files changed, 487 insertions(+), 531 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 725be2f..dd05ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ description = "A command-line utility for easily compressing and decompressing f [dependencies] colored = "2.0.0" walkdir = "2.3.2" -clap = "2.33.3" tar = "0.4.33" xz2 = "0.1.6" bzip2 = "0.4.2" diff --git a/src/cli.rs b/src/cli.rs index cb31aba..9aa040d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,260 +1,199 @@ -use std::{ - convert::TryFrom, - env, - ffi::OsString, - fs, - path::{Path, PathBuf}, - vec::Vec, -}; +use std::{env, ffi::OsString, io, path::PathBuf, vec::Vec}; use oof::{arg_flag, flag}; -// use clap::{Arg, Values}; -// use colored::Colorize; - -// use crate::{extension::Extension, file::File}; -use crate::file::File; - #[derive(PartialEq, Eq, Debug)] pub enum Command { /// Files to be compressed Compress { files: Vec, - flags: oof::Flags, + compressed_output_path: PathBuf, }, /// Files to be decompressed and their extensions Decompress { files: Vec, output_folder: Option, - flags: oof::Flags, }, ShowHelp, ShowVersion, } -// #[derive(PartialEq, Eq, Debug)] -// pub struct Command { -// pub kind: CommandKind, -// pub output: Option, -// } +#[derive(PartialEq, Eq, Debug)] +pub struct CommandInfo { + pub command: Command, + pub flags: oof::Flags, + // pub config: Config, // From .TOML, maybe, in the future +} -// // pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { -// // clap::App::new("ouch") -// // .version("0.1.4") -// // .about("ouch is a unified compression & decompression utility") -// // .after_help( -// // "ouch infers what to based on the extensions of the input files and output file received. -// // Examples: `ouch -i movies.tar.gz classes.zip -o Videos/` in order to decompress files into a folder. -// // `ouch -i headers/ sources/ Makefile -o my-project.tar.gz` -// // `ouch -i image{1..50}.jpeg -o images.zip` -// // Please relate any issues or contribute at https://github.com/vrmiguel/ouch") -// // .author("Vinícius R. Miguel") -// // .help_message("Displays this message and exits") -// // .settings(&[ -// // clap::AppSettings::ColoredHelp, -// // clap::AppSettings::ArgRequiredElseHelp, -// // ]) -// // .arg( -// // Arg::with_name("input") -// // .required(true) -// // .multiple(true) -// // .long("input") -// // .short("i") -// // .help("The input files or directories.") -// // .takes_value(true), -// // ) -// // .arg( -// // Arg::with_name("output") -// // // --output/-o not required when output can be inferred from the input files -// // .required(false) -// // .multiple(false) -// // .long("output") -// // .short("o") -// // .help("The output directory or compressed file.") -// // .takes_value(true), -// // ) -// // .arg( -// // Arg::with_name("yes") -// // .required(false) -// // .multiple(false) -// // .long("yes") -// // .short("y") -// // .help("Says yes to all confirmation dialogs.") -// // .conflicts_with("no") -// // .takes_value(false), -// // ) -// // .arg( -// // Arg::with_name("no") -// // .required(false) -// // .multiple(false) -// // .long("no") -// // .short("n") -// // .help("Says no to all confirmation dialogs.") -// // .conflicts_with("yes") -// // .takes_value(false), -// // ) -// // } +/// Calls parse_args_and_flags_from using std::env::args_os ( argv ) +pub fn parse_args() -> crate::Result { + let args = env::args_os().skip(1).collect(); + parse_args_from(args) +} -// // pub fn get_matches() -> clap::ArgMatches<'static> { -// // clap_app().get_matches() -// // } +pub struct ParsedArgs { + pub command: Command, + pub flags: oof::Flags, + // pub program_called: OsString, // Useful? +} -// pub fn parse_matches(matches: clap::ArgMatches<'static>) -> { -// let flag = match (matches.is_present("yes"), matches.is_present("no")) { -// (true, true) => unreachable!(), -// (true, _) => Flags::AlwaysYes, -// (_, true) => Flags::AlwaysNo, -// (_, _) => Flags::None, -// }; +fn canonicalize_files(files: Vec) -> io::Result> { + files.into_iter().map(|path| path.canonicalize()).collect() +} -// Ok((Command::try_from(matches)?, flag)) -// } - -// impl TryFrom> for Command { -// type Error = crate::Error; - -// fn try_from(matches: clap::ArgMatches<'static>) -> crate::Result { -// let process_decompressible_input = |input_files: Values| { -// let input_files = -// input_files.map(|filename| (Path::new(filename), Extension::new(filename))); - -// for file in input_files.clone() { -// match file { -// (filename, Ok(_)) => { -// let path = Path::new(filename); -// if !path.exists() { -// return Err(crate::Error::FileNotFound(filename.into())); -// } -// } -// (filename, Err(_)) => { -// return Err(crate::Error::InputsMustHaveBeenDecompressible( -// filename.into(), -// )); -// } -// } -// } - -// Ok(input_files -// .map(|(filename, extension)| { -// (fs::canonicalize(filename).unwrap(), extension.unwrap()) -// }) -// .map(File::from) -// .collect::>()) -// }; - -// // Possibilities: -// // * Case 1: output not supplied, therefore try to infer output by checking if all input files are decompressible -// // * Case 2: output supplied - -// let output_was_supplied = matches.is_present("output"); - -// let input_files = matches.values_of("input").unwrap(); // Safe to unwrap since input is an obligatory argument - -// if output_was_supplied { -// let output_file = matches.value_of("output").unwrap(); // Safe unwrap since we've established that output was supplied - -// let output_file_extension = Extension::new(output_file); - -// let output_is_compressible = output_file_extension.is_ok(); -// if output_is_compressible { -// // The supplied output is compressible, so we'll compress our inputs to it - -// let canonical_paths = input_files.clone().map(Path::new).map(fs::canonicalize); -// for (filename, canonical_path) in input_files.zip(canonical_paths.clone()) { -// if let Err(err) = canonical_path { -// let path = PathBuf::from(filename); -// if !path.exists() { -// return Err(crate::Error::FileNotFound(path)); -// } - -// eprintln!("{} {}", "[ERROR]".red(), err); -// return Err(crate::Error::IoError); -// } -// } - -// let input_files = canonical_paths.map(Result::unwrap).collect(); - -// Ok(Command { -// kind: CommandKind::Compression(input_files), -// output: Some(File { -// path: output_file.into(), -// contents_in_memory: None, -// extension: Some(output_file_extension.unwrap()), -// }), -// }) -// } else { -// // Output not supplied -// // Checking if input files are decompressible - -// let input_files = process_decompressible_input(input_files)?; - -// Ok(Command { -// kind: CommandKind::Decompression(input_files), -// output: Some(File { -// path: output_file.into(), -// contents_in_memory: None, -// extension: None, -// }), -// }) -// } -// } else { -// // else: output file not supplied -// // Case 1: all input files are decompressible -// // Case 2: error -// let input_files = process_decompressible_input(input_files)?; - -// Ok(Command { -// kind: CommandKind::Decompression(input_files), -// output: None, -// }) -// } -// } -// } - -pub fn parse_args_and_flags() -> crate::Result { - let args: Vec = env::args_os().skip(1).collect(); - - if oof::matches_any_arg(&args, &["--help", "-h"]) { - return Ok(Command::ShowHelp); +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(Command::ShowHelp); + return Ok(ParsedArgs { + command: Command::ShowVersion, + flags: oof::Flags::default(), + }); } let subcommands = &["compress"]; - let mut flags_info = vec![ - flag!('y', "yes"), - flag!('n', "no"), - // flag!('v', "verbose"), - ]; + let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")]; - match oof::pop_subcommand(&mut args, subcommands) { + let parsed_args = match oof::pop_subcommand(&mut args, subcommands) { Some(&"compress") => { let (args, flags) = oof::filter_flags(args, &flags_info)?; - let files = args.into_iter().map(PathBuf::from).collect(); + let mut files: Vec = args.into_iter().map(PathBuf::from).collect(); - todo!("Adicionar output_file, que é o files.pop() do fim"); - Ok(Command::Compress { files, flags }) + if files.len() < 2 { + panic!("The compress subcommands demands at least 2 arguments, see usage:......."); + } + // Safety: we checked that args.len() >= 2 + let compressed_output_path = files.pop().unwrap(); + + let files = canonicalize_files(files)?; + + let command = Command::Compress { + files, + compressed_output_path, + }; + ParsedArgs { command, flags } } // Defaults to decompression when there is no subcommand None => { - // Specific flag - flags_info.push(arg_flag!('o', "output_file")); + flags_info.push(arg_flag!('o', "output")); // Parse flags - let (args, flags) = oof::filter_flags(args, &flags_info)?; + let (args, mut flags) = oof::filter_flags(args, &flags_info)?; - let files = args.into_iter().map(PathBuf::from).collect(); - let output_folder = flags.take_arg("output_folder").map(PathBuf::from); + let files: Vec<_> = args.into_iter().map(PathBuf::from).collect(); + let output_folder = flags.take_arg("output").map(PathBuf::from); - Ok(Command::Decompress { + // Is the output here fully correct? + // With the paths not canonicalized? + + let command = Command::Decompress { files, output_folder, - flags, - }) + }; + ParsedArgs { command, flags } } _ => 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).collect() + } + + // // util for test the argument parsing + // macro_rules! test { + // ($expected_command:expr, $input_text:expr) => {{ + // assert_eq!( + // $expected_command, + // oof::try_arg_parsing($input_text.split_whitespace()) + // ) + // }}; + // } + + macro_rules! parse { + ($input_text:expr) => {{ + let args = gen_args($input_text); + parse_args_from(args).unwrap() + }}; + } + + #[test] + // The absolute flags that ignore all the other argparsing rules are --help and --version + fn test_absolute_flags() { + let expected = Command::ShowHelp; + assert_eq!(expected, parse!("").command); + assert_eq!(expected, parse!("-h").command); + assert_eq!(expected, parse!("--help").command); + assert_eq!(expected, parse!("aaaaaaaa --help -o -e aaa").command); + assert_eq!(expected, parse!("aaaaaaaa -h").command); + assert_eq!(expected, parse!("--help compress aaaaaaaa").command); + assert_eq!(expected, parse!("compress --help").command); + assert_eq!(expected, parse!("--version --help").command); + assert_eq!(expected, parse!("aaaaaaaa -v aaaa -h").command); + + let expected = Command::ShowVersion; + assert_eq!(expected, parse!("ouch --version").command); + assert_eq!(expected, parse!("ouch a --version b").command); + } + + #[test] + fn test_arg_parsing_compress_subcommand() { + let files = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + let expected = Command::Compress { + files, + compressed_output_path: "d".into(), + }; + assert_eq!(expected, parse!("compress a b c d").command); + } + + #[test] + fn test_arg_parsing_decompress_subcommand() { + let files: Vec<_> = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + let expected = Command::Decompress { + files: files.clone(), + output_folder: None, + }; + assert_eq!(expected, parse!("a b c").command); + + let expected = Command::Decompress { + files, + output_folder: Some("folder".into()), + }; + assert_eq!(expected, parse!("a b c --output folder").command); + assert_eq!(expected, parse!("a b --output folder c").command); + assert_eq!(expected, parse!("a --output folder b c").command); + assert_eq!(expected, parse!("--output folder a b c").command); + } + + // #[test] + // fn test_arg_parsing_decompress_subcommand() { + // let files: Vec = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + // let expected = Ok(Command::Decompress { + // files: files.clone(), + // }); + // test!(expected, "ouch a b c"); + + // let files: Vec = ["a", "b", "c", "d"].iter().map(PathBuf::from).collect(); + + // let expected = Ok(Command::Decompress { + // files: files.clone(), + // }); + // test!(expected, "ouch a b c d"); + // } } diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 8f885fd..77ff2b8 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -8,9 +8,9 @@ use crate::file::File; // FileInMemory(Vec) // } -pub enum Entry { +pub enum Entry<'a> { Files(Vec), - InMemory(File), + InMemory(File<'a>), } pub trait Compressor { diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 0bf89ff..67573a6 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -5,8 +5,7 @@ use tar::Builder; use walkdir::WalkDir; use super::compressor::Entry; -use crate::utils; -use crate::{compressors::Compressor, file::File}; +use crate::{compressors::Compressor, file::File, utils}; pub struct TarCompressor {} diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index 9d004c9..d14bd69 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{cli::Flags, file::File}; +use crate::file::File; pub enum DecompressionResult { FilesUnpacked(Vec), @@ -12,6 +12,6 @@ pub trait Decompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result; } diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index ef5086e..c52868c 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -8,13 +8,13 @@ use colored::Colorize; use tar::{self, Archive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[derive(Debug)] pub struct TarDecompressor {} impl TarDecompressor { - fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result> { println!( "{}: attempting to decompress {:?}", "ouch".bright_blue(), @@ -64,7 +64,7 @@ impl Decompressor for TarDecompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result { let destination_path = utils::get_destination_path(into); diff --git a/src/decompressors/to_memory.rs b/src/decompressors/to_memory.rs index 5ac3f31..810a29c 100644 --- a/src/decompressors/to_memory.rs +++ b/src/decompressors/to_memory.rs @@ -6,7 +6,7 @@ use std::{ use colored::Colorize; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, utils}; +use crate::utils; // use niffler; use crate::{extension::CompressionFormat, file::File}; @@ -28,8 +28,8 @@ fn get_decoder<'a>( } impl DecompressorToMemory { - fn unpack_file(from: &Path, format: CompressionFormat) -> crate::Result> { - let file = std::fs::read(from)?; + fn unpack_file(path: &Path, format: CompressionFormat) -> crate::Result> { + let file = std::fs::read(path)?; let mut reader = get_decoder(format, Box::new(&file[..])); @@ -39,7 +39,7 @@ impl DecompressorToMemory { println!( "{}: {:?} extracted into memory ({} bytes).", "info".yellow(), - from, + path, bytes_read ); @@ -66,7 +66,7 @@ impl Decompressor for GzipDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into) } @@ -77,7 +77,7 @@ impl Decompressor for BzipDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into) } @@ -88,7 +88,7 @@ impl Decompressor for LzmaDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into) } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index fb81341..48971a8 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -8,7 +8,7 @@ use colored::Colorize; use zip::{self, read::ZipFile, ZipArchive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[cfg(unix)] fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { @@ -34,13 +34,13 @@ impl ZipDecompressor { } } - pub fn zip_decompress( - archive: &mut ZipArchive, + pub fn zip_decompress( + archive: &mut ZipArchive, into: &Path, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result> where - T: Read + Seek, + R: Read + Seek, { let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let mut unpacked_files = vec![]; @@ -94,7 +94,7 @@ impl ZipDecompressor { Ok(unpacked_files) } - fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result> { println!("{} decompressing {:?}", "[OUCH]".bright_blue(), &from.path); match from.contents_in_memory { @@ -119,7 +119,7 @@ impl Decompressor for ZipDecompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result { let destination_path = utils::get_destination_path(into); diff --git a/src/error.rs b/src/error.rs index 5767370..1ffd474 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,7 +15,7 @@ pub enum Error { InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), - InputsMustHaveBeenDecompressible(PathBuf), + // InputsMustBeDecompressible(PathBuf), InternalError, CompressingRootFolder, WalkdirError, @@ -44,10 +44,10 @@ impl fmt::Display for Error { write!(f, "{} ", "[ERROR]".red())?; write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) } - Error::InputsMustHaveBeenDecompressible(file) => { - write!(f, "{} ", "[ERROR]".red())?; - write!(f, "file '{:?}' is not decompressible", file) - } + // Error::InputsMustBeDecompressible(file) => { + // write!(f, "{} ", "[ERROR]".red())?; + // write!(f, "file '{:?}' is not decompressible", file) + // } Error::WalkdirError => { // Already printed in the From block write!(f, "") @@ -88,7 +88,7 @@ impl fmt::Display for Error { impl From for Error { fn from(err: std::io::Error) -> Self { match err.kind() { - std::io::ErrorKind::NotFound => Self::FileNotFound("".into()), + std::io::ErrorKind::NotFound => panic!("{}", err), std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, _other => { @@ -119,7 +119,7 @@ impl From for Error { } impl From for Error { - fn from(err: oof::OofError) -> Self { + fn from(_err: oof::OofError) -> Self { todo!("We need to implement this properly"); } } diff --git a/src/evaluator.rs b/src/evaluator.rs index a44c1b9..b77b7df 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,4 +1,8 @@ -use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; use colored::Colorize; @@ -20,10 +24,13 @@ use crate::{ pub struct Evaluator {} +type BoxedCompressor = Box; +type BoxedDecompressor = Box; + impl Evaluator { pub fn get_compressor( file: &File, - ) -> crate::Result<(Option>, Box)> { + ) -> crate::Result<(Option, BoxedCompressor)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -65,7 +72,7 @@ impl Evaluator { pub fn get_decompressor( file: &File, - ) -> crate::Result<(Option>, Box)> { + ) -> crate::Result<(Option, BoxedDecompressor)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -100,24 +107,25 @@ impl Evaluator { fn decompress_file_in_memory( bytes: Vec, - file_path: PathBuf, + file_path: &Path, decompressor: Option>, - output_file: &Option, + output_file: Option, extension: Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result<()> { - let output_file_path = utils::get_destination_path(output_file); + let output_file_path = utils::get_destination_path(&output_file); - let mut filename = file_path + let file_name = file_path .file_stem() - .unwrap_or_else(|| output_file_path.as_os_str()); + .map(Path::new) + .unwrap_or(output_file_path); - if filename == "." { + if "." == file_name.as_os_str() { // I believe this is only possible when the supplied input has a name // of the sort `.tar` or `.zip' and no output has been supplied. - filename = OsStr::new("ouch-output"); + // file_name = OsStr::new("ouch-output"); + todo!("Pending review, what is this supposed to do??"); } - let filename = PathBuf::from(filename); // If there is a decompressor to use, we'll create a file in-memory and decompress it let decompressor = match decompressor { @@ -125,43 +133,47 @@ impl Evaluator { None => { // There is no more processing to be done on the input file (or there is but currently unsupported) // Therefore, we'll save what we have in memory into a file. - println!("{}: saving to {:?}.", "info".yellow(), filename); + println!("{}: saving to {:?}.", "info".yellow(), file_name); - if filename.exists() { + if file_name.exists() { let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - if !utils::permission_for_overwriting(&filename, flags, &confirm)? { + if !utils::permission_for_overwriting(&file_name, flags, &confirm)? { return Ok(()); } } - let mut f = fs::File::create(output_file_path.join(filename))?; + let mut f = fs::File::create(output_file_path.join(file_name))?; f.write_all(&bytes)?; return Ok(()); } }; let file = File { - path: filename, + path: file_name, contents_in_memory: Some(bytes), extension, }; - let decompression_result = decompressor.decompress(file, output_file, flags)?; + let decompression_result = decompressor.decompress(file, &output_file, flags)?; if let DecompressionResult::FileInMemory(_) = decompression_result { - // Should not be reachable. - unreachable!(); + unreachable!("Shouldn't"); } Ok(()) } - fn compress_files(files: Vec, mut output: File, flags: Flags) -> crate::Result<()> { + fn compress_files( + files: Vec, + output_path: &Path, + flags: &oof::Flags, + ) -> crate::Result<()> { + let mut output = File::from(output_path)?; + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let (first_compressor, second_compressor) = Self::get_compressor(&output)?; // TODO: use -y and -n here - let output_path = output.path.clone(); if output_path.exists() && !utils::permission_for_overwriting(&output_path, flags, &confirm)? { @@ -187,7 +199,7 @@ impl Evaluator { println!( "{}: writing to {:?}. ({} bytes)", "info".yellow(), - &output_path, + output_path, bytes.len() ); fs::write(output_path, bytes)?; @@ -195,13 +207,21 @@ impl Evaluator { Ok(()) } - fn decompress_file(file: File, output: &Option, flags: Flags) -> crate::Result<()> { + fn decompress_file( + file_path: &Path, + output: Option<&Path>, + flags: &oof::Flags, + ) -> crate::Result<()> { + let file = File::from(file_path)?; + let output = match output { + Some(inner) => Some(File::from(inner)?), + None => None, + }; let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; - let file_path = file.path.clone(); let extension = file.extension.clone(); - let decompression_result = second_decompressor.decompress(file, output, flags)?; + let decompression_result = second_decompressor.decompress(file, &output, &flags)?; match decompression_result { DecompressionResult::FileInMemory(bytes) => { @@ -230,43 +250,24 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command) -> crate::Result<()> { - // Compress { - // files: Vec, - // flags: oof::Flags, - // }, - // /// Files to be decompressed and their extensions - // Decompress { - // files: Vec, - // output_folder: Option, - // flags: oof::Flags, - // }, - // ShowHelp, - // ShowVersion, + pub fn evaluate(command: Command, flags: &oof::Flags) -> crate::Result<()> { match command { - Command::Compress { files, flags } => {} + Command::Compress { + files, + compressed_output_path, + } => Self::compress_files(files, &compressed_output_path, flags)?, Command::Decompress { files, output_folder, - flags, } => { - // for file in files { decompress } - } - Command::ShowHelp => todo!(), - Command::ShowVersion => todo!(), - } - - match command.kind { - CommandKind::Compression(files_to_compress) => { - // Safe to unwrap since output is mandatory for compression - let output = output.unwrap(); - Self::compress_files(files_to_compress, output, flags)?; - } - CommandKind::Decompression(files_to_decompress) => { - for file in files_to_decompress { - Self::decompress_file(file, &output, flags)?; + // From Option to Option<&Path> + let output_folder = output_folder.as_ref().map(|path| Path::new(path)); + for file in files.iter() { + Self::decompress_file(file, output_folder, flags)?; } } + Command::ShowHelp => todo!("call help function"), + Command::ShowVersion => todo!("call version function"), } Ok(()) } diff --git a/src/extension.rs b/src/extension.rs index 0510efc..f8a4029 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -7,6 +7,8 @@ use std::{ use CompressionFormat::*; +use crate::utils::to_utf; + /// Represents the extension of a file, but only really caring about /// compression formats (and .tar). /// Ex.: Extension::new("file.tar.gz") == Extension { first_ext: Some(Tar), second_ext: Gzip } @@ -16,20 +18,17 @@ pub struct Extension { pub second_ext: CompressionFormat, } -pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { - let path = Path::new(filename); +pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr)> { + let path = Path::new(file_name); - let ext = path.extension().and_then(OsStr::to_str)?; + let ext = path.extension()?; - let previous_extension = path - .file_stem() - .and_then(OsStr::to_str) - .and_then(get_extension_from_filename); + let previous_extension = path.file_stem().and_then(get_extension_from_filename); if let Some((_, prev)) = previous_extension { Some((prev, ext)) } else { - Some(("", ext)) + Some((OsStr::new(""), ext)) } } @@ -43,32 +42,32 @@ impl From for Extension { } impl Extension { - pub fn new(filename: &str) -> crate::Result { - let ext_from_str = |ext| match ext { - "zip" => Ok(Zip), - "tar" => Ok(Tar), - "gz" => Ok(Gzip), - "bz" | "bz2" => Ok(Bzip), - "xz" | "lz" | "lzma" => Ok(Lzma), - other => Err(crate::Error::UnknownExtensionError(other.into())), + pub fn from(file_name: &OsStr) -> crate::Result { + let compression_format_from = |ext: &OsStr| match ext { + _ if ext == "zip" => Ok(Zip), + _ if ext == "tar" => Ok(Tar), + _ if ext == "gz" => Ok(Gzip), + _ if ext == "bz" || ext == "bz2" => Ok(Bzip), + _ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma), + other => Err(crate::Error::UnknownExtensionError(to_utf(other))), }; - let (first_ext, second_ext) = match get_extension_from_filename(filename) { + let (first_ext, second_ext) = match get_extension_from_filename(&file_name) { Some(extension_tuple) => match extension_tuple { - ("", snd) => (None, snd), + (os_str, snd) if os_str.is_empty() => (None, snd), (fst, snd) => (Some(fst), snd), }, - None => return Err(crate::Error::MissingExtensionError(filename.into())), + None => return Err(crate::Error::MissingExtensionError(to_utf(file_name))), }; let (first_ext, second_ext) = match (first_ext, second_ext) { (None, snd) => { - let ext = ext_from_str(snd)?; + let ext = compression_format_from(snd)?; (None, ext) } (Some(fst), snd) => { - let snd = ext_from_str(snd)?; - let fst = ext_from_str(fst).ok(); + let snd = compression_format_from(snd)?; + let fst = compression_format_from(fst).ok(); (fst, snd) } }; @@ -130,9 +129,9 @@ impl TryFrom<&PathBuf> for CompressionFormat { impl TryFrom<&str> for CompressionFormat { type Error = crate::Error; - fn try_from(filename: &str) -> Result { - let filename = Path::new(filename); - let ext = match filename.extension() { + fn try_from(file_name: &str) -> Result { + let file_name = Path::new(file_name); + let ext = match file_name.extension() { Some(ext) => ext, None => return Err(crate::Error::MissingExtensionError(String::new())), }; diff --git a/src/file.rs b/src/file.rs index ddb31e5..56d2c26 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; +use std::path::Path; use crate::extension::Extension; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct File { +pub struct File<'a> { /// File's (relative) path - pub path: PathBuf, + pub path: &'a Path, /// The bytes that compose the file. /// Only used when the whole file is kept in-memory pub contents_in_memory: Option>, @@ -17,12 +17,15 @@ pub struct File { pub extension: Option, } -impl From<(PathBuf, Extension)> for File { - fn from((path, format): (PathBuf, Extension)) -> Self { - Self { +impl<'a> File<'a> { + pub fn from(path: &'a Path) -> crate::Result { + let extension = Extension::from(path.as_ref())?; + eprintln!("dev warning: Should we really ignore the errors from the convertion above?"); + + Ok(File { path, contents_in_memory: None, - extension: Some(format), - } + extension: Some(extension), + }) } } diff --git a/src/main.rs b/src/main.rs index 15cc88d..164dfce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ mod utils; use error::{Error, Result}; use evaluator::Evaluator; +use crate::cli::ParsedArgs; + fn main() { if let Err(err) = run() { println!("{}", err); @@ -20,6 +22,7 @@ fn main() { } fn run() -> crate::Result<()> { - let command = cli::parse_args_and_flags()?; - Evaluator::evaluate(command) + let ParsedArgs { command, flags } = cli::parse_args()?; + dbg!(&command); + Evaluator::evaluate(command, &flags) } diff --git a/src/test.rs b/src/test.rs index 269f31e..322606d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,211 +1,219 @@ // TODO: remove tests of CompressionFormat::try_from since that's no longer used anywhere -#[cfg(test)] -mod cli { - use std::{convert::TryFrom, fs, path::Path}; +// use std::{ +// convert::TryFrom, +// ffi::{OsStr, OsString}, +// fs, +// path::Path, +// }; - use crate::{ - cli::{clap_app, Command, CommandKind::*}, - extension::{CompressionFormat::*, Extension}, - file::File, - }; +// use crate::{ +// cli::Command, +// extension::{CompressionFormat, CompressionFormat::*, Extension}, +// file::File, +// }; - // ouch's command-line logic uses fs::canonicalize on its inputs so we cannot - // use made-up files for testing. - // make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors - fn make_dummy_file<'a, P>(path: P) -> crate::Result<()> - where - P: AsRef + 'a, - { - fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; - Ok(()) - } +// // Helper +// fn gen_args(text: &str) -> Vec { +// let args = text.split_whitespace(); +// args.map(OsString::from).collect() +// } - #[test] - fn decompress_files_into_folder() -> crate::Result<()> { - make_dummy_file("file.zip")?; - let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "-o", "folder/"]); - let command_from_matches = Command::try_from(matches)?; +// #[cfg(test)] +// mod cli { +// use super::*; - assert_eq!( - command_from_matches, - Command { - kind: Decompression(vec![File { - path: fs::canonicalize("file.zip")?, - contents_in_memory: None, - extension: Some(Extension::from(Zip)) - }]), - output: Some(File { - path: "folder".into(), - contents_in_memory: None, - extension: None - }), - } - ); +// // ouch's command-line logic uses fs::canonicalize on its inputs so we cannot +// // use made-up files for testing. +// // make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors +// fn make_dummy_file<'a, P>(path: P) -> crate::Result<()> +// where +// P: AsRef + 'a, +// { +// fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; +// Ok(()) +// } - fs::remove_file("file.zip")?; +// #[test] +// fn decompress_files_into_folder() -> crate::Result<()> { +// make_dummy_file("file.zip")?; +// let args = gen_args("ouch -i file.zip -o folder/"); +// let (command, flags) = cli::parse_args_and_flags_from(args)?; - Ok(()) - } +// assert_eq!( +// command, +// Command::Decompress { +// files: args, +// compressed_output_path: PathBuf, +// } // kind: Decompress(vec![File { +// // path: fs::canonicalize("file.zip")?, +// // contents_in_memory: None, +// // extension: Some(Extension::from(Zip)) +// // }]), +// // output: Some(File { +// // path: "folder".into(), +// // contents_in_memory: None, +// // extension: None +// // }), +// // } +// ); - #[test] - fn decompress_files() -> crate::Result<()> { - make_dummy_file("my-cool-file.zip")?; - make_dummy_file("file.tar")?; - let matches = - clap_app().get_matches_from(vec!["ouch", "-i", "my-cool-file.zip", "file.tar"]); - let command_from_matches = Command::try_from(matches)?; +// fs::remove_file("file.zip")?; - assert_eq!( - command_from_matches, - Command { - kind: Decompression(vec![ - File { - path: fs::canonicalize("my-cool-file.zip")?, - contents_in_memory: None, - extension: Some(Extension::from(Zip)) - }, - File { - path: fs::canonicalize("file.tar")?, - contents_in_memory: None, - extension: Some(Extension::from(Tar)) - } - ],), - output: None, - } - ); +// Ok(()) +// } - fs::remove_file("my-cool-file.zip")?; - fs::remove_file("file.tar")?; +// #[test] +// fn decompress_files() -> crate::Result<()> { +// make_dummy_file("my-cool-file.zip")?; +// make_dummy_file("file.tar")?; +// let matches = +// clap_app().get_matches_from(vec!["ouch", "-i", "my-cool-file.zip", "file.tar"]); +// let command_from_matches = Command::try_from(matches)?; - Ok(()) - } +// assert_eq!( +// command_from_matches, +// Command { +// kind: Decompress(vec![ +// File { +// path: fs::canonicalize("my-cool-file.zip")?, +// contents_in_memory: None, +// extension: Some(Extension::from(Zip)) +// }, +// File { +// path: fs::canonicalize("file.tar")?, +// contents_in_memory: None, +// extension: Some(Extension::from(Tar)) +// } +// ],), +// output: None, +// } +// ); - #[test] - fn compress_files() -> crate::Result<()> { - make_dummy_file("file")?; - make_dummy_file("file2.jpeg")?; - make_dummy_file("file3.ok")?; +// fs::remove_file("my-cool-file.zip")?; +// fs::remove_file("file.tar")?; - let matches = clap_app().get_matches_from(vec![ - "ouch", - "-i", - "file", - "file2.jpeg", - "file3.ok", - "-o", - "file.tar", - ]); - let command_from_matches = Command::try_from(matches)?; +// Ok(()) +// } - assert_eq!( - command_from_matches, - Command { - kind: Compression(vec![ - fs::canonicalize("file")?, - fs::canonicalize("file2.jpeg")?, - fs::canonicalize("file3.ok")? - ]), - output: Some(File { - path: "file.tar".into(), - contents_in_memory: None, - extension: Some(Extension::from(Tar)) - }), - } - ); +// #[test] +// fn compress_files() -> crate::Result<()> { +// make_dummy_file("file")?; +// make_dummy_file("file2.jpeg")?; +// make_dummy_file("file3.ok")?; - fs::remove_file("file")?; - fs::remove_file("file2.jpeg")?; - fs::remove_file("file3.ok")?; +// let matches = clap_app().get_matches_from(vec![ +// "ouch", +// "-i", +// "file", +// "file2.jpeg", +// "file3.ok", +// "-o", +// "file.tar", +// ]); +// let command_from_matches = Command::try_from(matches)?; - Ok(()) - } -} +// assert_eq!( +// command_from_matches, +// Command { +// kind: Compress(vec![ +// fs::canonicalize("file")?, +// fs::canonicalize("file2.jpeg")?, +// fs::canonicalize("file3.ok")? +// ]), +// output: Some(File { +// path: "file.tar".into(), +// contents_in_memory: None, +// extension: Some(Extension::from(Tar)) +// }), +// } +// ); -#[cfg(test)] -mod cli_errors { +// fs::remove_file("file")?; +// fs::remove_file("file2.jpeg")?; +// fs::remove_file("file3.ok")?; - use std::convert::TryFrom; +// Ok(()) +// } +// } - use crate::cli::{clap_app, Command}; +// #[cfg(test)] +// mod cli_errors { - #[test] - fn compress_files() -> crate::Result<()> { - let matches = - clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); - let res = Command::try_from(matches); +// #[test] +// fn compress_files() -> crate::Result<()> { +// let matches = +// clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); +// let res = Command::try_from(matches); - assert_eq!( - res, - Err(crate::Error::InputsMustHaveBeenDecompressible( - "a_file".into() - )) - ); +// assert_eq!( +// res, +// Err(crate::Error::InputsMustHaveBeenDecompressible( +// "a_file".into() +// )) +// ); - Ok(()) - } -} +// Ok(()) +// } +// } -#[cfg(test)] -mod extension_extraction { - use std::convert::TryFrom; +// #[cfg(test)] +// mod extension_extraction { - use crate::extension::{CompressionFormat, Extension}; +// #[test] +// fn test_extension_zip() { +// let path = "filename.tar.zip"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Zip) +// ); +// } - #[test] - fn test_extension_zip() { - let path = "filename.tar.zip"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Zip) - ); - } +// #[test] +// fn test_extension_tar_gz() { +// let extension = Extension::from(OsStr::new("folder.tar.gz")).unwrap(); +// assert_eq!( +// extension, +// Extension { +// first_ext: Some(CompressionFormat::Tar), +// second_ext: CompressionFormat::Gzip +// } +// ); +// } - #[test] - fn test_extension_tar_gz() { - let extension = Extension::new("folder.tar.gz").unwrap(); - assert_eq!( - extension, - Extension { - first_ext: Some(CompressionFormat::Tar), - second_ext: CompressionFormat::Gzip - } - ); - } +// #[test] +// fn test_extension_tar() { +// let path = "pictures.tar"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Tar) +// ); +// } - #[test] - fn test_extension_tar() { - let path = "pictures.tar"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Tar) - ); - } +// #[test] +// fn test_extension_gz() { +// let path = "passwords.tar.gz"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Gzip) +// ); +// } - #[test] - fn test_extension_gz() { - let path = "passwords.tar.gz"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Gzip) - ); - } +// #[test] +// fn test_extension_lzma() { +// let path = "mygame.tar.lzma"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Lzma) +// ); +// } - #[test] - fn test_extension_lzma() { - let path = "mygame.tar.lzma"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Lzma) - ); - } - - #[test] - fn test_extension_bz() { - let path = "songs.tar.bz"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Bzip) - ); - } -} +// #[test] +// fn test_extension_bz() { +// let path = "songs.tar.bz"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Bzip) +// ); +// } +// } diff --git a/src/utils.rs b/src/utils.rs index 98e260a..ab9fc79 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,13 @@ use std::{ - env, fs, + env, + ffi::OsStr, + fs, path::{Path, PathBuf}, }; use colored::Colorize; -use crate::{cli::Flags, dialogs::Confirmation, extension::CompressionFormat, file::File}; +use crate::{dialogs::Confirmation, extension::CompressionFormat, file::File}; pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()> where @@ -47,13 +49,12 @@ pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { Ok(()) } -pub(crate) fn get_destination_path(dest: &Option) -> &Path { +pub(crate) fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { match dest { - Some(output) => { + Some(output_file) => { // Must be None according to the way command-line arg. parsing in Ouch works - assert_eq!(output.extension, None); - - Path::new(&output.path) + assert_eq!(output_file.extension, None); + Path::new(&output_file.path) } None => Path::new("."), } @@ -68,19 +69,23 @@ pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result crate::Result { - match flags { - Flags::AlwaysYes => return Ok(true), - Flags::AlwaysNo => return Ok(false), - Flags::None => {} + match (flags.is_present("yes"), flags.is_present("false")) { + (true, true) => { + unreachable!("This shoul've been cutted out in the ~/src/cli.rs filter flags function.") + } + (true, _) => return Ok(true), + (_, true) => return Ok(false), + _ => {} } let file_path_str = to_utf(path);