use std::collections::BTreeSet;
use std::path::PathBuf;
use cfg_if::cfg_if;
use clap::{ArgAction, Parser, ValueEnum};
use crate::utils::{CLAP_LONG_VERSION, CLAP_VERSION};
use crate::String as StringType;
cfg_if! {
if #[cfg(windows)] {
const DEFAULT_FORMAT: &str = "powershell";
} else {
const DEFAULT_FORMAT: &str = "sh";
}
}
#[derive(ValueEnum, Eq, PartialEq, Debug, Copy, Clone)]
pub enum OutputFormat {
#[clap(alias = "shell", alias = "bash")]
Sh,
#[clap(alias = "powershell")]
PowerShell,
Text,
#[cfg(feature = "json")]
Json,
}
#[derive(ValueEnum, Eq, PartialEq, Debug, Copy, Clone)]
pub enum Prompt {
Never,
Error,
Always,
}
#[derive(Parser, Debug)]
#[allow(clippy::struct_excessive_bools)]
#[clap(
version = CLAP_VERSION.as_str(),
long_version = CLAP_LONG_VERSION.as_str(),
author = option_env!("CARGO_PKG_AUTHORS").unwrap_or("Lynnesbian"),
about = option_env!("CARGO_PKG_DESCRIPTION").unwrap_or("File Info Fixer"),
before_help = "Copyright © 2021-2024 Lynnesbian under the GPL3 (or later) License.",
after_long_help = "Copyright © 2021-2024 Lynnesbian\n\
This program is free software: you can redistribute it and/or modify \
it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 \
of the License, or (at your option) any later version.",
max_term_width = 120
)]
pub struct Parameters {
#[clap(long, help_heading = "RENAMING")]
pub fix: bool,
#[clap(short = 'p', long, value_enum, requires = "fix", help_heading = "RENAMING")]
pub prompt: Option<Prompt>,
#[clap(long, requires = "fix", help_heading = "RENAMING")]
pub overwrite: bool,
#[clap(short, long, use_value_delimiter = true, value_delimiter = ',', value_name = "ext", num_args(1),
value_parser = validate_exts, help_heading = "FILTERING")]
pub exts: Option<Vec<StringType>>,
#[clap(
short = 'E',
long,
value_enum,
use_value_delimiter = true,
value_delimiter = ',',
value_name = "set",
help_heading = "FILTERING"
)]
pub ext_set: Vec<ExtensionSet>,
#[clap(short = 'x', long, use_value_delimiter = true, value_delimiter = ',', value_name = "ext", value_parser =
validate_exts, help_heading = "FILTERING")]
pub exclude: Option<Vec<StringType>>,
#[clap(
short = 'X',
long,
value_enum,
use_value_delimiter = true,
value_delimiter = ',',
value_name = "set",
help_heading = "FILTERING"
)]
pub exclude_set: Vec<ExtensionSet>,
#[clap(short, long, help_heading = "FILTERING")]
pub scan_hidden: bool,
#[clap(short = 'S', long, help_heading = "FILTERING")]
pub scan_extensionless: bool,
#[clap(short, long, help_heading = "FILTERING")]
pub follow_symlinks: bool,
#[clap(short = 'I', long, help_heading = "FILTERING")]
pub ignore_unknown_exts: bool,
#[clap(short, long, default_value = DEFAULT_FORMAT, value_enum, value_name = "format", help_heading = "OUTPUT")]
pub output_format: OutputFormat,
#[clap(short, long, action = ArgAction::Count, group = "verbosity", help_heading = "OUTPUT")]
pub verbose: u8,
#[clap(short, long, action = ArgAction::Count, group = "verbosity", help_heading = "OUTPUT")]
pub quiet: u8,
#[clap(long, help_heading = "OUTPUT")]
pub canonical_paths: bool,
#[clap(name = "DIR", default_value = ".", value_parser)]
pub dir: PathBuf,
#[cfg(feature = "multi-threaded")]
#[clap(short = 'j', long, default_value = "0", help_heading = "MISC")]
pub jobs: usize,
}
fn validate_exts(exts: &str) -> Result<StringType, String> {
if exts.is_empty() {
return Err(String::from("Cannot specify empty extensions"));
}
if exts.to_lowercase() != exts {
return Err(String::from("Supplied extensions must be lowercase"));
}
Ok(exts.into())
}
#[derive(Eq, PartialEq, Debug, Copy, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ScanOpts {
pub hidden: bool,
pub extensionless: bool,
pub follow_symlinks: bool,
pub ignore_unknown_exts: bool,
}
impl Parameters {
pub fn extensions(&self) -> Option<BTreeSet<&str>> {
if let Some(included) = self.included_extensions() {
if let Some(excluded) = self.excluded_extensions() {
Some(included.into_iter().filter(|ext| !excluded.contains(ext)).collect())
} else {
Some(included)
}
} else {
None
}
}
pub fn included_extensions(&self) -> Option<BTreeSet<&str>> {
let mut included = BTreeSet::new();
if let Some(exts) = self.exts.as_ref() {
included.extend(exts.iter().map(crate::String::as_str));
}
if !&self.ext_set.is_empty() {
included.extend(self.ext_set.iter().flat_map(|set| set.extensions()));
}
match included {
x if x.is_empty() => None,
x => Some(x),
}
}
pub fn excluded_extensions(&self) -> Option<BTreeSet<&str>> {
let mut excluded = BTreeSet::new();
if let Some(exclude) = self.exclude.as_ref() {
excluded.extend(exclude.iter().map(crate::String::as_str));
}
if !&self.exclude_set.is_empty() {
excluded.extend(self.exclude_set.iter().flat_map(|set| set.extensions()));
}
match excluded {
x if x.is_empty() => None,
x => Some(x),
}
}
pub const fn get_scan_opts(&self) -> ScanOpts {
ScanOpts {
hidden: self.scan_hidden,
extensionless: self.scan_extensionless,
follow_symlinks: self.follow_symlinks,
ignore_unknown_exts: self.ignore_unknown_exts,
}
}
pub const fn get_verbosity(&self) -> log::LevelFilter {
use log::LevelFilter;
match self.quiet {
0 => {
match self.verbose {
0 => LevelFilter::Info, 1 => LevelFilter::Debug, _ => LevelFilter::Trace, }
}
1 => LevelFilter::Warn, 2 => LevelFilter::Error, _ => LevelFilter::Off, }
}
}
#[derive(ValueEnum, Eq, PartialEq, Debug, Copy, Clone)]
pub enum ExtensionSet {
Images,
Audio,
#[clap(alias = "videos")]
Video,
Media,
Documents,
Text,
Archives,
System,
}
impl ExtensionSet {
pub fn extensions(&self) -> Vec<&str> {
match self {
Self::Images => mime_guess::get_mime_extensions_str("image/*").unwrap().to_vec(),
Self::Audio => mime_guess::get_mime_extensions_str("audio/*").unwrap().to_vec(),
Self::Video => mime_guess::get_mime_extensions_str("video/*").unwrap().to_vec(),
Self::Media => [Self::Images.extensions(), Self::Audio.extensions(), Self::Video.extensions()].concat(),
Self::Documents => vec![
"pdf", "doc", "docx", "ppt", "pptx", "xls", "xlsx", "csv", "tsv", "odt", "ods", "odp", "oda", "rtf", "ps",
"pages", "key", "numbers",
],
Self::Text => [
mime_guess::get_mime_extensions_str("text/*").unwrap(),
&["js", "pl", "csh", "sh", "bash", "zsh", "fish", "bat", "php"],
]
.concat(),
Self::Archives => vec![
"zip", "tar", "gz", "zst", "xz", "rar", "7z", "bz", "bz2", "tgz", "rpa", "txz", "tz2", "sea", "sitx", "z",
"cpio",
],
Self::System => vec![
"com", "dll", "exe", "sys", "reg", "nt", "cpl", "msi", "efi", "bio", "rcv", "mbr", "sbf", "grub", "ko",
"dylib", "pdb", "hdmp", "crash", "cab",
],
}
}
}