//! [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::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. Sh, /// A PowerShell script. PowerShell, /// Also a PowerShell script, with different casing to allow for `fif -o powershell`. Powershell, /// Plain text. Text, } // 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 { /// Only examine files with these extensions (comma-separated list). /// This argument conflicts with `-E`. #[clap(short, long, use_delimiter = true, require_delimiter = true, group = "extensions")] pub exts: Option>, /// Use a preset list of extensions as the search filter. /// `media` includes all extensions from the `audio`, `video`, and `images` sets. This argument conflicts with `-e`. #[clap(short = 'E', long, arg_enum, group = "extensions")] pub ext_set: Option, /// Don't scan files with these extensions (comma-separated list). /// This option takes preference over files specified with -e or -E. #[clap(short = 'x', long, use_delimiter = true, require_delimiter = true)] pub exclude: Option>, /// Exclude files using a preset list of extensions. #[clap(short = 'X', long, arg_enum)] pub exclude_set: Option, /// 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)] pub output_format: OutputFormat, /// Follow symlinks. #[clap(short, long)] pub follow_symlinks: bool, /// Output verbosity. Defaults to only logging warnings and errors. /// Can be overridden by RUST_LOG. #[clap(short, long, parse(from_occurrences))] pub verbose: u8, /// The directory to process. // TODO: right now this can only take a single directory - should this be improved? #[clap(name = "DIR", default_value = ".", parse(from_os_str))] pub dirs: PathBuf, } /// 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. pub fn extensions(&self) -> Option> { let empty_vec = vec![]; let exclude = &self.excluded_extensions().unwrap_or(empty_vec); // TODO: bleugh if let Some(exts) = &self.exts { // extensions supplied like "-e png,jpg,jpeg" Some( exts .iter() .map(|ext| ext.as_str()) .filter(|ext| !exclude.contains(ext)) .collect(), ) } else if let Some(exts) = &self.ext_set { // extensions supplied like "-E images" Some( exts .extensions() .into_iter() .filter(|ext| !exclude.contains(ext)) .collect(), ) } else { // neither -E nor -e was passed None } } pub fn excluded_extensions(&self) -> Option> { // start with an empty vec let mut excluded = vec![]; if let Some(exclude) = self.exclude.as_ref() { // add extensions excluded by `-x` excluded.append(&mut exclude.iter().map(|ext| ext.as_str()).collect()); } if let Some(exclude_set) = &self.exclude_set { // add extensions excluded by `-X` excluded.append(&mut exclude_set.extensions()); } if excluded.is_empty() { // no extensions to exclude - return none None } else { // excluded doesn't sound like a word anymore // tongue twister: enter X-options' excellent extension exclusion Some(excluded) } } 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.verbose { 0 => "warn", 1 => "info", 2 => "debug", _ => "trace", } } } /// 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. Videos, /// 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::Videos => mime_guess::get_mime_extensions_str("video/*").unwrap().to_vec(), Self::Media => [ Self::Images.extensions(), Self::Audio.extensions(), Self::Videos.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().to_vec(), // 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"], Self::System => vec![ "com", "dll", "exe", "sys", "reg", "nt", "cpl", "msi", "efi", "bio", "rcv", "mbr", "sbf", "grub", "ko", "dylib", "pdb", "hdmp", "crash", ], } } }