Skip to content

Commit

Permalink
Added the --and flag for matching multiple patterns
Browse files Browse the repository at this point in the history
  • Loading branch information
Uthar committed Nov 20, 2022
1 parent bc94fcc commit 5794d62
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 22 deletions.
11 changes: 11 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,17 @@ pub struct Opts {
)]
pub pattern: String,

/// 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 fulfilled. Multiple \
additional patterns can be specified. The patterns are regular expressions,
unless '--glob'or '--fixed-strings' is used.",
hide_short_help = true
)]
pub exprs: Option<Vec<String>>,

/// Set path separator when printing file paths
#[arg(
long,
Expand Down
54 changes: 38 additions & 16 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
.iter()
.map(|pat| build_regex(pat.to_string(), &config))
.collect::<Result<Vec<Regex>>>()?;

walk::scan(&search_paths, Arc::new(regexps), Arc::new(config))
}

#[cfg(feature = "completions")]
Expand Down Expand Up @@ -139,8 +155,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()
Expand All @@ -166,11 +181,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 have 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
Expand Down Expand Up @@ -409,14 +427,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(())
Expand Down
15 changes: 9 additions & 6 deletions src/walk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,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
Expand Down Expand Up @@ -150,7 +150,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();
Expand Down Expand Up @@ -380,13 +380,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);

Expand Down Expand Up @@ -456,7 +456,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;
}

Expand Down
197 changes: 197 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 5794d62

Please sign in to comment.