diff options
author | David Peter <sharkdp@users.noreply.github.com> | 2022-11-21 21:19:08 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-11-21 21:19:08 +0100 |
commit | c9d39684758d0d898082370d5c5fe7e8aef3a385 (patch) | |
tree | d03e8013844360bdd4a7380b65bd7aed5de4145e | |
parent | 8dda4998304e4fef04bc0119aa7114d47969129a (diff) | |
parent | 36e60223ebd2904f277719f281eefef005afa2c7 (diff) |
Merge pull request #1139 from Uthar/master
add support for matching multiple patterns
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | src/cli.rs | 12 | ||||
-rw-r--r-- | src/main.rs | 54 | ||||
-rw-r--r-- | src/walk.rs | 15 | ||||
-rw-r--r-- | tests/tests.rs | 197 |
5 files changed, 258 insertions, 22 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 713f6b6..44e5aa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Features +- New `--and <pattern>` option to add additional patterns that must also be matched. See #315 + and #1139 (@Uthar) - Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`. @@ -144,6 +144,18 @@ pub struct Opts { )] pub fixed_strings: bool, + /// Additional search patterns that need to be matched + #[arg( + long = "and", + value_name = "pattern", + long_help = "Add additional required search patterns, all of which must be matched. Multiple \ + additional patterns can be specified. The patterns are regular expressions, \ + unless '--glob' or '--fixed-strings' is used.", + hide_short_help = true, + allow_hyphen_values = true + )] + pub exprs: Option<Vec<String>>, + /// Show absolute instead of relative paths #[arg( long, diff --git a/src/main.rs b/src/main.rs index cdc48c7..ec677cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ use atty::Stream; use clap::{CommandFactory, Parser}; use globset::GlobBuilder; use lscolors::LsColors; -use regex::bytes::{RegexBuilder, RegexSetBuilder}; +use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder}; use crate::cli::{ColorWhen, Opts}; use crate::config::Config; @@ -81,12 +81,28 @@ fn run() -> Result<ExitCode> { } ensure_search_pattern_is_not_a_path(&opts)?; - let pattern_regex = build_pattern_regex(&opts)?; + let pattern = &opts.pattern; + let exprs = &opts.exprs; + let empty = Vec::new(); + + let pattern_regexps = exprs + .as_ref() + .unwrap_or(&empty) + .iter() + .chain([pattern]) + .map(|pat| build_pattern_regex(pat, &opts)) + .collect::<Result<Vec<String>>>()?; + + let config = construct_config(opts, &pattern_regexps)?; - let config = construct_config(opts, &pattern_regex)?; - ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?; - let re = build_regex(pattern_regex, &config)?; - walk::scan(&search_paths, Arc::new(re), Arc::new(config)) + ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?; + + let regexps = pattern_regexps + .into_iter() + .map(|pat| build_regex(pat, &config)) + .collect::<Result<Vec<Regex>>>()?; + + walk::scan(&search_paths, Arc::new(regexps), Arc::new(config)) } #[cfg(feature = "completions")] @@ -145,8 +161,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> { } } -fn build_pattern_regex(opts: &Opts) -> Result<String> { - let pattern = &opts.pattern; +fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> { Ok(if opts.glob && !pattern.is_empty() { let glob = GlobBuilder::new(pattern).literal_separator(true).build()?; glob.regex().to_owned() @@ -172,11 +187,14 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> { } } -fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> { +fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config> { // The search will be case-sensitive if the command line flag is set or - // if the pattern has an uppercase character (smart case). - let case_sensitive = - !opts.ignore_case && (opts.case_sensitive || pattern_has_uppercase_char(pattern_regex)); + // 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))); let path_separator = opts .path_separator @@ -415,14 +433,18 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> { fn ensure_use_hidden_option_for_leading_dot_pattern( config: &Config, - pattern_regex: &str, + pattern_regexps: &[String], ) -> Result<()> { - if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex) + if cfg!(unix) + && config.ignore_hidden + && pattern_regexps + .iter() + .any(|pat| pattern_matches_strings_with_leading_dot(pat)) { Err(anyhow!( - "The pattern seems to only match files with a leading dot, but hidden files are \ + "The pattern(s) seems to only match files with a leading dot, but hidden files are \ filtered by default. Consider adding -H/--hidden to search hidden files as well \ - or adjust your search pattern." + or adjust your search pattern(s)." )) } else { Ok(()) diff --git a/src/walk.rs b/src/walk.rs index 29e591b..a1afda9 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -47,12 +47,12 @@ pub const MAX_BUFFER_LENGTH: usize = 1000; /// Default duration until output buffering switches to streaming. pub const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100); -/// Recursively scan the given search path for files / pathnames matching the pattern. +/// Recursively scan the given search path for files / pathnames matching the patterns. /// /// 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], pattern: Arc<Regex>, config: Arc<Config>) -> Result<ExitCode> { +pub fn scan(paths: &[PathBuf], patterns: Arc<Vec<Regex>>, config: Arc<Config>) -> Result<ExitCode> { let first_path = &paths[0]; // Channel capacity was chosen empircally to perform similarly to an unbounded channel @@ -153,7 +153,7 @@ pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Resu let receiver_thread = spawn_receiver(&config, &quit_flag, &interrupt_flag, rx); // Spawn the sender threads. - spawn_senders(&config, &quit_flag, pattern, parallel_walker, tx); + spawn_senders(&config, &quit_flag, patterns, parallel_walker, tx); // Wait for the receiver thread to print out all results. let exit_code = receiver_thread.join().unwrap(); @@ -383,13 +383,13 @@ fn spawn_receiver( fn spawn_senders( config: &Arc<Config>, quit_flag: &Arc<AtomicBool>, - pattern: Arc<Regex>, + patterns: Arc<Vec<Regex>>, parallel_walker: ignore::WalkParallel, tx: Sender<WorkerResult>, ) { parallel_walker.run(|| { let config = Arc::clone(config); - let pattern = Arc::clone(&pattern); + let patterns = Arc::clone(&patterns); let tx_thread = tx.clone(); let quit_flag = Arc::clone(quit_flag); @@ -459,7 +459,10 @@ fn spawn_senders( } }; - if !pattern.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())) { + if !patterns + .iter() + .all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref()))) + { return ignore::WalkState::Continue; } diff --git a/tests/tests.rs b/tests/tests.rs index 732b61e..e884c50 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -76,6 +76,203 @@ fn test_simple() { ); } +static AND_EXTRA_FILES: &[&str] = &[ + "a.foo", + "one/b.foo", + "one/two/c.foo", + "one/two/C.Foo2", + "one/two/three/baz-quux", + "one/two/three/Baz-Quux2", + "one/two/three/d.foo", + "fdignored.foo", + "gitignored.foo", + ".hidden.foo", + "A-B.jpg", + "A-C.png", + "B-A.png", + "B-C.png", + "C-A.jpg", + "C-B.png", + "e1 e2", +]; + +/// AND test +#[test] +fn test_and_basic() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["foo", "--and", "c"], + "one/two/C.Foo2 + one/two/c.foo + one/two/three/directory_foo/", + ); + + te.assert_output( + &["f", "--and", "[ad]", "--and", "[_]"], + "one/two/three/directory_foo/", + ); + + te.assert_output( + &["f", "--and", "[ad]", "--and", "[.]"], + "a.foo + one/two/three/d.foo", + ); + + te.assert_output(&["Foo", "--and", "C"], "one/two/C.Foo2"); + + te.assert_output(&["foo", "--and", "asdasdasdsadasd"], ""); +} + +#[test] +fn test_and_empty_pattern() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + te.assert_output(&["Foo", "--and", "2", "--and", ""], "one/two/C.Foo2"); +} + +#[test] +fn test_and_bad_pattern() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_failure(&["Foo", "--and", "2", "--and", "[", "--and", "C"]); + te.assert_failure(&["Foo", "--and", "[", "--and", "2", "--and", "C"]); + te.assert_failure(&["Foo", "--and", "2", "--and", "C", "--and", "["]); + te.assert_failure(&["[", "--and", "2", "--and", "C", "--and", "Foo"]); +} + +#[test] +fn test_and_pattern_starts_with_dash() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["baz", "--and", "quux"], + "one/two/three/Baz-Quux2 + one/two/three/baz-quux", + ); + te.assert_output( + &["baz", "--and", "-"], + "one/two/three/Baz-Quux2 + one/two/three/baz-quux", + ); + te.assert_output( + &["Quu", "--and", "x", "--and", "-"], + "one/two/three/Baz-Quux2", + ); +} + +#[test] +fn test_and_plus_extension() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &[ + "A", + "--and", + "B", + "--extension", + "jpg", + "--extension", + "png", + ], + "A-B.jpg + B-A.png", + ); + + te.assert_output( + &[ + "A", + "--extension", + "jpg", + "--and", + "B", + "--extension", + "png", + ], + "A-B.jpg + B-A.png", + ); +} + +#[test] +fn test_and_plus_type() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["c", "--type", "d", "--and", "foo"], + "one/two/three/directory_foo/", + ); + + te.assert_output( + &["c", "--type", "f", "--and", "foo"], + "one/two/C.Foo2 + one/two/c.foo", + ); +} + +#[test] +fn test_and_plus_glob() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output(&["*foo", "--glob", "--and", "c*"], "one/two/c.foo"); +} + +#[test] +fn test_and_plus_fixed_strings() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["foo", "--fixed-strings", "--and", "c", "--and", "."], + "one/two/c.foo + one/two/C.Foo2", + ); + + te.assert_output( + &["foo", "--fixed-strings", "--and", "[c]", "--and", "."], + "", + ); + + te.assert_output( + &["Foo", "--fixed-strings", "--and", "C", "--and", "."], + "one/two/C.Foo2", + ); +} + +#[test] +fn test_and_plus_ignore_case() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["Foo", "--ignore-case", "--and", "C", "--and", "[.]"], + "one/two/C.Foo2 + one/two/c.foo", + ); +} + +#[test] +fn test_and_plus_case_sensitive() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["foo", "--case-sensitive", "--and", "c", "--and", "[.]"], + "one/two/c.foo", + ); +} + +#[test] +fn test_and_plus_full_path() { + let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES); + + te.assert_output( + &["three", "--full-path", "--and", "foo", "--and", "dir"], + "one/two/three/directory_foo/", + ); + + te.assert_output( + &["three", "--full-path", "--and", "two", "--and", "dir"], + "one/two/three/directory_foo/", + ); +} + /// Test each pattern type with an empty pattern. #[test] fn test_empty_pattern() { |