Skip to content

Commit c9d3968

Browse files
authored
Merge pull request #1139 from Uthar/master
add support for matching multiple patterns
2 parents 8dda499 + 36e6022 commit c9d3968

5 files changed

Lines changed: 258 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Features
44

5+
- New `--and <pattern>` option to add additional patterns that must also be matched. See #315
6+
and #1139 (@Uthar)
57
- Added `--changed-after` as alias for `--changed-within`, to have a name consistent with `--changed-before`.
68

79

src/cli.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,18 @@ pub struct Opts {
144144
)]
145145
pub fixed_strings: bool,
146146

147+
/// Additional search patterns that need to be matched
148+
#[arg(
149+
long = "and",
150+
value_name = "pattern",
151+
long_help = "Add additional required search patterns, all of which must be matched. Multiple \
152+
additional patterns can be specified. The patterns are regular expressions, \
153+
unless '--glob' or '--fixed-strings' is used.",
154+
hide_short_help = true,
155+
allow_hyphen_values = true
156+
)]
157+
pub exprs: Option<Vec<String>>,
158+
147159
/// Show absolute instead of relative paths
148160
#[arg(
149161
long,

src/main.rs

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use atty::Stream;
2121
use clap::{CommandFactory, Parser};
2222
use globset::GlobBuilder;
2323
use lscolors::LsColors;
24-
use regex::bytes::{RegexBuilder, RegexSetBuilder};
24+
use regex::bytes::{Regex, RegexBuilder, RegexSetBuilder};
2525

2626
use crate::cli::{ColorWhen, Opts};
2727
use crate::config::Config;
@@ -81,12 +81,28 @@ fn run() -> Result<ExitCode> {
8181
}
8282

8383
ensure_search_pattern_is_not_a_path(&opts)?;
84-
let pattern_regex = build_pattern_regex(&opts)?;
84+
let pattern = &opts.pattern;
85+
let exprs = &opts.exprs;
86+
let empty = Vec::new();
87+
88+
let pattern_regexps = exprs
89+
.as_ref()
90+
.unwrap_or(&empty)
91+
.iter()
92+
.chain([pattern])
93+
.map(|pat| build_pattern_regex(pat, &opts))
94+
.collect::<Result<Vec<String>>>()?;
95+
96+
let config = construct_config(opts, &pattern_regexps)?;
8597

86-
let config = construct_config(opts, &pattern_regex)?;
87-
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regex)?;
88-
let re = build_regex(pattern_regex, &config)?;
89-
walk::scan(&search_paths, Arc::new(re), Arc::new(config))
98+
ensure_use_hidden_option_for_leading_dot_pattern(&config, &pattern_regexps)?;
99+
100+
let regexps = pattern_regexps
101+
.into_iter()
102+
.map(|pat| build_regex(pat, &config))
103+
.collect::<Result<Vec<Regex>>>()?;
104+
105+
walk::scan(&search_paths, Arc::new(regexps), Arc::new(config))
90106
}
91107

92108
#[cfg(feature = "completions")]
@@ -145,8 +161,7 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
145161
}
146162
}
147163

