diff --git a/Cargo.lock b/Cargo.lock index 5bafc0dab..9b676e0fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -313,9 +315,11 @@ dependencies = [ "jiff", "libc", "lscolors", + "memchr", "nix 0.31.2", "normpath", "nu-ansi-term", + "pcre2", "regex", "regex-syntax", "tempfile", @@ -345,6 +349,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -353,7 +369,7 @@ checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] @@ -479,6 +495,16 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -588,6 +614,34 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pcre2" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e970b0fcce0c7ee6ef662744ff711f21ccd6f11b7cf03cd187a80e89797fc67" +dependencies = [ + "libc", + "log", + "pcre2-sys", +] + +[[package]] +name = "pcre2-sys" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18b9073c1a2549bd409bf4a32c94d903bb1a09bf845bc306ae148897fa0760a4" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -631,6 +685,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -766,7 +826,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/Cargo.toml b/Cargo.toml index c147f60d7..ea1e41f13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ ignore = "0.4.25" regex = "1.12.2" regex-syntax = "0.8" ctrlc = "3.5" -globset = "0.4" +globset = "0.4.18" anyhow = "1.0" etcetera = "0.11" normpath = "1.5.1" @@ -46,6 +46,8 @@ crossbeam-channel = "0.5.15" clap_complete = {version = "4.6.5", optional = true} faccess = "0.2.4" jiff = "0.2.27" +memchr = "2.8.0" +pcre2 = {version = "0.2.11", optional = true} [dependencies.clap] version = "4.6.1" @@ -95,6 +97,7 @@ codegen-units = 1 use-jemalloc = ["tikv-jemallocator"] completions = ["clap_complete"] base = ["use-jemalloc"] +pcre = ["dep:pcre2"] default = ["use-jemalloc", "completions"] [package.metadata.binstall] diff --git a/src/cli.rs b/src/cli.rs index 477f7fdf9..9d2943b48 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -171,6 +171,18 @@ pub struct Opts { )] pub regex: bool, + /// Use the PCRE regex engine + /// + /// This allows you to use features like backreferences and lookarounds. + #[cfg(feature = "pcre")] + #[arg( + long, + overrides_with_all(["glob", "regex"]), + conflicts_with("fixed_strings"), + long_help + )] + pub pcre: bool, + /// Treat the pattern as a literal string instead of a regular expression. Note /// that this also performs substring comparison. If you want to match on an /// exact filename, consider using '--glob' or '--exact' instead. @@ -629,13 +641,11 @@ pub struct Opts { /// is considered a match. If your pattern starts with a dash (-), make sure to /// pass '--' first, or it will be considered as a flag (fd -- '-foo'). #[arg( - default_value = "", - hide_default_value = true, value_name = "pattern", help = "the search pattern (a regular expression, unless '--glob' is used; optional)", long_help )] - pub pattern: String, + pub pattern: Option, /// Set the path separator to use when printing file paths. The default is /// the OS-specific separator ('/' on Unix, '\' on Windows). diff --git a/src/config.rs b/src/config.rs index a027812a4..58a1169a9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,9 +12,6 @@ use crate::fmt::FormatTemplate; /// Configuration options for *fd*. pub struct Config { - /// Whether the search is case-sensitive or case-insensitive. - pub case_sensitive: bool, - /// Cached current working directory for absolute path construction. /// Populated when `--full-path` is set; `None` means search by filename only. pub full_path_base: Option, diff --git a/src/filesystem.rs b/src/filesystem.rs index 4a04f9d52..44e4faf01 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -1,6 +1,4 @@ -use std::borrow::Cow; use std::env; -use std::ffi::OsStr; use std::fs; use std::io; #[cfg(any(unix, target_os = "redox"))] @@ -99,22 +97,6 @@ pub fn is_pipe(_: fs::FileType) -> bool { false } -#[cfg(any(unix, target_os = "redox"))] -pub fn osstr_to_bytes(input: &OsStr) -> Cow<'_, [u8]> { - use std::os::unix::ffi::OsStrExt; - Cow::Borrowed(input.as_bytes()) -} - -#[cfg(windows)] -pub fn osstr_to_bytes(input: &OsStr) -> Cow<'_, [u8]> { - let string = input.to_string_lossy(); - - match string { - Cow::Owned(string) => Cow::Owned(string.into_bytes()), - Cow::Borrowed(string) => Cow::Borrowed(string.as_bytes()), - } -} - /// Remove the `./` prefix from a path. pub fn strip_current_dir(path: &Path) -> &Path { path.strip_prefix(".").unwrap_or(path) diff --git a/src/main.rs b/src/main.rs index 42868ba32..de3a15fff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod filter; mod fmt; mod hyperlink; mod output; +mod patterns; mod regex_helper; mod walk; @@ -20,9 +21,9 @@ use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail}; use clap::{CommandFactory, Parser}; -use globset::GlobBuilder; use lscolors::LsColors; -use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder}; +use patterns::PatternType; +use regex::bytes::RegexSetBuilder; use crate::cli::{ColorWhen, HyperlinkWhen, Opts}; use crate::config::Config; @@ -32,6 +33,7 @@ use crate::filetypes::FileTypes; #[cfg(unix)] use crate::filter::OwnerFilter; use crate::filter::TimeFilter; +use crate::patterns::build_patterns; use crate::regex_helper::{pattern_has_uppercase_char, pattern_matches_strings_with_leading_dot}; // We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481 @@ -72,7 +74,7 @@ fn main() { } fn run() -> Result { - let opts = Opts::parse(); + let mut opts = Opts::parse(); #[cfg(feature = "completions")] if let Some(shell) = opts.gen_completions()? { @@ -86,28 +88,24 @@ fn run() -> Result { } ensure_search_pattern_is_not_a_path(&opts)?; - let pattern = &opts.pattern; - let exprs = &opts.exprs; - let empty = Vec::new(); + let mut patterns = opts.exprs.take().unwrap_or(Vec::new()); + if let Some(pattern) = opts.pattern.take() { + patterns.push(pattern); + } - let pattern_regexps = exprs - .as_ref() - .unwrap_or(&empty) - .iter() - .chain([pattern]) - .map(|pat| build_pattern_regex(pat, &opts)) - .collect::>>()?; + let pattern_type = determine_pattern_type(&opts); + // The search will be case-sensitive if the command line flag is set or + // if any of the patterns has an uppercase character (smart case). + let ignore_case = opts.ignore_case + || !(opts.case_sensitive || patterns.iter().any(|pat| pattern_has_uppercase_char(pat))); - let config = construct_config(opts, &pattern_regexps)?; + let config = construct_config(opts)?; - ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?; + ensure_use_hidden_option_for_leading_dot_pattern(&patterns, &config, pattern_type)?; - let regexps = pattern_regexps - .into_iter() - .map(|pat| build_regex(pat, &config)) - .collect::>>()?; + let matcher = build_patterns(patterns, pattern_type, ignore_case)?; - walk::scan(&search_paths, regexps, config) + walk::scan(&search_paths, matcher, config) } #[cfg(feature = "completions")] @@ -167,54 +165,52 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> { if opts.full_path { return Ok(()); } + if let Some(ref pattern) = opts.pattern { + // Start with the cheap check: '/' is always a path separator, including on + // Windows, and has no regex meaning, so flagging it is safe and catches the + // Linux/macOS mistake of pasting a full path as the pattern. + #[cfg_attr(not(windows), allow(unused_mut))] + let mut should_warn = pattern.contains('/'); + + // On Windows we additionally accept the native `\` separator, but only when + // the pattern actually resolves to an existing directory - `\` is also the + // regex escape char there, so valid patterns like `\Ac` or `\d+` must still + // run. The is_dir syscall is only needed when `should_warn` is still false, + // so short-circuit via `||` to avoid the stat call on the happy path. + #[cfg(windows)] + { + should_warn = should_warn + || (pattern.contains(std::path::MAIN_SEPARATOR) && Path::new(pattern).is_dir()); + } - // Start with the cheap check: '/' is always a path separator, including on - // Windows, and has no regex meaning, so flagging it is safe and catches the - // Linux/macOS mistake of pasting a full path as the pattern. - #[cfg_attr(not(windows), allow(unused_mut))] - let mut should_warn = opts.pattern.contains('/'); - - // On Windows we additionally accept the native `\` separator, but only when - // the pattern actually resolves to an existing directory - `\` is also the - // regex escape char there, so valid patterns like `\Ac` or `\d+` must still - // run. The is_dir syscall is only needed when `should_warn` is still false, - // so short-circuit via `||` to avoid the stat call on the happy path. - #[cfg(windows)] - { - should_warn = should_warn - || (opts.pattern.contains(std::path::MAIN_SEPARATOR) - && Path::new(&opts.pattern).is_dir()); - } - - if should_warn { - Err(anyhow!( - "The search pattern '{pattern}' contains a path-separation character \ + if should_warn { + return Err(anyhow!( + "The search pattern '{pattern}' contains a path-separation character \ and will not lead to any search results.\n\n\ If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n \ fd . '{pattern}'\n\n\ Instead, if you want your pattern to match the full file path, use:\n\n \ - fd --full-path '{pattern}'", - pattern = &opts.pattern, - )) - } else { - Ok(()) + fd --full-path '{pattern}'" + )); + } } + Ok(()) } -fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result { - Ok(if opts.glob && !pattern.is_empty() { - let glob = GlobBuilder::new(pattern).literal_separator(true).build()?; - glob.regex().to_owned() +fn determine_pattern_type(opts: &Opts) -> PatternType { + #[cfg(feature = "pcre")] + if opts.pcre { + return PatternType::Pcre; + } + if opts.glob { + PatternType::Glob } else if opts.exact { - // Anchor the escaped pattern so the full filename (or path) must match exactly. - // Literal. No substring matching. - format!("^{}$", regex::escape(pattern)) + PatternType::Exact } else if opts.fixed_strings { - // Treat pattern as literal string if '--fixed-strings' is used - regex::escape(pattern) + PatternType::Fixed } else { - String::from(pattern) - }) + PatternType::Regex + } } fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> { @@ -231,15 +227,7 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> { } } -fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result { - // The search will be case-sensitive if the command line flag is set or - // if any of the patterns has an uppercase character (smart case). - let case_sensitive = !opts.ignore_case - && (opts.case_sensitive - || pattern_regexps - .iter() - .any(|pat| pattern_has_uppercase_char(pat))); - +fn construct_config(mut opts: Opts) -> Result { let path_separator = opts .path_separator .take() @@ -294,7 +282,6 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result Result> { } fn ensure_use_hidden_option_for_leading_dot_pattern( + patterns: &[String], config: &Config, - pattern_regexps: &[String], + pattern_type: PatternType, ) -> Result<()> { if cfg!(unix) && config.ignore_hidden - && pattern_regexps - .iter() - .any(|pat| pattern_matches_strings_with_leading_dot(pat)) + && patterns_match_strings_with_leading_dots(patterns, pattern_type) { Err(anyhow!( "The pattern(s) seems to only match files with a leading dot, but hidden files are \ @@ -524,18 +510,20 @@ fn ensure_use_hidden_option_for_leading_dot_pattern( } } -fn build_regex(pattern_regex: String, config: &Config) -> Result { - RegexBuilder::new(&pattern_regex) - .case_insensitive(!config.case_sensitive) - .dot_matches_new_line(true) - .build() - .map_err(|e| { - anyhow!( - "{}\n\nNote: You can search for literal substrings with '--fixed-strings' \ - or literal strings with '--exact' options (instead of a regular expression). \ - Alternatively, you can \ - also use the '--glob' option to match on a glob pattern.", - e - ) - }) +fn patterns_match_strings_with_leading_dots( + patterns: &[String], + pattern_type: PatternType, +) -> bool { + let mut iter = patterns.iter(); + match pattern_type { + PatternType::Regex => iter.any(|pat| pattern_matches_strings_with_leading_dot(pat)), + // For PCRE just do a basic check if the pattern starts with "\." for a literal + // . since we can't parse it to an AST. + #[cfg(feature = "pcre")] + PatternType::Pcre => iter.any(|pat| pat.starts_with("^\\.")), + // fixed strings aren't anchored so always false + PatternType::Fixed => false, + // globs and exact just check if it starts with a . + PatternType::Glob | PatternType::Exact => patterns.iter().any(|pat| pat.starts_with(".")), + } } diff --git a/src/patterns.rs b/src/patterns.rs new file mode 100644 index 000000000..d35786a2b --- /dev/null +++ b/src/patterns.rs @@ -0,0 +1,194 @@ +use std::cell::RefCell; +use std::ffi::OsStr; + +use anyhow::{Result, anyhow}; +use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; +use memchr::memmem; +use regex::bytes::{RegexSet, RegexSetBuilder}; + +pub trait Matcher { + fn matches_path(&self, path: &OsStr) -> bool; +} + +pub type Patterns = Box; + +#[derive(Eq, PartialEq, Copy, Clone)] +pub enum PatternType { + Regex, + Fixed, + Glob, + Exact, + #[cfg(feature = "pcre")] + Pcre, +} + +impl Matcher for RegexSet { + fn matches_path(&self, path: &OsStr) -> bool { + let haystack = path.as_encoded_bytes(); + let matches = self.matches(haystack); + // Return true if the number of regexes that matched + // equals the total number of regexes. + matches.iter().count() == self.len() + } +} + +#[cfg(feature = "pcre")] +impl Matcher for Vec { + fn matches_path(&self, path: &OsStr) -> bool { + let path = path.as_encoded_bytes(); + self.iter().all(|pat| pat.is_match(path).unwrap()) + } +} + +thread_local! { + /// Thread local cache for Vec to use for globset matches + static GLOB_MATCHES: RefCell> = const { RefCell::new(Vec::new()) }; +} +impl Matcher for GlobSet { + fn matches_path(&self, path: &OsStr) -> bool { + GLOB_MATCHES.with_borrow_mut(|matches| { + matches.clear(); + self.matches_into(path, matches); + matches.len() == self.len() + }) + } +} + +/// In the common case a single glob, it is simpler, and +/// faster to just use a single Glob instead of a GlobSet +impl Matcher for GlobMatcher { + fn matches_path(&self, path: &OsStr) -> bool { + self.is_match(path) + } +} + +/// Matcher that matches fixed strings +pub struct FixedStrings(pub Vec); + +impl Matcher for FixedStrings { + fn matches_path(&self, path: &OsStr) -> bool { + let path = path.as_encoded_bytes(); + self.0.iter().all(|f| bytes_contains(path, f.as_bytes())) + } +} + +pub struct ExactStrings(pub Vec); + +impl Matcher for ExactStrings { + fn matches_path(&self, path: &OsStr) -> bool { + self.0.iter().all(|s| s.as_str() == path) + } +} + +/// Matcher that matches everything +pub struct MatchAll; +impl Matcher for MatchAll { + fn matches_path(&self, _path: &OsStr) -> bool { + true + } +} +pub fn build_patterns( + mut patterns: Vec, + pattern_type: PatternType, + ignore_case: bool, +) -> Result { + if patterns.is_empty() { + return Ok(Box::new(MatchAll)); + } + match pattern_type { + PatternType::Glob => build_glob_matcher(patterns, ignore_case), + #[cfg(feature = "pcre")] + PatternType::Pcre => Ok(Box::new(build_pcre_matcher(patterns, ignore_case)?)), + PatternType::Fixed if !ignore_case => Ok(Box::new(FixedStrings(patterns))), + PatternType::Exact if !ignore_case => Ok(Box::new(ExactStrings(patterns))), + typ => { + // TODO: is there a better way we could handle case insensitive fixed strings? + if typ == PatternType::Fixed { + for pattern in patterns.iter_mut() { + *pattern = regex::escape(pattern); + } + } else if typ == PatternType::Exact { + // Would using eq_ignore_ascii case be good enough? + for pattern in patterns.iter_mut() { + // Anchor the escaped pattern so the full filename (or path) must match exactly. + // Literal. No substring matching. + *pattern = format!("^{}$", regex::escape(pattern)); + } + } + Ok(Box::new(build_regex_matcher(patterns, ignore_case)?)) + } + } +} + +fn build_glob_matcher(patterns: Vec, ignore_case: bool) -> Result { + Ok(if patterns.len() == 1 { + Box::new(build_glob(&patterns[0], ignore_case)?.compile_matcher()) + } else { + let mut builder = GlobSetBuilder::new(); + for pat in patterns { + eprintln!("adding glob for {}", pat); + builder.add(build_glob(&pat, ignore_case)?); + } + Box::new(builder.build()?) + }) +} + +fn build_glob(pattern: &str, ignore_case: bool) -> Result { + Ok(GlobBuilder::new(pattern) + .literal_separator(true) + .case_insensitive(ignore_case) + .build()?) +} + +// Should we enable the unicde/utf8 features for regex and pcre? + +#[cfg(feature = "pcre")] +fn build_pcre_matcher( + patterns: Vec, + ignore_case: bool, +) -> Result> { + use pcre2::bytes::RegexBuilder; + patterns + .iter() + .map(|pat| { + RegexBuilder::new() + .dotall(true) + .caseless(ignore_case) + .build(pat) + .map_err(|e| { + anyhow!( + "{}\n\nNote: You can search for literal substrings with '--fixed-strings' \ + or literal strings with '--exact' options (instead of a regular expression). \ + Alternatively, you can also use the '--glob' option to match on a glob pattern.", + e + ) + }) + }) + .collect() +} + +#[cfg(feature = "pcre")] +const PCRE_ALT_MSG: &str = " Use --pcre to enable perl-compatible regex features."; +#[cfg(not(feature = "pcre"))] +const PCRE_ALT_MSG: &str = ""; + +fn build_regex_matcher(patterns: Vec, ignore_case: bool) -> Result { + RegexSetBuilder::new(patterns) + .case_insensitive(ignore_case) + .dot_matches_new_line(true) + .build() + .map_err(|e| { + anyhow!( + "{}\n\nNote: You can search for literal substrings with '--fixed-strings' \ + or literal strings with '--exact' options (instead of a regular expression). \ + Alternatively, you can also use the '--glob' option to match on a glob pattern.{}", + e, + PCRE_ALT_MSG + ) + }) +} + +/// Test if the needle is a substring of the haystack +fn bytes_contains(haystack: &[u8], needle: &[u8]) -> bool { + memmem::find(haystack, needle).is_some() +} diff --git a/src/walk.rs b/src/walk.rs index 018ad2f70..03e892cf6 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -13,15 +13,14 @@ use crossbeam_channel::{Receiver, RecvTimeoutError, SendError, Sender, bounded}; use etcetera::BaseStrategy; use ignore::overrides::{Override, OverrideBuilder}; use ignore::{WalkBuilder, WalkParallel, WalkState}; -use regex::bytes::Regex; use crate::config::Config; use crate::dir_entry::DirEntry; use crate::error::print_error; use crate::exec; use crate::exit_codes::{ExitCode, merge_exitcodes}; -use crate::filesystem; use crate::output; +use crate::patterns::Patterns; /// The receiver thread can either be buffering results or directly streaming to the console. #[derive(PartialEq)] @@ -305,7 +304,7 @@ impl<'a, W: Write> ReceiverBuffer<'a, W> { /// State shared by the sender and receiver threads. struct WorkerState { /// The search patterns. - patterns: Vec, + patterns: Patterns, /// The command line configuration. config: Config, /// Flag for cleanly shutting down the parallel walk @@ -315,7 +314,7 @@ struct WorkerState { } impl WorkerState { - fn new(patterns: Vec, config: Config) -> Self { + fn new(patterns: Patterns, config: Config) -> Self { let quit_flag = Arc::new(AtomicBool::new(false)); let interrupt_flag = Arc::new(AtomicBool::new(false)); @@ -526,17 +525,14 @@ impl WorkerState { let search_str = search_str_for_entry(entry_path, config.full_path_base.as_deref()); - if !patterns - .iter() - .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref()))) - { + if !patterns.matches_path(&search_str) { return WalkState::Continue; } // Filter out unwanted extensions. if let Some(ref exts_regex) = config.extensions { if let Some(path_str) = entry_path.file_name() { - if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) { + if !exts_regex.is_match(path_str.as_encoded_bytes()) { return WalkState::Continue; } } else { @@ -692,7 +688,7 @@ fn search_str_for_entry<'a>( /// If the `--exec` argument was supplied, this will create a thread pool for executing /// jobs in parallel from a given command line and the discovered paths. Otherwise, each /// path will simply be written to standard output. -pub fn scan(paths: &[PathBuf], patterns: Vec, config: Config) -> Result { +pub fn scan(paths: &[PathBuf], patterns: Patterns, config: Config) -> Result { WorkerState::new(patterns, config).scan(paths) }