mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-07 12:05:46 +00:00
Merge pull request #12 from vrmiguel/oof-argparsing
Change argparsing lib
This commit is contained in:
commit
9796bd3b6d
11
Cargo.toml
11
Cargo.toml
@ -4,28 +4,33 @@ version = "0.1.4"
|
|||||||
authors = ["Vinícius Rodrigues Miguel <vrmiguel99@gmail.com>", "João M. Bezerra <marcospb19@hotmail.com>"]
|
authors = ["Vinícius Rodrigues Miguel <vrmiguel99@gmail.com>", "João M. Bezerra <marcospb19@hotmail.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://github.com/vrmiguel/ouch"
|
|
||||||
repository = "https://github.com/vrmiguel/ouch"
|
repository = "https://github.com/vrmiguel/ouch"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["decompression", "compression", "zip", "tar", "gzip"]
|
keywords = ["decompression", "compression", "zip", "tar", "gzip"]
|
||||||
categories = ["command-line-utilities", "compression", "encoding"]
|
categories = ["command-line-utilities", "compression", "encoding"]
|
||||||
description = "A command-line utility for easily compressing and decompressing files and directories."
|
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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
colored = "2.0.0"
|
colored = "2.0.0"
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
clap = "2.33.3"
|
|
||||||
tar = "0.4.33"
|
tar = "0.4.33"
|
||||||
xz2 = "0.1.6"
|
xz2 = "0.1.6"
|
||||||
bzip2 = "0.4.2"
|
bzip2 = "0.4.2"
|
||||||
flate2 = "1.0.14"
|
flate2 = "1.0.14"
|
||||||
zip = "0.5.11"
|
zip = "0.5.11"
|
||||||
|
|
||||||
|
# Dependency from workspace
|
||||||
|
oof = { path = "./oof" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
".",
|
||||||
|
"oof",
|
||||||
|
]
|
||||||
|
9
oof/Cargo.toml
Normal file
9
oof/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "oof"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["João M. Bezerra <marcospb19@hotmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "Ouch's argparsing library"
|
||||||
|
repository = "https://github.com/vrmiguel/ouch"
|
||||||
|
|
||||||
|
[dependencies]
|
35
oof/src/error.rs
Normal file
35
oof/src/error.rs
Normal file
@ -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
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
oof/src/flags.rs
Normal file
110
oof/src/flags.rs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ArgFlag;
|
||||||
|
|
||||||
|
impl ArgFlag {
|
||||||
|
pub fn long(name: &'static str) -> Flag {
|
||||||
|
Flag {
|
||||||
|
long: name,
|
||||||
|
short: None,
|
||||||
|
takes_value: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub struct Flag {
|
||||||
|
// Also the name
|
||||||
|
pub long: &'static str,
|
||||||
|
pub short: Option<char>,
|
||||||
|
pub takes_value: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl 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: 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<OsString> {
|
||||||
|
self.argument_flags.remove(flag_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FlagType {
|
||||||
|
None,
|
||||||
|
Short,
|
||||||
|
Long,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagType {
|
||||||
|
pub fn from(text: impl AsRef<OsStr>) -> Self {
|
||||||
|
let text = text.as_ref();
|
||||||
|
|
||||||
|
let mut iter;
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
{
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
iter = text.as_bytes().iter();
|
||||||
|
}
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
{
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
iter = text.encode_wide
|
||||||
|
}
|
||||||
|
|
||||||
|
// 45 is the code for a hyphen
|
||||||
|
// Typed as 45_u16 for Windows
|
||||||
|
// Typed as 45_u8 for Unix
|
||||||
|
if let Some(45) = iter.next() {
|
||||||
|
if let Some(45) = iter.next() {
|
||||||
|
Self::Long
|
||||||
|
} else {
|
||||||
|
Self::Short
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
371
oof/src/lib.rs
Normal file
371
oof/src/lib.rs
Normal file
@ -0,0 +1,371 @@
|
|||||||
|
//! Ouch's argparsing crate.
|
||||||
|
//!
|
||||||
|
//! The usage of this crate is heavily based on boolean_flags and
|
||||||
|
//! argument_flags, there should be an more _obvious_ naming.
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod flags;
|
||||||
|
pub mod util;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
ffi::{OsStr, OsString},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use error::OofError;
|
||||||
|
pub use flags::{ArgFlag, Flag, FlagType, Flags};
|
||||||
|
use util::trim_double_hyphen;
|
||||||
|
|
||||||
|
/// Pop leading application `subcommand`, if valid.
|
||||||
|
///
|
||||||
|
/// `args` can be a Vec of `OsString` or `OsStr`
|
||||||
|
/// `subcommands` is any container that can yield `&str` through `AsRef`, can be `Vec<&str>` or
|
||||||
|
/// a GREAT `BTreeSet<String>` (or `BTreeSet<&str>`).
|
||||||
|
pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec<T>, subcommands: I) -> Option<&'a II>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = &'a II>,
|
||||||
|
II: AsRef<str>,
|
||||||
|
T: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
if args.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
for subcommand in subcommands.into_iter() {
|
||||||
|
if subcommand.as_ref() == args[0].as_ref() {
|
||||||
|
args.remove(0);
|
||||||
|
return Some(subcommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect flags from args and filter from args.
|
||||||
|
///
|
||||||
|
/// Each flag received via flags_info should must have unique long and short identifiers.
|
||||||
|
///
|
||||||
|
/// # Panics (Developer errors)
|
||||||
|
/// - If there are duplicated short flag identifiers.
|
||||||
|
/// - If there are duplicated long flag identifiers.
|
||||||
|
///
|
||||||
|
/// Both conditions cause panic because your program's flags specification is meant to have unique
|
||||||
|
/// flags. There shouldn't be two "--verbose" flags, for example.
|
||||||
|
/// Caller should guarantee it, fortunately, this can almost always be caught while prototyping in
|
||||||
|
/// debug mode, test your CLI flags once, if it works once, you're good,
|
||||||
|
///
|
||||||
|
/// # Errors (User errors)
|
||||||
|
/// - Argument flag comes at last arg, so there's no way to provide an argument.
|
||||||
|
/// - Or if it doesn't comes at last, but the rest are just flags, no possible and valid arg.
|
||||||
|
/// - Short flags with multiple letters in the same arg contain a argument flag that does not come
|
||||||
|
/// as the last one in the list (example "-oahc", where 'o', 'a', or 'h' is a argument flag, but do
|
||||||
|
/// not comes at last, so it is impossible for them to receive the required argument.
|
||||||
|
/// - User passes same flag twice (short or long, boolean or arg).
|
||||||
|
///
|
||||||
|
/// ...
|
||||||
|
pub fn filter_flags(
|
||||||
|
args: Vec<OsString>,
|
||||||
|
flags_info: &[Flag],
|
||||||
|
) -> Result<(Vec<OsString>, Flags), OofError> {
|
||||||
|
let mut short_flags_info = BTreeMap::<char, &Flag>::new();
|
||||||
|
let mut long_flags_info = BTreeMap::<&'static str, &Flag>::new();
|
||||||
|
|
||||||
|
for flag in flags_info.iter() {
|
||||||
|
// Panics if duplicated/conflicts
|
||||||
|
assert!(
|
||||||
|
!long_flags_info.contains_key(flag.long),
|
||||||
|
"DEV ERROR: duplicated long flag '{}'.",
|
||||||
|
flag.long
|
||||||
|
);
|
||||||
|
|
||||||
|
long_flags_info.insert(flag.long, &flag);
|
||||||
|
|
||||||
|
if let Some(short) = flag.short {
|
||||||
|
// Panics if duplicated/conflicts
|
||||||
|
assert!(
|
||||||
|
!short_flags_info.contains_key(&short),
|
||||||
|
"DEV ERROR: duplicated short flag '-{}'.",
|
||||||
|
short
|
||||||
|
);
|
||||||
|
short_flags_info.insert(short, &flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consume args, filter out flags, and add back to args new vec
|
||||||
|
let mut iter = args.into_iter();
|
||||||
|
let mut new_args = vec![];
|
||||||
|
let mut result_flags = Flags::new();
|
||||||
|
|
||||||
|
while let Some(arg) = iter.next() {
|
||||||
|
let flag_type = FlagType::from(&arg);
|
||||||
|
|
||||||
|
// If it isn't a flag, retrieve to `args` and skip this iteration
|
||||||
|
if let FlagType::None = flag_type {
|
||||||
|
new_args.push(arg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is a flag, now we try to interpret 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::<Vec<char>>();
|
||||||
|
|
||||||
|
// For each letter in the short arg, except the last one
|
||||||
|
for (i, letter) in letters.iter().copied().enumerate() {
|
||||||
|
// Safety: this loop only runs when len >= 1
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Says if any text matches any arg
|
||||||
|
pub fn matches_any_arg<T, U>(args: &[T], texts: &[U]) -> bool
|
||||||
|
where
|
||||||
|
T: AsRef<OsStr>,
|
||||||
|
U: AsRef<str>,
|
||||||
|
{
|
||||||
|
texts
|
||||||
|
.iter()
|
||||||
|
.any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
fn gen_args(text: &str) -> Vec<OsString> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}};
|
||||||
|
}
|
14
oof/src/util.rs
Normal file
14
oof/src/util.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/// 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()
|
||||||
|
}
|
351
src/cli.rs
351
src/cli.rs
@ -1,212 +1,199 @@
|
|||||||
use std::{
|
use std::{env, ffi::OsString, io, path::PathBuf, vec::Vec};
|
||||||
convert::TryFrom,
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use clap::{Arg, Values};
|
use oof::{arg_flag, flag};
|
||||||
use colored::Colorize;
|
|
||||||
|
|
||||||
use crate::{extension::Extension, file::File};
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum CommandKind {
|
pub enum Command {
|
||||||
Compression(
|
|
||||||
/// Files to be compressed
|
/// Files to be compressed
|
||||||
Vec<PathBuf>,
|
Compress {
|
||||||
),
|
files: Vec<PathBuf>,
|
||||||
Decompression(
|
compressed_output_path: PathBuf,
|
||||||
|
},
|
||||||
/// Files to be decompressed and their extensions
|
/// Files to be decompressed and their extensions
|
||||||
Vec<File>,
|
Decompress {
|
||||||
),
|
files: Vec<PathBuf>,
|
||||||
}
|
output_folder: Option<PathBuf>,
|
||||||
|
},
|
||||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
ShowHelp,
|
||||||
pub enum Flags {
|
ShowVersion,
|
||||||
// No flags supplied
|
|
||||||
None,
|
|
||||||
// Flag -y, --yes supplied
|
|
||||||
AlwaysYes,
|
|
||||||
// Flag -n, --no supplied
|
|
||||||
AlwaysNo,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub struct Command {
|
pub struct CommandInfo {
|
||||||
pub kind: CommandKind,
|
pub command: Command,
|
||||||
pub output: Option<File>,
|
pub flags: oof::Flags,
|
||||||
|
// pub config: Config, // From .TOML, maybe, in the future
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> {
|
/// Calls parse_args_and_flags_from using std::env::args_os ( argv )
|
||||||
clap::App::new("ouch")
|
pub fn parse_args() -> crate::Result<ParsedArgs> {
|
||||||
.version("0.1.4")
|
let args = env::args_os().skip(1).collect();
|
||||||
.about("ouch is a unified compression & decompression utility")
|
parse_args_from(args)
|
||||||
.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> {
|
pub struct ParsedArgs {
|
||||||
clap_app().get_matches()
|
pub command: Command,
|
||||||
|
pub flags: oof::Flags,
|
||||||
|
// pub program_called: OsString, // Useful?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_matches(matches: clap::ArgMatches<'static>) -> crate::Result<(Command, Flags)> {
|
fn canonicalize_files(files: Vec<PathBuf>) -> io::Result<Vec<PathBuf>> {
|
||||||
let flag = match (matches.is_present("yes"), matches.is_present("no")) {
|
files.into_iter().map(|path| path.canonicalize()).collect()
|
||||||
(true, true) => unreachable!(),
|
}
|
||||||
(true, _) => Flags::AlwaysYes,
|
|
||||||
(_, true) => Flags::AlwaysNo,
|
pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
||||||
(_, _) => Flags::None,
|
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 = &["compress"];
|
||||||
|
|
||||||
|
let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")];
|
||||||
|
|
||||||
|
let parsed_args = match oof::pop_subcommand(&mut args, subcommands) {
|
||||||
|
Some(&"compress") => {
|
||||||
|
let (args, flags) = oof::filter_flags(args, &flags_info)?;
|
||||||
|
let mut files: Vec<PathBuf> = args.into_iter().map(PathBuf::from).collect();
|
||||||
|
|
||||||
|
if files.len() < 2 {
|
||||||
|
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 => {
|
||||||
|
flags_info.push(arg_flag!('o', "output"));
|
||||||
|
|
||||||
|
// Parse flags
|
||||||
|
let (args, mut flags) = oof::filter_flags(args, &flags_info)?;
|
||||||
|
|
||||||
|
let files: Vec<_> = args.into_iter().map(PathBuf::from).collect();
|
||||||
|
let output_folder = flags.take_arg("output").map(PathBuf::from);
|
||||||
|
|
||||||
|
// Is the output here fully correct?
|
||||||
|
// With the paths not canonicalized?
|
||||||
|
|
||||||
|
let command = Command::Decompress {
|
||||||
|
files,
|
||||||
|
output_folder,
|
||||||
|
};
|
||||||
|
ParsedArgs { command, flags }
|
||||||
|
}
|
||||||
|
_ => unreachable!("You should match each subcommand passed."),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((Command::try_from(matches)?, flag))
|
Ok(parsed_args)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<clap::ArgMatches<'static>> for Command {
|
#[cfg(test)]
|
||||||
type Error = crate::Error;
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
fn try_from(matches: clap::ArgMatches<'static>) -> crate::Result<Command> {
|
fn gen_args(text: &str) -> Vec<OsString> {
|
||||||
let process_decompressible_input = |input_files: Values| {
|
let args = text.split_whitespace();
|
||||||
let input_files =
|
args.map(OsString::from).collect()
|
||||||
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
|
// // util for test the argument parsing
|
||||||
.map(|(filename, extension)| {
|
// macro_rules! test {
|
||||||
(fs::canonicalize(filename).unwrap(), extension.unwrap())
|
// ($expected_command:expr, $input_text:expr) => {{
|
||||||
})
|
// assert_eq!(
|
||||||
.map(File::from)
|
// $expected_command,
|
||||||
.collect::<Vec<_>>())
|
// 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);
|
||||||
// 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);
|
#[test]
|
||||||
return Err(crate::Error::IoError);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_files = canonical_paths.map(Result::unwrap).collect();
|
// #[test]
|
||||||
|
// fn test_arg_parsing_decompress_subcommand() {
|
||||||
|
// let files: Vec<PathBuf> = ["a", "b", "c"].iter().map(PathBuf::from).collect();
|
||||||
|
|
||||||
Ok(Command {
|
// let expected = Ok(Command::Decompress {
|
||||||
kind: CommandKind::Compression(input_files),
|
// files: files.clone(),
|
||||||
output: Some(File {
|
// });
|
||||||
path: output_file.into(),
|
// test!(expected, "ouch a b c");
|
||||||
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 files: Vec<PathBuf> = ["a", "b", "c", "d"].iter().map(PathBuf::from).collect();
|
||||||
|
|
||||||
Ok(Command {
|
// let expected = Ok(Command::Decompress {
|
||||||
kind: CommandKind::Decompression(input_files),
|
// files: files.clone(),
|
||||||
output: Some(File {
|
// });
|
||||||
path: output_file.into(),
|
// test!(expected, "ouch a b c d");
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ use crate::file::File;
|
|||||||
// FileInMemory(Vec<u8>)
|
// FileInMemory(Vec<u8>)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub enum Entry {
|
pub enum Entry<'a> {
|
||||||
Files(Vec<PathBuf>),
|
Files(Vec<PathBuf>),
|
||||||
InMemory(File),
|
InMemory(File<'a>),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Compressor {
|
pub trait Compressor {
|
||||||
|
@ -5,8 +5,7 @@ use tar::Builder;
|
|||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use super::compressor::Entry;
|
use super::compressor::Entry;
|
||||||
use crate::utils;
|
use crate::{compressors::Compressor, file::File, utils};
|
||||||
use crate::{compressors::Compressor, file::File};
|
|
||||||
|
|
||||||
pub struct TarCompressor {}
|
pub struct TarCompressor {}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::{cli::Flags, file::File};
|
use crate::file::File;
|
||||||
|
|
||||||
pub enum DecompressionResult {
|
pub enum DecompressionResult {
|
||||||
FilesUnpacked(Vec<PathBuf>),
|
FilesUnpacked(Vec<PathBuf>),
|
||||||
@ -12,6 +12,6 @@ pub trait Decompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult>;
|
) -> crate::Result<DecompressionResult>;
|
||||||
}
|
}
|
||||||
|
@ -8,13 +8,13 @@ use colored::Colorize;
|
|||||||
use tar::{self, Archive};
|
use tar::{self, Archive};
|
||||||
|
|
||||||
use super::decompressor::{DecompressionResult, Decompressor};
|
use super::decompressor::{DecompressionResult, Decompressor};
|
||||||
use crate::{cli::Flags, dialogs::Confirmation, file::File, utils};
|
use crate::{dialogs::Confirmation, file::File, utils};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TarDecompressor {}
|
pub struct TarDecompressor {}
|
||||||
|
|
||||||
impl TarDecompressor {
|
impl TarDecompressor {
|
||||||
fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result<Vec<PathBuf>> {
|
fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> {
|
||||||
println!(
|
println!(
|
||||||
"{}: attempting to decompress {:?}",
|
"{}: attempting to decompress {:?}",
|
||||||
"ouch".bright_blue(),
|
"ouch".bright_blue(),
|
||||||
@ -35,12 +35,12 @@ impl TarDecompressor {
|
|||||||
let mut file = file?;
|
let mut file = file?;
|
||||||
|
|
||||||
let file_path = PathBuf::from(into).join(file.path()?);
|
let file_path = PathBuf::from(into).join(file.path()?);
|
||||||
if file_path.exists() {
|
if file_path.exists()
|
||||||
if !utils::permission_for_overwriting(&file_path, flags, &confirm)? {
|
&& !utils::permission_for_overwriting(&file_path, flags, &confirm)?
|
||||||
|
{
|
||||||
// The user does not want to overwrite the file
|
// The user does not want to overwrite the file
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
file.unpack_in(into)?;
|
file.unpack_in(into)?;
|
||||||
|
|
||||||
@ -64,7 +64,7 @@ impl Decompressor for TarDecompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult> {
|
) -> crate::Result<DecompressionResult> {
|
||||||
let destination_path = utils::get_destination_path(into);
|
let destination_path = utils::get_destination_path(into);
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ use std::{
|
|||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
use super::decompressor::{DecompressionResult, Decompressor};
|
use super::decompressor::{DecompressionResult, Decompressor};
|
||||||
use crate::{cli::Flags, utils};
|
use crate::utils;
|
||||||
// use niffler;
|
// use niffler;
|
||||||
use crate::{extension::CompressionFormat, file::File};
|
use crate::{extension::CompressionFormat, file::File};
|
||||||
|
|
||||||
@ -28,8 +28,8 @@ fn get_decoder<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DecompressorToMemory {
|
impl DecompressorToMemory {
|
||||||
fn unpack_file(from: &Path, format: CompressionFormat) -> crate::Result<Vec<u8>> {
|
fn unpack_file(path: &Path, format: CompressionFormat) -> crate::Result<Vec<u8>> {
|
||||||
let file = std::fs::read(from)?;
|
let file = std::fs::read(path)?;
|
||||||
|
|
||||||
let mut reader = get_decoder(format, Box::new(&file[..]));
|
let mut reader = get_decoder(format, Box::new(&file[..]));
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ impl DecompressorToMemory {
|
|||||||
println!(
|
println!(
|
||||||
"{}: {:?} extracted into memory ({} bytes).",
|
"{}: {:?} extracted into memory ({} bytes).",
|
||||||
"info".yellow(),
|
"info".yellow(),
|
||||||
from,
|
path,
|
||||||
bytes_read
|
bytes_read
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ impl Decompressor for GzipDecompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
_: Flags,
|
_: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult> {
|
) -> crate::Result<DecompressionResult> {
|
||||||
DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into)
|
DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into)
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ impl Decompressor for BzipDecompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
_: Flags,
|
_: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult> {
|
) -> crate::Result<DecompressionResult> {
|
||||||
DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into)
|
DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into)
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ impl Decompressor for LzmaDecompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
_: Flags,
|
_: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult> {
|
) -> crate::Result<DecompressionResult> {
|
||||||
DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into)
|
DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into)
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@ use colored::Colorize;
|
|||||||
use zip::{self, read::ZipFile, ZipArchive};
|
use zip::{self, read::ZipFile, ZipArchive};
|
||||||
|
|
||||||
use super::decompressor::{DecompressionResult, Decompressor};
|
use super::decompressor::{DecompressionResult, Decompressor};
|
||||||
use crate::{cli::Flags, dialogs::Confirmation, file::File, utils};
|
use crate::{dialogs::Confirmation, file::File, utils};
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[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;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
if let Some(mode) = file.unix_mode() {
|
if let Some(mode) = file.unix_mode() {
|
||||||
@ -34,13 +34,13 @@ impl ZipDecompressor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn zip_decompress<T>(
|
pub fn zip_decompress<R>(
|
||||||
archive: &mut ZipArchive<T>,
|
archive: &mut ZipArchive<R>,
|
||||||
into: &Path,
|
into: &Path,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<Vec<PathBuf>>
|
) -> crate::Result<Vec<PathBuf>>
|
||||||
where
|
where
|
||||||
T: Read + Seek,
|
R: Read + Seek,
|
||||||
{
|
{
|
||||||
let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE"));
|
let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE"));
|
||||||
let mut unpacked_files = vec![];
|
let mut unpacked_files = vec![];
|
||||||
@ -52,12 +52,12 @@ impl ZipDecompressor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let file_path = into.join(file_path);
|
let file_path = into.join(file_path);
|
||||||
if file_path.exists() {
|
if file_path.exists()
|
||||||
if !utils::permission_for_overwriting(&file_path, flags, &confirm)? {
|
&& !utils::permission_for_overwriting(&file_path, flags, &confirm)?
|
||||||
|
{
|
||||||
// The user does not want to overwrite the file
|
// The user does not want to overwrite the file
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Self::check_for_comments(&file);
|
Self::check_for_comments(&file);
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ impl ZipDecompressor {
|
|||||||
Ok(unpacked_files)
|
Ok(unpacked_files)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result<Vec<PathBuf>> {
|
fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result<Vec<PathBuf>> {
|
||||||
println!("{} decompressing {:?}", "[OUCH]".bright_blue(), &from.path);
|
println!("{} decompressing {:?}", "[OUCH]".bright_blue(), &from.path);
|
||||||
|
|
||||||
match from.contents_in_memory {
|
match from.contents_in_memory {
|
||||||
@ -119,7 +119,7 @@ impl Decompressor for ZipDecompressor {
|
|||||||
&self,
|
&self,
|
||||||
from: File,
|
from: File,
|
||||||
into: &Option<File>,
|
into: &Option<File>,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<DecompressionResult> {
|
) -> crate::Result<DecompressionResult> {
|
||||||
let destination_path = utils::get_destination_path(into);
|
let destination_path = utils::get_destination_path(into);
|
||||||
|
|
||||||
|
33
src/error.rs
33
src/error.rs
@ -15,10 +15,10 @@ pub enum Error {
|
|||||||
InvalidZipArchive(&'static str),
|
InvalidZipArchive(&'static str),
|
||||||
PermissionDenied,
|
PermissionDenied,
|
||||||
UnsupportedZipArchive(&'static str),
|
UnsupportedZipArchive(&'static str),
|
||||||
InputsMustHaveBeenDecompressible(PathBuf),
|
// InputsMustBeDecompressible(PathBuf),
|
||||||
InternalError,
|
InternalError,
|
||||||
CompressingRootFolder,
|
CompressingRootFolder,
|
||||||
WalkdirError
|
WalkdirError,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
@ -44,10 +44,10 @@ impl fmt::Display for Error {
|
|||||||
write!(f, "{} ", "[ERROR]".red())?;
|
write!(f, "{} ", "[ERROR]".red())?;
|
||||||
write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename)
|
write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename)
|
||||||
}
|
}
|
||||||
Error::InputsMustHaveBeenDecompressible(file) => {
|
// Error::InputsMustBeDecompressible(file) => {
|
||||||
write!(f, "{} ", "[ERROR]".red())?;
|
// write!(f, "{} ", "[ERROR]".red())?;
|
||||||
write!(f, "file '{:?}' is not decompressible", file)
|
// write!(f, "file '{:?}' is not decompressible", file)
|
||||||
}
|
// }
|
||||||
Error::WalkdirError => {
|
Error::WalkdirError => {
|
||||||
// Already printed in the From block
|
// Already printed in the From block
|
||||||
write!(f, "")
|
write!(f, "")
|
||||||
@ -61,8 +61,17 @@ impl fmt::Display for Error {
|
|||||||
write!(f, "{} ", "[ERROR]".red())?;
|
write!(f, "{} ", "[ERROR]".red())?;
|
||||||
let spacing = " ";
|
let spacing = " ";
|
||||||
writeln!(f, "It seems you're trying to compress the root folder.")?;
|
writeln!(f, "It seems you're trying to compress the root folder.")?;
|
||||||
writeln!(f, "{}This is unadvisable since ouch does compressions in-memory.", spacing)?;
|
writeln!(
|
||||||
write!(f, "{}Use a more appropriate tool for this, such as {}.", spacing, "rsync".green())
|
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 => {
|
Error::InternalError => {
|
||||||
write!(f, "{} ", "[ERROR]".red())?;
|
write!(f, "{} ", "[ERROR]".red())?;
|
||||||
@ -79,7 +88,7 @@ impl fmt::Display for Error {
|
|||||||
impl From<std::io::Error> for Error {
|
impl From<std::io::Error> for Error {
|
||||||
fn from(err: std::io::Error) -> Self {
|
fn from(err: std::io::Error) -> Self {
|
||||||
match err.kind() {
|
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::PermissionDenied => Self::PermissionDenied,
|
||||||
std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
|
std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
|
||||||
_other => {
|
_other => {
|
||||||
@ -108,3 +117,9 @@ impl From<walkdir::Error> for Error {
|
|||||||
Self::WalkdirError
|
Self::WalkdirError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<oof::OofError> for Error {
|
||||||
|
fn from(_err: oof::OofError) -> Self {
|
||||||
|
todo!("We need to implement this properly");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
108
src/evaluator.rs
108
src/evaluator.rs
@ -1,9 +1,13 @@
|
|||||||
use std::{ffi::OsStr, fs, io::Write, path::PathBuf};
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::Write,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{Command, CommandKind, Flags},
|
cli::Command,
|
||||||
compressors::{
|
compressors::{
|
||||||
BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor,
|
BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor,
|
||||||
ZipCompressor,
|
ZipCompressor,
|
||||||
@ -20,10 +24,13 @@ use crate::{
|
|||||||
|
|
||||||
pub struct Evaluator {}
|
pub struct Evaluator {}
|
||||||
|
|
||||||
|
type BoxedCompressor = Box<dyn Compressor>;
|
||||||
|
type BoxedDecompressor = Box<dyn Decompressor>;
|
||||||
|
|
||||||
impl Evaluator {
|
impl Evaluator {
|
||||||
pub fn get_compressor(
|
pub fn get_compressor(
|
||||||
file: &File,
|
file: &File,
|
||||||
) -> crate::Result<(Option<Box<dyn Compressor>>, Box<dyn Compressor>)> {
|
) -> crate::Result<(Option<BoxedCompressor>, BoxedCompressor)> {
|
||||||
let extension = match &file.extension {
|
let extension = match &file.extension {
|
||||||
Some(extension) => extension.clone(),
|
Some(extension) => extension.clone(),
|
||||||
None => {
|
None => {
|
||||||
@ -65,7 +72,7 @@ impl Evaluator {
|
|||||||
|
|
||||||
pub fn get_decompressor(
|
pub fn get_decompressor(
|
||||||
file: &File,
|
file: &File,
|
||||||
) -> crate::Result<(Option<Box<dyn Decompressor>>, Box<dyn Decompressor>)> {
|
) -> crate::Result<(Option<BoxedDecompressor>, BoxedDecompressor)> {
|
||||||
let extension = match &file.extension {
|
let extension = match &file.extension {
|
||||||
Some(extension) => extension.clone(),
|
Some(extension) => extension.clone(),
|
||||||
None => {
|
None => {
|
||||||
@ -100,24 +107,25 @@ impl Evaluator {
|
|||||||
|
|
||||||
fn decompress_file_in_memory(
|
fn decompress_file_in_memory(
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
file_path: PathBuf,
|
file_path: &Path,
|
||||||
decompressor: Option<Box<dyn Decompressor>>,
|
decompressor: Option<Box<dyn Decompressor>>,
|
||||||
output_file: &Option<File>,
|
output_file: Option<File>,
|
||||||
extension: Option<Extension>,
|
extension: Option<Extension>,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
) -> crate::Result<()> {
|
) -> 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()
|
.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
|
// 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.
|
// 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
|
// If there is a decompressor to use, we'll create a file in-memory and decompress it
|
||||||
let decompressor = match decompressor {
|
let decompressor = match decompressor {
|
||||||
@ -125,49 +133,53 @@ impl Evaluator {
|
|||||||
None => {
|
None => {
|
||||||
// There is no more processing to be done on the input file (or there is but currently unsupported)
|
// 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.
|
// 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 =
|
let confirm =
|
||||||
Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE"));
|
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(());
|
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)?;
|
f.write_all(&bytes)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = File {
|
let file = File {
|
||||||
path: filename,
|
path: file_name,
|
||||||
contents_in_memory: Some(bytes),
|
contents_in_memory: Some(bytes),
|
||||||
extension,
|
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 {
|
if let DecompressionResult::FileInMemory(_) = decompression_result {
|
||||||
// Should not be reachable.
|
unreachable!("Shouldn't");
|
||||||
unreachable!();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compress_files(files: Vec<PathBuf>, mut output: File, flags: Flags) -> crate::Result<()> {
|
fn compress_files(
|
||||||
|
files: Vec<PathBuf>,
|
||||||
|
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 confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE"));
|
||||||
let (first_compressor, second_compressor) = Self::get_compressor(&output)?;
|
let (first_compressor, second_compressor) = Self::get_compressor(&output)?;
|
||||||
|
|
||||||
// TODO: use -y and -n here
|
// TODO: use -y and -n here
|
||||||
let output_path = output.path.clone();
|
if output_path.exists()
|
||||||
if output_path.exists() {
|
&& !utils::permission_for_overwriting(&output_path, flags, &confirm)?
|
||||||
if !utils::permission_for_overwriting(&output_path, flags, &confirm)? {
|
{
|
||||||
// The user does not want to overwrite the file
|
// The user does not want to overwrite the file
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let bytes = match first_compressor {
|
let bytes = match first_compressor {
|
||||||
Some(first_compressor) => {
|
Some(first_compressor) => {
|
||||||
@ -187,7 +199,7 @@ impl Evaluator {
|
|||||||
println!(
|
println!(
|
||||||
"{}: writing to {:?}. ({} bytes)",
|
"{}: writing to {:?}. ({} bytes)",
|
||||||
"info".yellow(),
|
"info".yellow(),
|
||||||
&output_path,
|
output_path,
|
||||||
bytes.len()
|
bytes.len()
|
||||||
);
|
);
|
||||||
fs::write(output_path, bytes)?;
|
fs::write(output_path, bytes)?;
|
||||||
@ -195,13 +207,21 @@ impl Evaluator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decompress_file(file: File, output: &Option<File>, 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 (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?;
|
||||||
|
|
||||||
let file_path = file.path.clone();
|
|
||||||
let extension = file.extension.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 {
|
match decompression_result {
|
||||||
DecompressionResult::FileInMemory(bytes) => {
|
DecompressionResult::FileInMemory(bytes) => {
|
||||||
@ -230,20 +250,24 @@ impl Evaluator {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate(command: Command, flags: Flags) -> crate::Result<()> {
|
pub fn evaluate(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
||||||
let output = command.output.clone();
|
match command {
|
||||||
|
Command::Compress {
|
||||||
match command.kind {
|
files,
|
||||||
CommandKind::Compression(files_to_compress) => {
|
compressed_output_path,
|
||||||
// Safe to unwrap since output is mandatory for compression
|
} => Self::compress_files(files, &compressed_output_path, flags)?,
|
||||||
let output = output.unwrap();
|
Command::Decompress {
|
||||||
Self::compress_files(files_to_compress, output, flags)?;
|
files,
|
||||||
}
|
output_folder,
|
||||||
CommandKind::Decompression(files_to_decompress) => {
|
} => {
|
||||||
for file in files_to_decompress {
|
// From Option<PathBuf> to Option<&Path>
|
||||||
Self::decompress_file(file, &output, flags)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ use std::{
|
|||||||
|
|
||||||
use CompressionFormat::*;
|
use CompressionFormat::*;
|
||||||
|
|
||||||
|
use crate::utils::to_utf;
|
||||||
|
|
||||||
/// Represents the extension of a file, but only really caring about
|
/// Represents the extension of a file, but only really caring about
|
||||||
/// compression formats (and .tar).
|
/// compression formats (and .tar).
|
||||||
/// Ex.: Extension::new("file.tar.gz") == Extension { first_ext: Some(Tar), second_ext: Gzip }
|
/// 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 second_ext: CompressionFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> {
|
pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr)> {
|
||||||
let path = Path::new(filename);
|
let path = Path::new(file_name);
|
||||||
|
|
||||||
let ext = path.extension().and_then(OsStr::to_str)?;
|
let ext = path.extension()?;
|
||||||
|
|
||||||
let previous_extension = path
|
let previous_extension = path.file_stem().and_then(get_extension_from_filename);
|
||||||
.file_stem()
|
|
||||||
.and_then(OsStr::to_str)
|
|
||||||
.and_then(get_extension_from_filename);
|
|
||||||
|
|
||||||
if let Some((_, prev)) = previous_extension {
|
if let Some((_, prev)) = previous_extension {
|
||||||
Some((prev, ext))
|
Some((prev, ext))
|
||||||
} else {
|
} else {
|
||||||
Some(("", ext))
|
Some((OsStr::new(""), ext))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,32 +42,32 @@ impl From<CompressionFormat> for Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Extension {
|
impl Extension {
|
||||||
pub fn new(filename: &str) -> crate::Result<Self> {
|
pub fn from(file_name: &OsStr) -> crate::Result<Self> {
|
||||||
let ext_from_str = |ext| match ext {
|
let compression_format_from = |ext: &OsStr| match ext {
|
||||||
"zip" => Ok(Zip),
|
_ if ext == "zip" => Ok(Zip),
|
||||||
"tar" => Ok(Tar),
|
_ if ext == "tar" => Ok(Tar),
|
||||||
"gz" => Ok(Gzip),
|
_ if ext == "gz" => Ok(Gzip),
|
||||||
"bz" | "bz2" => Ok(Bzip),
|
_ if ext == "bz" || ext == "bz2" => Ok(Bzip),
|
||||||
"xz" | "lz" | "lzma" => Ok(Lzma),
|
_ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma),
|
||||||
other => Err(crate::Error::UnknownExtensionError(other.into())),
|
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 {
|
Some(extension_tuple) => match extension_tuple {
|
||||||
("", snd) => (None, snd),
|
(os_str, snd) if os_str.is_empty() => (None, snd),
|
||||||
(fst, snd) => (Some(fst), 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) {
|
let (first_ext, second_ext) = match (first_ext, second_ext) {
|
||||||
(None, snd) => {
|
(None, snd) => {
|
||||||
let ext = ext_from_str(snd)?;
|
let ext = compression_format_from(snd)?;
|
||||||
(None, ext)
|
(None, ext)
|
||||||
}
|
}
|
||||||
(Some(fst), snd) => {
|
(Some(fst), snd) => {
|
||||||
let snd = ext_from_str(snd)?;
|
let snd = compression_format_from(snd)?;
|
||||||
let fst = ext_from_str(fst).ok();
|
let fst = compression_format_from(fst).ok();
|
||||||
(fst, snd)
|
(fst, snd)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -130,9 +129,9 @@ impl TryFrom<&PathBuf> for CompressionFormat {
|
|||||||
impl TryFrom<&str> for CompressionFormat {
|
impl TryFrom<&str> for CompressionFormat {
|
||||||
type Error = crate::Error;
|
type Error = crate::Error;
|
||||||
|
|
||||||
fn try_from(filename: &str) -> Result<Self, Self::Error> {
|
fn try_from(file_name: &str) -> Result<Self, Self::Error> {
|
||||||
let filename = Path::new(filename);
|
let file_name = Path::new(file_name);
|
||||||
let ext = match filename.extension() {
|
let ext = match file_name.extension() {
|
||||||
Some(ext) => ext,
|
Some(ext) => ext,
|
||||||
None => return Err(crate::Error::MissingExtensionError(String::new())),
|
None => return Err(crate::Error::MissingExtensionError(String::new())),
|
||||||
};
|
};
|
||||||
|
19
src/file.rs
19
src/file.rs
@ -1,11 +1,11 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::extension::Extension;
|
use crate::extension::Extension;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct File {
|
pub struct File<'a> {
|
||||||
/// File's (relative) path
|
/// File's (relative) path
|
||||||
pub path: PathBuf,
|
pub path: &'a Path,
|
||||||
/// The bytes that compose the file.
|
/// The bytes that compose the file.
|
||||||
/// Only used when the whole file is kept in-memory
|
/// Only used when the whole file is kept in-memory
|
||||||
pub contents_in_memory: Option<Vec<u8>>,
|
pub contents_in_memory: Option<Vec<u8>>,
|
||||||
@ -17,12 +17,15 @@ pub struct File {
|
|||||||
pub extension: Option<Extension>,
|
pub extension: Option<Extension>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(PathBuf, Extension)> for File {
|
impl<'a> File<'a> {
|
||||||
fn from((path, format): (PathBuf, Extension)) -> Self {
|
pub fn from(path: &'a Path) -> crate::Result<Self> {
|
||||||
Self {
|
let extension = Extension::from(path.as_ref())?;
|
||||||
|
eprintln!("dev warning: Should we really ignore the errors from the convertion above?");
|
||||||
|
|
||||||
|
Ok(File {
|
||||||
path,
|
path,
|
||||||
contents_in_memory: None,
|
contents_in_memory: None,
|
||||||
extension: Some(format),
|
extension: Some(extension),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ mod utils;
|
|||||||
use error::{Error, Result};
|
use error::{Error, Result};
|
||||||
use evaluator::Evaluator;
|
use evaluator::Evaluator;
|
||||||
|
|
||||||
|
use crate::cli::ParsedArgs;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
if let Err(err) = run() {
|
if let Err(err) = run() {
|
||||||
println!("{}", err);
|
println!("{}", err);
|
||||||
@ -20,7 +22,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> crate::Result<()> {
|
fn run() -> crate::Result<()> {
|
||||||
let matches = cli::get_matches();
|
let ParsedArgs { command, flags } = cli::parse_args()?;
|
||||||
let (command, flags) = cli::parse_matches(matches)?;
|
dbg!(&command);
|
||||||
Evaluator::evaluate(command, flags)
|
Evaluator::evaluate(command, &flags)
|
||||||
}
|
}
|
||||||
|
370
src/test.rs
370
src/test.rs
@ -1,211 +1,219 @@
|
|||||||
// TODO: remove tests of CompressionFormat::try_from since that's no longer used anywhere
|
// TODO: remove tests of CompressionFormat::try_from since that's no longer used anywhere
|
||||||
|
|
||||||
#[cfg(test)]
|
// use std::{
|
||||||
mod cli {
|
// convert::TryFrom,
|
||||||
use std::{convert::TryFrom, fs, path::Path};
|
// ffi::{OsStr, OsString},
|
||||||
|
// fs,
|
||||||
|
// path::Path,
|
||||||
|
// };
|
||||||
|
|
||||||
use crate::{
|
// use crate::{
|
||||||
cli::{clap_app, Command, CommandKind::*},
|
// cli::Command,
|
||||||
extension::{CompressionFormat::*, Extension},
|
// extension::{CompressionFormat, CompressionFormat::*, Extension},
|
||||||
file::File,
|
// file::File,
|
||||||
};
|
// };
|
||||||
|
|
||||||
// ouch's command-line logic uses fs::canonicalize on its inputs so we cannot
|
// // Helper
|
||||||
// use made-up files for testing.
|
// fn gen_args(text: &str) -> Vec<OsString> {
|
||||||
// make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors
|
// let args = text.split_whitespace();
|
||||||
fn make_dummy_file<'a, P>(path: P) -> crate::Result<()>
|
// args.map(OsString::from).collect()
|
||||||
where
|
// }
|
||||||
P: AsRef<Path> + 'a,
|
|
||||||
{
|
|
||||||
fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
// #[cfg(test)]
|
||||||
fn decompress_files_into_folder() -> crate::Result<()> {
|
// mod cli {
|
||||||
make_dummy_file("file.zip")?;
|
// use super::*;
|
||||||
let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "-o", "folder/"]);
|
|
||||||
let command_from_matches = Command::try_from(matches)?;
|
|
||||||
|
|
||||||
assert_eq!(
|
// // ouch's command-line logic uses fs::canonicalize on its inputs so we cannot
|
||||||
command_from_matches,
|
// // use made-up files for testing.
|
||||||
Command {
|
// // make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors
|
||||||
kind: Decompression(vec![File {
|
// fn make_dummy_file<'a, P>(path: P) -> crate::Result<()>
|
||||||
path: fs::canonicalize("file.zip")?,
|
// where
|
||||||
contents_in_memory: None,
|
// P: AsRef<Path> + 'a,
|
||||||
extension: Some(Extension::from(Zip))
|
// {
|
||||||
}]),
|
// fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?;
|
||||||
output: Some(File {
|
// Ok(())
|
||||||
path: "folder".into(),
|
// }
|
||||||
contents_in_memory: None,
|
|
||||||
extension: None
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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]
|
// fs::remove_file("file.zip")?;
|
||||||
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)?;
|
|
||||||
|
|
||||||
assert_eq!(
|
// Ok(())
|
||||||
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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_file("my-cool-file.zip")?;
|
// #[test]
|
||||||
fs::remove_file("file.tar")?;
|
// 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]
|
// fs::remove_file("my-cool-file.zip")?;
|
||||||
fn compress_files() -> crate::Result<()> {
|
// fs::remove_file("file.tar")?;
|
||||||
make_dummy_file("file")?;
|
|
||||||
make_dummy_file("file2.jpeg")?;
|
|
||||||
make_dummy_file("file3.ok")?;
|
|
||||||
|
|
||||||
let matches = clap_app().get_matches_from(vec![
|
// Ok(())
|
||||||
"ouch",
|
// }
|
||||||
"-i",
|
|
||||||
"file",
|
|
||||||
"file2.jpeg",
|
|
||||||
"file3.ok",
|
|
||||||
"-o",
|
|
||||||
"file.tar",
|
|
||||||
]);
|
|
||||||
let command_from_matches = Command::try_from(matches)?;
|
|
||||||
|
|
||||||
assert_eq!(
|
// #[test]
|
||||||
command_from_matches,
|
// fn compress_files() -> crate::Result<()> {
|
||||||
Command {
|
// make_dummy_file("file")?;
|
||||||
kind: Compression(vec![
|
// make_dummy_file("file2.jpeg")?;
|
||||||
fs::canonicalize("file")?,
|
// make_dummy_file("file3.ok")?;
|
||||||
fs::canonicalize("file2.jpeg")?,
|
|
||||||
fs::canonicalize("file3.ok")?
|
|
||||||
]),
|
|
||||||
output: Some(File {
|
|
||||||
path: "file.tar".into(),
|
|
||||||
contents_in_memory: None,
|
|
||||||
extension: Some(Extension::from(Tar))
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
fs::remove_file("file")?;
|
// let matches = clap_app().get_matches_from(vec![
|
||||||
fs::remove_file("file2.jpeg")?;
|
// "ouch",
|
||||||
fs::remove_file("file3.ok")?;
|
// "-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)]
|
// fs::remove_file("file")?;
|
||||||
mod cli_errors {
|
// 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]
|
// #[test]
|
||||||
fn compress_files() -> crate::Result<()> {
|
// fn compress_files() -> crate::Result<()> {
|
||||||
let matches =
|
// let matches =
|
||||||
clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]);
|
// clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]);
|
||||||
let res = Command::try_from(matches);
|
// let res = Command::try_from(matches);
|
||||||
|
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
res,
|
// res,
|
||||||
Err(crate::Error::InputsMustHaveBeenDecompressible(
|
// Err(crate::Error::InputsMustHaveBeenDecompressible(
|
||||||
"a_file".into()
|
// "a_file".into()
|
||||||
))
|
// ))
|
||||||
);
|
// );
|
||||||
|
|
||||||
Ok(())
|
// Ok(())
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[cfg(test)]
|
// #[cfg(test)]
|
||||||
mod extension_extraction {
|
// mod extension_extraction {
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
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]
|
// #[test]
|
||||||
fn test_extension_zip() {
|
// fn test_extension_tar_gz() {
|
||||||
let path = "filename.tar.zip";
|
// let extension = Extension::from(OsStr::new("folder.tar.gz")).unwrap();
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
CompressionFormat::try_from(path),
|
// extension,
|
||||||
Ok(CompressionFormat::Zip)
|
// Extension {
|
||||||
);
|
// first_ext: Some(CompressionFormat::Tar),
|
||||||
}
|
// second_ext: CompressionFormat::Gzip
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_extension_tar_gz() {
|
// fn test_extension_tar() {
|
||||||
let extension = Extension::new("folder.tar.gz").unwrap();
|
// let path = "pictures.tar";
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
extension,
|
// CompressionFormat::try_from(path),
|
||||||
Extension {
|
// Ok(CompressionFormat::Tar)
|
||||||
first_ext: Some(CompressionFormat::Tar),
|
// );
|
||||||
second_ext: CompressionFormat::Gzip
|
// }
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_extension_tar() {
|
// fn test_extension_gz() {
|
||||||
let path = "pictures.tar";
|
// let path = "passwords.tar.gz";
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
CompressionFormat::try_from(path),
|
// CompressionFormat::try_from(path),
|
||||||
Ok(CompressionFormat::Tar)
|
// Ok(CompressionFormat::Gzip)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_extension_gz() {
|
// fn test_extension_lzma() {
|
||||||
let path = "passwords.tar.gz";
|
// let path = "mygame.tar.lzma";
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
CompressionFormat::try_from(path),
|
// CompressionFormat::try_from(path),
|
||||||
Ok(CompressionFormat::Gzip)
|
// Ok(CompressionFormat::Lzma)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[test]
|
// #[test]
|
||||||
fn test_extension_lzma() {
|
// fn test_extension_bz() {
|
||||||
let path = "mygame.tar.lzma";
|
// let path = "songs.tar.bz";
|
||||||
assert_eq!(
|
// assert_eq!(
|
||||||
CompressionFormat::try_from(path),
|
// CompressionFormat::try_from(path),
|
||||||
Ok(CompressionFormat::Lzma)
|
// Ok(CompressionFormat::Bzip)
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
#[test]
|
|
||||||
fn test_extension_bz() {
|
|
||||||
let path = "songs.tar.bz";
|
|
||||||
assert_eq!(
|
|
||||||
CompressionFormat::try_from(path),
|
|
||||||
Ok(CompressionFormat::Bzip)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
44
src/utils.rs
44
src/utils.rs
@ -1,11 +1,13 @@
|
|||||||
use std::{
|
use std::{
|
||||||
env, fs,
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use colored::Colorize;
|
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<()>
|
pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()>
|
||||||
where
|
where
|
||||||
@ -47,19 +49,18 @@ pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_destination_path(dest: &Option<File>) -> &Path {
|
pub(crate) fn get_destination_path<'a>(dest: &'a Option<File>) -> &'a Path {
|
||||||
match dest {
|
match dest {
|
||||||
Some(output) => {
|
Some(output_file) => {
|
||||||
// Must be None according to the way command-line arg. parsing in Ouch works
|
// Must be None according to the way command-line arg. parsing in Ouch works
|
||||||
assert_eq!(output.extension, None);
|
assert_eq!(output_file.extension, None);
|
||||||
|
Path::new(&output_file.path)
|
||||||
Path::new(&output.path)
|
|
||||||
}
|
}
|
||||||
None => Path::new("."),
|
None => Path::new("."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result<PathBuf> {
|
pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> {
|
||||||
let previous_location = env::current_dir()?;
|
let previous_location = env::current_dir()?;
|
||||||
|
|
||||||
let parent = if let Some(parent) = filename.parent() {
|
let parent = if let Some(parent) = filename.parent() {
|
||||||
@ -68,21 +69,30 @@ pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result<
|
|||||||
return Err(crate::Error::CompressingRootFolder);
|
return Err(crate::Error::CompressingRootFolder);
|
||||||
};
|
};
|
||||||
|
|
||||||
env::set_current_dir(parent)?;
|
env::set_current_dir(parent).unwrap();
|
||||||
|
|
||||||
Ok(previous_location)
|
Ok(previous_location)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn permission_for_overwriting(
|
pub fn permission_for_overwriting(
|
||||||
path: &PathBuf,
|
path: &Path,
|
||||||
flags: Flags,
|
flags: &oof::Flags,
|
||||||
confirm: &Confirmation,
|
confirm: &Confirmation,
|
||||||
) -> crate::Result<bool> {
|
) -> crate::Result<bool> {
|
||||||
match flags {
|
match (flags.is_present("yes"), flags.is_present("false")) {
|
||||||
Flags::AlwaysYes => return Ok(true),
|
(true, true) => {
|
||||||
Flags::AlwaysNo => return Ok(false),
|
unreachable!("This shoul've been cutted out in the ~/src/cli.rs filter flags function.")
|
||||||
Flags::None => {}
|
}
|
||||||
|
(true, _) => return Ok(true),
|
||||||
|
(_, true) => return Ok(false),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path_str = &*path.as_path().to_string_lossy();
|
let file_path_str = to_utf(path);
|
||||||
Ok(confirm.ask(Some(file_path_str))?)
|
confirm.ask(Some(&file_path_str))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_utf(os_str: impl AsRef<OsStr>) -> String {
|
||||||
|
let text = format!("{:?}", os_str.as_ref());
|
||||||
|
text.trim_matches('"').to_string()
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user