285 lines
9.9 KiB
Rust
285 lines
9.9 KiB
Rust
//! [Clap] struct used to parse command line arguments.
|
|
|
|
use crate::string_type::String as StringType;
|
|
use cfg_if::cfg_if;
|
|
use clap::{AppSettings, Clap};
|
|
use std::collections::BTreeSet;
|
|
use std::path::PathBuf;
|
|
|
|
cfg_if! {
|
|
if #[cfg(windows)] {
|
|
const DEFAULT_FORMAT: &str = "powershell";
|
|
} else {
|
|
const DEFAULT_FORMAT: &str = "sh";
|
|
}
|
|
}
|
|
|
|
#[derive(Clap, PartialEq, Debug)]
|
|
pub enum OutputFormat {
|
|
/// A Bourne shell compatible script.
|
|
#[clap(alias = "shell", alias = "bash")]
|
|
Sh,
|
|
/// A PowerShell script.
|
|
#[clap(alias = "powershell")]
|
|
PowerShell,
|
|
/// Plain text.
|
|
Text,
|
|
/// JSON.
|
|
#[cfg(feature = "json")]
|
|
Json,
|
|
}
|
|
|
|
// TODO: convert this to macro style?: https://docs.rs/clap/3.0.0-beta.2/clap/index.html#using-macros
|
|
|
|
#[derive(Clap, Debug)]
|
|
#[clap(
|
|
version = option_env!("CARGO_PKG_VERSION").unwrap_or("???"),
|
|
author = option_env!("CARGO_PKG_AUTHORS").unwrap_or("Lynnesbian"),
|
|
about = option_env!("CARGO_PKG_DESCRIPTION").unwrap_or("File Info Fixer"),
|
|
before_help = "Copyright © 2021 Lynnesbian under the GPL3 (or later) License.",
|
|
before_long_help = "Copyright © 2021 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.",
|
|
setting(AppSettings::ColoredHelp)
|
|
)]
|
|
pub struct Parameters {
|
|
// NOTE: clap's comma-separated argument parser makes it impossible to specify extensions with commas in their name -
|
|
// `-e sil\,ly` is treated as ["sil", "ly"] rather than as ["silly"], no matter how i escape the comma (in bash,
|
|
// anyway). is this really an issue? it does technically exclude some perfectly valid extensions, but i've never seen
|
|
// a file extension with a comma in its name before.
|
|
/// Only examine files with these extensions.
|
|
/// Multiple extensions can be specified by either using the flag multiple times (`-e jpg -e png -e gif`), or by
|
|
/// separating them with commas (`-e jpg,png,gif`).
|
|
#[clap(short, long, use_delimiter = true, require_delimiter = true, value_name = "ext", validator = lowercase_exts)]
|
|
pub exts: Option<Vec<StringType>>,
|
|
|
|
/// Use these preset lists of extensions as the search filter (comma-separated list).
|
|
/// `media` includes all extensions from the `audio`, `video`, and `images` sets, making `-E media` equivalent to
|
|
/// `-E audio,video,images`.
|
|
#[clap(
|
|
short = 'E',
|
|
long,
|
|
arg_enum,
|
|
use_delimiter = true,
|
|
require_delimiter = true,
|
|
value_name = "set"
|
|
)]
|
|
pub ext_set: Vec<ExtensionSet>,
|
|
|
|
/// Don't scan files with these extensions.
|
|
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
|
#[clap(short = 'x', long, use_delimiter = true, require_delimiter = true, value_name = "ext", validator = lowercase_exts)]
|
|
pub exclude: Option<Vec<StringType>>,
|
|
|
|
/// Exclude files using a preset list of extensions.
|
|
/// This option takes precedence over extensions specified with `-e` or `-E`.
|
|
#[clap(
|
|
short = 'X',
|
|
long,
|
|
arg_enum,
|
|
use_delimiter = true,
|
|
require_delimiter = true,
|
|
value_name = "set"
|
|
)]
|
|
pub exclude_set: Vec<ExtensionSet>,
|
|
|
|
/// Don't skip hidden files and directories.
|
|
/// Even if this flag is not present, fif will still recurse into a hidden root directory - for example, `fif
|
|
/// ~/.hidden` will recurse into `~/.hidden` regardless of whether or not -s was passed as an argument.
|
|
#[clap(short, long)]
|
|
pub scan_hidden: bool,
|
|
|
|
/// Scan files without extensions.
|
|
/// By default, fif will ignore files without extensions - for example, a jpeg file named `photo` won't be considered
|
|
/// misnamed. Supplying the -S flag will cause fif to recommend renaming this file to `photo.jpg`.
|
|
#[clap(short = 'S', long)]
|
|
pub scan_extensionless: bool,
|
|
|
|
/// Output format to use.
|
|
/// By default, fif will output a PowerShell script on Windows, and a Bourne Shell script on other platforms.
|
|
#[clap(short, long, default_value = DEFAULT_FORMAT, arg_enum, value_name = "format")]
|
|
pub output_format: OutputFormat,
|
|
|
|
/// Follow symlinks.
|
|
#[clap(short, long)]
|
|
pub follow_symlinks: bool,
|
|
|
|
/// Output verbosity. Each additional `-v` increases verbosity.
|
|
/// Can be overridden by RUST_LOG.
|
|
#[clap(short, long, parse(from_occurrences), group = "verbosity")]
|
|
pub verbose: u8,
|
|
|
|
/// Output quietness. Each additional `-q` decreases verbosity.
|
|
#[clap(short, long, parse(from_occurrences), group = "verbosity")]
|
|
pub quiet: u8,
|
|
|
|
/// The directory to process.
|
|
#[clap(name = "DIR", default_value = ".", parse(from_os_str))]
|
|
pub dir: PathBuf,
|
|
}
|
|
|
|
fn lowercase_exts(exts: &str) -> Result<(), String> {
|
|
// TODO: i would much rather accept uppercase exts and convert them to lowercase than just rejecting lowercase exts...
|
|
if exts.to_lowercase() != exts {
|
|
return Err(String::from("Supplied extensions must be lowercase"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Further options relating to scanning.
|
|
#[derive(PartialEq, Debug)]
|
|
pub struct ScanOpts {
|
|
/// Whether hidden files and directories should be scanned.
|
|
pub hidden: bool,
|
|
/// Whether files without extensions should be scanned.
|
|
pub extensionless: bool,
|
|
/// Should symlinks be followed?
|
|
pub follow_symlinks: bool,
|
|
}
|
|
|
|
impl Parameters {
|
|
/// Returns an optional vec of the extensions to be scanned - i.e., extensions specified via the `-e` or `-E` flag,
|
|
/// minus the extensions excluded with the `-x` flag; i.e., the difference between the included and excluded sets.
|
|
pub fn extensions(&self) -> Option<BTreeSet<&str>> {
|
|
if let Some(included) = self.included_extensions() {
|
|
if let Some(excluded) = self.excluded_extensions() {
|
|
// return included extensions without excluded extensions
|
|
// ...maybe i should have called them "suffixes" instead of extensions...
|
|
Some(included.into_iter().filter(|ext| !excluded.contains(ext)).collect())
|
|
} else {
|
|
// no extensions excluded - just return all included
|
|
Some(included)
|
|
}
|
|
} else {
|
|
// no extensions included - return none
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Returns an optional vec of extensions that were specified by `-e` or `-E`. Note that this doesn't account for
|
|
/// extensions excluded by the exclusion flags.
|
|
pub fn included_extensions(&self) -> Option<BTreeSet<&str>> {
|
|
let mut included = BTreeSet::new();
|
|
if let Some(exts) = self.exts.as_ref() {
|
|
// -e
|
|
included.extend(exts.iter().map(|ext| ext.as_str()));
|
|
}
|
|
|
|
if !&self.ext_set.is_empty() {
|
|
// -E
|
|
included.extend(self.ext_set.iter().flat_map(|set| set.extensions()));
|
|
}
|
|
|
|
match included {
|
|
x if x.is_empty() => None,
|
|
x => Some(x),
|
|
}
|
|
}
|
|
|
|
/// Returns an optional vec of extensions that were specified by `-x` or `-X`.
|
|
pub fn excluded_extensions(&self) -> Option<BTreeSet<&str>> {
|
|
let mut excluded = BTreeSet::new();
|
|
if let Some(exclude) = self.exclude.as_ref() {
|
|
// -x
|
|
excluded.extend(exclude.iter().map(|ext| ext.as_str()));
|
|
}
|
|
|
|
if !&self.exclude_set.is_empty() {
|
|
// -X
|
|
excluded.extend(self.exclude_set.iter().flat_map(|set| set.extensions()));
|
|
}
|
|
|
|
// excluded doesn't sound like a word anymore
|
|
// tongue twister: enter X-options' excellent extension exclusion
|
|
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,
|
|
}
|
|
}
|
|
|
|
pub fn default_verbosity(&self) -> &'static str {
|
|
#![allow(clippy::missing_const_for_fn)]
|
|
// match was not permitted inside const functions until 1.46
|
|
|
|
match self.quiet {
|
|
0 => {
|
|
match self.verbose {
|
|
0 => "info", // no verbosity flags specified
|
|
1 => "debug", // -v
|
|
_ => "trace", // -vv...
|
|
}
|
|
}
|
|
1 => "warn", // -q
|
|
2 => "error", // -qq
|
|
_ => "off", // -qqq...
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sets of extensions for use with [Parameter](crate::parameters::Parameters)'s `-E` flag.
|
|
#[derive(Clap, PartialEq, Debug)]
|
|
pub enum ExtensionSet {
|
|
/// Extensions used for image file formats, such as `png`, `jpeg`, `webp`, etc.
|
|
Images,
|
|
/// Extensions used for audio file formats, such as `mp3`, `ogg`, `flac`, etc.
|
|
Audio,
|
|
/// Extensions used for video file formats, such as `mkv`, `mp4`, `mov`, etc.
|
|
#[clap(alias = "videos")]
|
|
Video,
|
|
/// Extensions used for media file formats. This acts as a combination of the [Images](ExtensionSet::Images),
|
|
/// [Audio](ExtensionSet::Audio) and [Videos](ExtensionSet::Videos) variants.
|
|
Media,
|
|
/// Extensions used for document file formats, such as `pdf`, `odt`, `docx`, etc.
|
|
Documents,
|
|
/// Extensions used for text file formats, such as `txt`, `toml`, `html`, etc.
|
|
Text,
|
|
/// Extensions used for archive file formats, such as `zip`, `zst`, `gz`, etc.
|
|
Archives,
|
|
/// Extensions used for system file formats, such as `mbr`, `crash`, `dll`, etc.
|
|
System,
|
|
}
|
|
|
|
impl ExtensionSet {
|
|
/// The list of known extensions for this `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(),
|
|
// many compressed file types follow the name scheme "application/x.+compressed.*" - maybe this can be used
|
|
// somehow to extract extensions for compressed files from mime_guess?
|
|
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",
|
|
],
|
|
}
|
|
}
|
|
}
|