148-
fn build_pattern_regex(opts: &Opts) -> Result<String> {
149-
let pattern = &opts.pattern;
164+
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
150165
Ok(if opts.glob && !pattern.is_empty() {
151166
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
152167
glob.regex().to_owned()
@@ -172,11 +187,14 @@ fn check_path_separator_length(path_separator: Option<&str>) -> Result<()> {
172187
}
173188
}
174189

175-
fn construct_config(mut opts: Opts, pattern_regex: &str) -> Result<Config> {
190+
fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config> {
176191
// The search will be case-sensitive if the command line flag is set or
177-
// if the pattern has an uppercase character (smart case).
178-
let case_sensitive =
179-
!opts.ignore_case && (opts.case_sensitive || pattern_has_uppercase_char(pattern_regex));
192+
// if any of the patterns has an uppercase character (smart case).
193+
let case_sensitive = !opts.ignore_case
194+
&& (opts.case_sensitive
195+
|| pattern_regexps
196+
.iter()
197+
.any(|pat| pattern_has_uppercase_char(pat)));
180198

181199
let path_separator = opts
182200
.path_separator
@@ -415,14 +433,18 @@ fn extract_time_constraints(opts: &Opts) -> Result<Vec<TimeFilter>> {
415433

416434
fn ensure_use_hidden_option_for_leading_dot_pattern(
417435
config: &Config,
418-
pattern_regex: &str,
436+
pattern_regexps: &[String],
419437
) -> Result<()> {
420-
if cfg!(unix) && config.ignore_hidden && pattern_matches_strings_with_leading_dot(pattern_regex)
438+
if cfg!(unix)
439+
&& config.ignore_hidden
440+
&& pattern_regexps
441+
.iter()
442+
.any(|pat| pattern_matches_strings_with_leading_dot(pat))
421443
{
422444
Err(anyhow!(
423-
"The pattern seems to only match files with a leading dot, but hidden files are \
445+
"The pattern(s) seems to only match files with a leading dot, but hidden files are \
424446
filtered by default. Consider adding -H/--hidden to search hidden files as well \
425-
or adjust your search pattern."
447+
or adjust your search pattern(s)."
426448
))
427449
} else {
428450
Ok(())

src/walk.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ pub const MAX_BUFFER_LENGTH: usize = 1000;
4747
/// Default duration until output buffering switches to streaming.
4848
pub const DEFAULT_MAX_BUFFER_TIME: Duration = Duration::from_millis(100);
4949

50-
/// Recursively scan the given search path for files / pathnames matching the pattern.
50+
/// Recursively scan the given search path for files / pathnames matching the patterns.
5151
///
5252
/// If the `--exec` argument was supplied, this will create a thread pool for executing
5353
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
5454
/// path will simply be written to standard output.
55-
pub fn scan(paths: &[PathBuf], pattern: Arc<Regex>, config: Arc<Config>) -> Result<ExitCode> {
55+
pub fn scan(paths: &[PathBuf], patterns: Arc<Vec<Regex>>, config: Arc<Config>) -> Result<ExitCode> {
5656
let first_path = &paths[0];
5757

5858
// 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
153153
let receiver_thread = spawn_receiver(&config, &quit_flag, &interrupt_flag, rx);
154154

155155
// Spawn the sender threads.
156-
spawn_senders(&config, &quit_flag, pattern, parallel_walker, tx);
156+
spawn_senders(&config, &quit_flag, patterns, parallel_walker, tx);
157157

158158
// Wait for the receiver thread to print out all results.
159159
let exit_code = receiver_thread.join().unwrap();
@@ -383,13 +383,13 @@ fn spawn_receiver(
383383
fn spawn_senders(
384384
config: &Arc<Config>,
385385
quit_flag: &Arc<AtomicBool>,
386-
pattern: Arc<Regex>,
386+
patterns: Arc<Vec<Regex>>,
387387
parallel_walker: ignore::WalkParallel,
388388
tx: Sender<WorkerResult>,
389389
) {
390390
parallel_walker.run(|| {
391391
let config = Arc::clone(config);
392-
let pattern = Arc::clone(&pattern);
392+
let patterns = Arc::clone(&patterns);
393393
let tx_thread = tx.clone();
394394
let quit_flag = Arc::clone(quit_flag);
395395

@@ -459,7 +459,10 @@ fn spawn_senders(
459459
}
460460
};
461461

462-
if !pattern.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())) {
462+
if !patterns
463+
.iter()
464+
.all(|pat| pat.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())))
465+
{
463466
return ignore::WalkState::Continue;
464467
}
465468

tests/tests.rs

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,203 @@ fn test_simple() {
7676
);
7777
}
7878

79+
static AND_EXTRA_FILES: &[&str] = &[
80+
"a.foo",
81+
"one/b.foo",
82+
"one/two/c.foo",
83+
"one/two/C.Foo2",
84+
"one/two/three/baz-quux",
85+
"one/two/three/Baz-Quux2",
86+
"one/two/three/d.foo",
87+
"fdignored.foo",
88+
"gitignored.foo",
89+
".hidden.foo",
90+
"A-B.jpg",
91+
"A-C.png",
92+
"B-A.png",
93+
"B-C.png",
94+
"C-A.jpg",
95+
"C-B.png",
96+
"e1 e2",
97+
];
98+
99+
/// AND test
100+
#[test]
101+
fn test_and_basic() {
102+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
103+
104+
te.assert_output(
105+
&["foo", "--and", "c"],
106+
"one/two/C.Foo2
107+
one/two/c.foo
108+
one/two/three/directory_foo/",
109+
);
110+
111+
te.assert_output(
112+
&["f", "--and", "[ad]", "--and", "[_]"],
113+
"one/two/three/directory_foo/",
114+
);
115+
116+
te.assert_output(
117+
&["f", "--and", "[ad]", "--and", "[.]"],
118+
"a.foo
119+
one/two/three/d.foo",
120+
);
121+
122+
te.assert_output(&["Foo", "--and", "C"], "one/two/C.Foo2");
123+
124+
te.assert_output(&["foo", "--and", "asdasdasdsadasd"], "");
125+
}
126+
127+
#[test]
128+
fn test_and_empty_pattern() {
129+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
130+
te.assert_output(&["Foo", "--and", "2", "--and", ""], "one/two/C.Foo2");
131+
}
132+
133+
#[test]
134+
fn test_and_bad_pattern() {
135+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
136+
137+
te.assert_failure(&["Foo", "--and", "2", "--and", "[", "--and", "C"]);
138+
te.assert_failure(&["Foo", "--and", "[", "--and", "2", "--and", "C"]);
139+
te.assert_failure(&["Foo", "--and", "2", "--and", "C", "--and", "["]);
140+
te.assert_failure(&["[", "--and", "2", "--and", "C", "--and", "Foo"]);
141+
}
142+
143+
#[test]
144+
fn test_and_pattern_starts_with_dash() {
145+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
146+
147+
te.assert_output(
148+
&["baz", "--and", "quux"],
149+
"one/two/three/Baz-Quux2
150+
one/two/three/baz-quux",
151+
);
152+
te.assert_output(
153+
&["baz", "--and", "-"],
154+
"one/two/three/Baz-Quux2
155+
one/two/three/baz-quux",
156+
);
157+
te.assert_output(
158+
&["Quu", "--and", "x", "--and", "-"],
159+
"one/two/three/Baz-Quux2",
160+
);
161+
}
162+
163+
#[test]
164+
fn test_and_plus_extension() {
165+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
166+
167+
te.assert_output(
168+
&[
169+
"A",
170+
"--and",
171+
"B",
172+
"--extension",
173+
"jpg",
174+
"--extension",
175+
"png",
176+
],
177+
"A-B.jpg
178+
B-A.png",
179+
);
180+
181+
te.assert_output(
182+
&[
183+
"A",
184+
"--extension",
185+
"jpg",
186+
"--and",
187+
"B",
188+
"--extension",
189+
"png",
190+
],
191+
"A-B.jpg
192+
B-A.png",
193+
);
194+
}
195+
196+
#[test]
197+
fn test_and_plus_type() {
198+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
199+
200+
te.assert_output(
201+
&["c", "--type", "d", "--and", "foo"],
202+
"one/two/three/directory_foo/",
203+
);
204+
205+
te.assert_output(
206+
&["c", "--type", "f", "--and", "foo"],
207+
"one/two/C.Foo2
208+
one/two/c.foo",
209+
);
210+
}
211+
212+
#[test]
213+
fn test_and_plus_glob() {
214+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
215+
216+
te.assert_output(&["*foo", "--glob", "--and", "c*"], "one/two/c.foo");
217+
}
218+
219+
#[test]
220+
fn test_and_plus_fixed_strings() {
221+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
222+
223+
te.assert_output(
224+
&["foo", "--fixed-strings", "--and", "c", "--and", "."],
225+
"one/two/c.foo
226+
one/two/C.Foo2",
227+
);
228+
229+
te.assert_output(
230+
&["foo", "--fixed-strings", "--and", "[c]", "--and", "."],
231+
"",
232+
);
233+
234+
te.assert_output(
235+
&["Foo", "--fixed-strings", "--and", "C", "--and", "."],
236+
"one/two/C.Foo2",
237+
);
238+
}
239+
240+
#[test]
241+
fn test_and_plus_ignore_case() {
242+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
243+
244+
te.assert_output(
245+
&["Foo", "--ignore-case", "--and", "C", "--and", "[.]"],
246+
"one/two/C.Foo2
247+
one/two/c.foo",
248+
);
249+
}
250+
251+
#[test]
252+
fn test_and_plus_case_sensitive() {
253+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
254+
255+
te.assert_output(
256+
&["foo", "--case-sensitive", "--and", "c", "--and", "[.]"],
257+
"one/two/c.foo",
258+
);
259+
}
260+
261+
#[test]
262+
fn test_and_plus_full_path() {
263+
let te = TestEnv::new(DEFAULT_DIRS, AND_EXTRA_FILES);
264+
265+
te.assert_output(
266+
&["three", "--full-path", "--and", "foo", "--and", "dir"],
267+
"one/two/three/directory_foo/",
268+
);
269+
270+
te.assert_output(
271+
&["three", "--full-path", "--and", "two", "--and", "dir"],
272+
"one/two/three/directory_foo/",
273+
);
274+
}
275+
79276
/// Test each pattern type with an empty pattern.
80277
#[test]
81278
fn test_empty_pattern() {

0 commit comments

Comments
 (0)