diff --git a/CHANGELOG.md b/CHANGELOG.md index 995c679..9de078c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Dates are given in YYYY-MM-DD format. ## v0.2 +### v0.2.14 (2021-xx-yy) +#### Features +- Added `-x`/`--exclude` flag for excluding file extensions (overrides `-e` or `-E`) + ### v0.2.13 (2021-04-26) #### Features - Added `-v`/`--verbose` flag for setting verbosity without using `RUST_LOG` diff --git a/Cargo.toml b/Cargo.toml index 559380c..9f5203e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,8 +66,9 @@ fastrand = "1.4.1" [profile.release] lto = "thin" +# perform some simple optimisations when testing [profile.test] -opt-level = 0 +opt-level = 1 # optimise dependencies, even when producing debug and test builds [profile.dev.package."*"] diff --git a/clippy.sh b/clippy.sh index 0197375..46f01fe 100755 --- a/clippy.sh +++ b/clippy.sh @@ -1,6 +1,6 @@ #!/bin/bash fd -e rs -x touch {} -cargo clippy --all-features -- \ +cargo clippy --all-features --tests -- \ -W clippy::nursery \ -W clippy::perf \ -W clippy::pedantic \ diff --git a/src/main.rs b/src/main.rs index 045319b..22282c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -78,10 +78,17 @@ fn main() { debug!("Iterating directory: {:?}", args.dirs); let extensions = args.extensions(); + let excludes = args.excluded_extensions(); - debug!("Checking files with extensions: {:?}", extensions); + if let Some(extensions) = &extensions { + debug!("Checking files with extensions: {:?}", extensions); + } else if let Some(excludes) = &excludes { + debug!("Skipping files with extensions: {:?}", excludes); + } else { + debug!("Checking files regardless of extensions"); + } - let entries = scan_directory(&args.dirs, extensions.as_ref(), &args.get_scan_opts()); + let entries = scan_directory(&args.dirs, extensions.as_ref(), excludes.as_ref(), &args.get_scan_opts()); if entries.is_none() { // no need to log anything for fatal errors - fif will already have printed something obvious like @@ -172,7 +179,7 @@ cfg_if! { /// Returns `true` if a file matches the given criteria. This means checking whether the file's extension appears in /// `exts` (if specified), potentially skipping over hidden files, and so on. -fn wanted_file(entry: &DirEntry, exts: Option<&Vec<&str>>, scan_opts: &ScanOpts) -> bool { +fn wanted_file(entry: &DirEntry, exts: Option<&Vec<&str>>, exclude: Option<&Vec<&str>>, scan_opts: &ScanOpts) -> bool { if entry.depth() == 0 { // the root directory should always be scanned. return true; @@ -188,19 +195,21 @@ fn wanted_file(entry: &DirEntry, exts: Option<&Vec<&str>>, scan_opts: &ScanOpts) return true; } - let ext = extension_from_path(entry.path()); + if let Some(ext) = extension_from_path(entry.path()) { + // file has extension - discard invalid UTF-8 and normalise it to lowercase. + let ext = ext.to_string_lossy().to_lowercase(); + let ext = ext.as_str(); - if ext.is_none() && !scan_opts.extensionless { - // don't scan files without extensions. - return false; - } - - if let Some(exts) = exts { - // only scan if the file has one of the specified extensions. - exts.contains(&ext.unwrap().to_string_lossy().to_lowercase().as_str()) + if let Some(exts) = exts { + // only scan if the file has one of the specified extensions. + exts.contains(&ext) + } else { + // no extensions specified - the file should be scanned unless its extension is on the exclude list. + exclude.map_or(true, |exclude| !exclude.contains(&ext)) + } } else { - // no extensions specified - no reason not to scan this file. - true + // no file extension + scan_opts.extensionless } } @@ -276,11 +285,11 @@ fn scan_from_walkdir(entries: &[DirEntry]) -> Vec> { /// Scans a given directory with [`WalkDir`], filters with [`wanted_file`], checks for errors, and returns a vector of /// [DirEntry]s. -fn scan_directory(dirs: &Path, exts: Option<&Vec<&str>>, scan_opts: &ScanOpts) -> Option> { +fn scan_directory(dirs: &Path, exts: Option<&Vec<&str>>, exclude: Option<&Vec<&str>>, scan_opts: &ScanOpts) -> Option> { let stepper = WalkDir::new(dirs).follow_links(scan_opts.follow_symlinks).into_iter(); let mut probably_fatal_error = false; let entries: Vec = stepper - .filter_entry(|e| wanted_file(e, exts, scan_opts)) // filter out unwanted files + .filter_entry(|e| wanted_file(e, exts, exclude, scan_opts)) // filter out unwanted files .filter_map(|e| { if let Err(err) = &e { debug!("uh oh spaghettio!! {:#?}", e); diff --git a/src/parameters.rs b/src/parameters.rs index 77e0cb2..57133aa 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -40,7 +40,7 @@ pub enum OutputFormat { setting(AppSettings::ColoredHelp) )] pub struct Parameters { - /// Only examine files with these extensions (Comma-separated list). + /// 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>, @@ -50,6 +50,16 @@ pub struct Parameters { #[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>, + /// 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. @@ -94,19 +104,41 @@ pub struct ScanOpts { } 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(|s| s.as_str()).collect()) + 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()) + 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> { + self.exclude.as_ref().map(|exclude| exclude.iter().map(|ext| ext.as_str()).collect()) + } + pub const fn get_scan_opts(&self) -> ScanOpts { ScanOpts { hidden: self.scan_hidden, diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 5221e45..8099c59 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -113,7 +113,7 @@ fn simple_directory() { follow_symlinks: false, }; - let entries = scan_directory(&dir.path().to_path_buf(), None, &scan_opts).expect("Directory scan failed."); + let entries = scan_directory(&dir.path().to_path_buf(), None, None, &scan_opts).expect("Directory scan failed."); assert_eq!(entries.len(), files.len()); @@ -168,7 +168,6 @@ fn simple_directory() { /// Ensure that command line argument parsing works correctly - flags are interpreted, booleans are set, and so on. fn argument_parsing() { use crate::parameters::{Parameters, ScanOpts}; - use clap::Clap; // pass `-f`, which enables following symlinks, and `-E images`, which scans files with image extensions @@ -177,8 +176,10 @@ fn argument_parsing() { // check if "jpg" is in the list of extensions to be scanned assert!(args .extensions() - .expect("args.extensions() should contain the `images` set!") - .contains(&"jpg")); + .expect("args.extensions() should be Some(_)!") + .contains(&"jpg"), + "args.extensions() should contain the `images` set!" + ); // make sure "scan_hidden" is false assert!(!args.scan_hidden); @@ -186,6 +187,9 @@ fn argument_parsing() { // exts should be none assert!(args.exts.is_none()); + // there shouldn't be any excluded extensions + assert!(args.excluded_extensions().is_none()); + // get the ScanOpts, and make sure they match expectations assert_eq!( args.get_scan_opts(), @@ -198,6 +202,36 @@ fn argument_parsing() { ) } +#[test] +/// Ensure exclude overrides `-e` and `-E`. +fn exclude_overrides() { + use crate::parameters::{Parameters}; + use clap::Clap; + + // pass `-E images`, which includes many image extensions, and `-x jpg,png`, which should remove "jpg" and "png" from + // the extensions list + let args: Parameters = Parameters::parse_from(vec!["fif", "-x", "jpg,png", "-E", "images"]); + let extensions = args.extensions(); + assert!(extensions.is_some(), "Extensions should contain the `images` set!"); + let extensions = extensions.unwrap(); + + assert!(!extensions.contains(&"jpg"), "\"jpg\" should be excluded!"); + assert!(!extensions.contains(&"png"), "\"png\" should be excluded!"); + assert!(extensions.contains(&"jpeg"), "\"jpeg\" should be included!"); + + // pass `-e abc,def,ghi,jkl` and `-x abc,def` -- extensions() should only contain "ghi" and "jkl" + let args: Parameters = Parameters::parse_from(vec!["fif", "-e", "abc,def,ghi,jkl", "-x", "abc,def"]); + let extensions = args.extensions(); + assert!(extensions.is_some(), "Extensions should be set!"); + let extensions = extensions.unwrap(); + + assert!(!extensions.contains(&"abc")); + assert!(!extensions.contains(&"def")); + assert!(extensions.contains(&"ghi")); + assert!(extensions.contains(&"jkl")); +} + + #[test] /// Ensure that badly formed command line arguments are rejected. fn rejects_bad_args() {