You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
9.5 KiB

use super::*;
use std::{
fmt,
path::{
Path,PathBuf,
},
};
use lazy_static::lazy_static;
/// Get the program name
pub fn program() -> &'static str
{
lazy_static!{
static ref NAME: &'static str = Box::leak(std::env::args().next().unwrap().into_boxed_str());
}
&NAME
}
pub fn usage() -> !
{
println!("rmdupe version {}", env!("CARGO_PKG_VERSION"));
println!(" by {}", env!("CARGO_PKG_AUTHORS"));
println!(" (licensed with GNU GPL v3)\n");
println!("Usage: {} [OPTIONS] [--] <dirs...>", program());
println!("Usage: {} --rebase [<files>]", program());
println!("Usage: {} --help", program());
println!("OPTIONS:");
println!(" --load -l:\t\tLoad the hashes from `load-file` if possible.");
println!(" --save -s:\t\tSave the hashes to `save-file` if possible.");
println!(" --load-file <file>\tSpecify file for `--load`");
println!(" --save-file <file>\tSpecify file for `--save`");
println!(" --load-save <file>\tSpecify file for `--save` and `--load`");
println!(" --delete -d\t\tDelete dupes instead of printing");
println!(" --verbose -v\t\tPrint verbose info");
println!(" --debug -V\t\tPrint very verbose info");
println!(" --error-mode <mode>\tSpecify error handling mode, one of: [IGNORE|WARN|CANCEL|TERMINATE]. Default is `WARN`");
println!(" --quiet -q\t\tAlias for `--error-mode IGNORE`");
println!(" --warn -i\t\tAlias for `--error-mode WARN`");
println!(" --cancel -w\t\tAlias for `--error-mode CANCEL`");
println!(" --error -W\t\tAlias for `--error-mode TERMINATE`");
println!(" --recurse <max>|inf\tRecursive mode, give max depth or infinite.");
#[cfg(feature="threads")]
println!(" --threads <max>|inf\tMax number of threads to run at once.");
#[cfg(feature="threads")]
println!(" -U\t\tUlimited max threads.");
#[cfg(feature="threads")]
println!(" --sync -S\t\tRun only one file at a time.");
println!(" --\t\t\tStop reading args");
println!("Other:");
println!(" --help -h:\t\tPrint this message");
println!(" --rebase:\t\tRebuild hash stores created by `--save`. If no files are provided, use default.");
#[cfg(feature="threads")]
println!("Compiled with threading support");
std::process::exit(1)
}
enum Ensure
{
None,
Dir,
File,
}
impl Default for Ensure
{
fn default() -> Self {
Self::None
}
}
fn validate_path<P>(path: P, ensure: Ensure, must_exist: bool) -> Result<P, Error>
where P: AsRef<Path>
{
if !must_exist || path.as_ref().exists() {
if !must_exist && !path.as_ref().exists() {return Ok(path);}
match ensure {
Ensure::File => {
if !path.as_ref().is_file() {
Err(Error::ExpectedFile(path.as_ref().to_owned()))
} else {
Ok(path)
}
},
Ensure::Dir => {
if !path.as_ref().is_dir() {
Err(Error::ExpectedDirectory(path.as_ref().to_owned()))
} else {
Ok(path)
}
},
_ => Ok(path),
}
} else {
Err(Error::FileNotFound(path.as_ref().to_owned()))
}
}
/// Try to parse `std::env::args()`
#[inline]
pub fn parse_args() -> Result<Output, Error>
{
parse(std::env::args().skip(1))
}
#[derive(Debug)]
pub enum Output
{
Normal(config::Config),
Help,
Rebase(config::Rebase),
}
/// Arg parse for rebase
fn parse_rebase<I>(args: I) -> Result<config::Rebase, Error>
where I: IntoIterator<Item=String>
{
let mut files = Vec::new();
for file in args.into_iter().map(|path| validate_path(path, Ensure::File, true))
{
files.push(file?);
}
if files.len() < 1 {
files.push(validate_path(config::DEFAULT_HASHNAME.to_string(), Ensure::File, false)?.to_owned());
}
Ok(config::Rebase{
save: files.clone(), //TODO: Seperate save+loads
load: files,
#[cfg(feature="threads")]
max_threads: config::MAX_THREADS,
})
}
/// Try to parse args
pub fn parse<I>(args: I) -> Result<Output, Error>
where I: IntoIterator<Item=String>
{
let mut args = args.into_iter();
let mut reading = true;
let mut paths = Vec::new();
let mut load = Vec::new();
let mut save = Vec::new();
let mut delete = false;
let mut mode_er = error::Mode::Cancel;
let mut mode_rec = config::RecursionMode::None;
let mut mode_log = log::Mode::Warn;
#[cfg(feature="threads")]
let mut threads = config::MAX_THREADS;
macro_rules! push {
($arg:expr) => {
{
let arg = $arg;
paths.push(validate_path(&arg, Ensure::None, true)?.to_owned())
}
};
}
let mut one = String::new();
macro_rules! take_one {
() => {
{
match args.next() {
Some(a) => {one = a; true},
_ => false,
}
}
}
}
while let Some(arg) = args.next()
{
if reading && arg.chars().next().unwrap_or('\0') == '-' {
match &arg[..] {
"--help" => return Ok(Output::Help),
"--rebase" => return Ok(Output::Rebase(parse_rebase(args)?)),
"--" => reading = false,
#[cfg(feature="threads")]
"--threads" if take_one!() => {
if one.to_lowercase().trim() == "inf" {
threads = None;
} else {
threads = Some(one.as_str().parse::<std::num::NonZeroUsize>().or_else(|e| Err(Error::BadNumber(e)))?);
}
},
#[cfg(feature="threads")]
"--sync" => threads = Some(unsafe{std::num::NonZeroUsize::new_unchecked(1)}),
"--load" => {
load.push(validate_path(config::DEFAULT_HASHNAME.to_string(), Ensure::File, false)?.to_owned());
},
"--save" => {
save.push(validate_path(config::DEFAULT_HASHNAME.to_string(), Ensure::File, false)?.to_owned());
},
"--save-file" if take_one!() => {
save.push(validate_path(&one, Ensure::File, false)?.to_owned());
},
"--load-file" if take_one!() => {
load.push(validate_path(&one, Ensure::Dir, false)?.to_owned());
},
"--load-save" if take_one!() => {
load.push(validate_path(&one, Ensure::Dir, false)?.to_owned());
save.push(validate_path(&one, Ensure::Dir, false)?.to_owned());
},
"--verbose" => mode_log = log::Mode::Verbose,
"--debug" => mode_log = log::Mode::Debug,
"--delete" => delete = true,
"--error" => mode_er = error::Mode::Terminate,
"--cancel" => mode_er = error::Mode::Cancel,
"--warn" => mode_er = error::Mode::Warn,
"--quiet" => mode_er = error::Mode::Ignore,
"--error-mode" if take_one!() => mode_er = match one.to_lowercase().trim() {
"terminate" => error::Mode::Terminate,
"cancel" => error::Mode::Cancel,
"warn" => error::Mode::Warn,
"ignore" => error::Mode::Ignore,
_ => return Err(Error::UnknownErrorMode(one)),
},
"--recurse" if take_one!() => {
if one.to_lowercase().trim() == "inf" {
mode_rec = config::RecursionMode::All;
} else if let Ok(sz) = one.parse::<usize>() {
mode_rec = if sz == 0 {config::RecursionMode::None} else {config::RecursionMode::N(sz)};
} else {
return Err(Error::Parse(one, "number"));
}
},
_ if arg.len() > 1 => {
// Parse small ones
for argchar in arg.chars().skip(1) {
match argchar {
'l' => load.push(validate_path(config::DEFAULT_HASHNAME.to_owned(), Ensure::File, false)?.to_owned()),
's' => save.push(validate_path(config::DEFAULT_HASHNAME.to_owned(), Ensure::File, false)?.to_owned()),
'd' => delete = true,
'v' => mode_log = log::Mode::Verbose,
'V' => mode_log = log::Mode::Debug,
'W' => mode_er = error::Mode::Terminate,
'w' => mode_er = error::Mode::Cancel,
'i' => mode_er = error::Mode::Warn,
'q' => mode_er = error::Mode::Ignore,
'h' => return Ok(Output::Help),
#[cfg(feature="threads")]
'U' => threads = None,
#[cfg(feature="threads")]
'S' => threads = Some(unsafe{std::num::NonZeroUsize::new_unchecked(1)}),
'r' => mode_rec = config::RecursionMode::All,
_ => return Err(Error::UnknownArgChar(argchar)),
}
}
},
_ => push!(arg),
};
} else {
push!(arg);
}
}
// Remove dupes & keeps ordering
load.dedup_full();
save.dedup_full();
paths.dedup_full();
if paths.len() < 1 {
return Err(Error::NoInput);
}
Ok(Output::Normal(config::Config{
paths: paths,
mode: config::Mode{
error_mode: mode_er,
recursion_mode: mode_rec,
operation_mode: if delete { config::OperationMode::Delete } else { config::OperationMode::Print },
logging_mode: mode_log,
},
save,
load,
#[cfg(feature="threads")]
max_threads: threads,
}))
}
#[derive(Debug)]
pub enum Error
{
NoInput,
Parse(String, &'static str),
UnknownArg(String),
UnknownArgChar(char),
FileNotFound(PathBuf),
ExpectedFile(PathBuf),
ExpectedDirectory(PathBuf),
UnknownErrorMode(String),
BadNumber(std::num::ParseIntError),
Unknown,
}
impl std::error::Error for Error{}
impl fmt::Display for Error
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result
{
write!(f, "failed to parse args: ")?;
match self {
Error::BadNumber(num) => write!(f, "{}", num),
Error::NoInput => write!(f, "need at least one input"),
Error::Parse(value, typ) => write!(f, "expected a {}, got `{}'", typ, value),
Error::UnknownArg(arg) => write!(f, "i don't know how to `{}'", arg),
Error::UnknownArgChar(arg) => write!(f, "i don't know how to `-{}'", arg),
Error::UnknownErrorMode(error) => write!(f, "unknown error mode `{}'", error),
Error::ExpectedDirectory(path) => write!(f, "expected directory, got file: {:?}", path),
Error::ExpectedFile(path) => write!(f, "expected file, got directory: {:?}", path),
Error::FileNotFound(path) => write!(f, "file not found: {:?}", path),
_ => write!(f, "unknown error"),
}
}
}