Skip to content

Commit 0df5c60

Browse files
committed
Add a flag to anchor patterns in the input
Closes #1476.
1 parent ff3fc81 commit 0df5c60

File tree

3 files changed

+90
-9
lines changed

3 files changed

+90
-9
lines changed

src/cli.rs

+35-2
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,8 @@ pub struct Opts {
168168
pub regex: bool,
169169

170170
/// Treat the pattern as a literal string instead of a regular expression. Note
171-
/// that this also performs substring comparison. If you want to match on an
172-
/// exact filename, consider using '--glob'.
171+
/// that the pattern would still match on a substring of the input. If you want
172+
/// to match on an exact filename, consider adding '--anchor=input' as well.
173173
#[arg(
174174
long,
175175
short = 'F',
@@ -246,6 +246,20 @@ pub struct Opts {
246246
)]
247247
pub full_path: bool,
248248

249+
/// By default, the search pattern for --regex and --fixed-strings can match any part of the input.
250+
/// (See the --full-path option for what constitutes input)
251+
///
252+
/// This flag allows other ways to anchor the pattern.
253+
///
254+
/// Conflicts with the --glob flag: globs always match the entire input
255+
#[arg(
256+
long,
257+
help = "Where to anchor the pattern",
258+
conflicts_with("glob"),
259+
long_help
260+
)]
261+
pub anchor: Option<Anchor>,
262+
249263
/// Separate search results by the null character (instead of newlines).
250264
/// Useful for piping results to 'xargs'.
251265
#[arg(
@@ -680,6 +694,17 @@ impl Opts {
680694
self.rg_alias_hidden_ignore > 0
681695
}
682696

697+
pub fn anchor(&self) -> Option<Anchor> {
698+
if self.glob {
699+
// globset has no way to use an anchor.
700+
// Otherwise we'd guard like this:
701+
// && !self.no_anchor && self.anchor.is_none()
702+
Some(Anchor::Input)
703+
} else {
704+
self.anchor
705+
}
706+
}
707+
683708
pub fn max_depth(&self) -> Option<usize> {
684709
self.max_depth.or(self.exact_depth)
685710
}
@@ -725,6 +750,14 @@ fn default_num_threads() -> NonZeroUsize {
725750
.min(limit)
726751
}
727752

753+
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
754+
pub enum Anchor {
755+
InputStart,
756+
InputEnd,
757+
Input,
758+
Word,
759+
}
760+
728761
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
729762
pub enum FileType {
730763
#[value(alias = "f")]

src/main.rs

+28-7
Original file line numberDiff line numberDiff line change
@@ -162,15 +162,36 @@ fn ensure_search_pattern_is_not_a_path(opts: &Opts) -> Result<()> {
162162
}
163163
}
164164

165+
fn apply_anchors(re: String, anchors: Option<cli::Anchor>) -> String {
166+
use cli::Anchor;
167+
match anchors {
168+
None => re,
169+
Some(Anchor::InputStart) => "^".to_owned() + &re,
170+
Some(Anchor::InputEnd) => re + "$",
171+
Some(Anchor::Input) => "^".to_owned() + &re + "$",
172+
// https://docs.rs/regex/latest/regex/#empty-matches
173+
Some(Anchor::Word) => r"\<".to_owned() + &re + r"\>",
174+
}
175+
}
176+
165177
fn build_pattern_regex(pattern: &str, opts: &Opts) -> Result<String> {
166-
Ok(if opts.glob && !pattern.is_empty() {
167-
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
168-
glob.regex().to_owned()
169-
} else if opts.fixed_strings {
170-
// Treat pattern as literal string if '--fixed-strings' is used
171-
regex::escape(pattern)
178+
Ok(if opts.glob {
179+
if !pattern.is_empty() {
180+
let glob = GlobBuilder::new(pattern).literal_separator(true).build()?;
181+
glob.regex().to_owned()
182+
} else {
183+
"".to_owned()
184+
}
172185
} else {
173-
String::from(pattern)
186+
apply_anchors(
187+
if opts.fixed_strings {
188+
// Treat pattern as literal string if '--fixed-strings' is used
189+
regex::escape(pattern)
190+
} else {
191+
String::from(pattern)
192+
},
193+
opts.anchor(),
194+
)
174195
})
175196
}
176197

tests/tests.rs

+27
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,33 @@ fn test_full_path() {
604604
);
605605
}
606606

607+
/// Anchoring (--anchor)
608+
#[test]
609+
fn test_anchors() {
610+
let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES);
611+
612+
te.assert_output(&["--anchor=input", "foo"], "");
613+
te.assert_output(&["--anchor=input", "b.foo"], "one/b.foo");
614+
te.assert_output(&["--anchor=input-start", "foo"], "");
615+
te.assert_output(&["--anchor=input-start", "b."], "one/b.foo");
616+
te.assert_output(
617+
&["--anchor=input-end", "oo"],
618+
"a.foo
619+
one/b.foo
620+
one/two/c.foo
621+
one/two/three/d.foo
622+
one/two/three/directory_foo/",
623+
);
624+
te.assert_output(&["--anchor=word", "oo"], "");
625+
te.assert_output(
626+
&["--anchor=word", "foo"],
627+
"a.foo
628+
one/b.foo
629+
one/two/c.foo
630+
one/two/three/d.foo",
631+
);
632+
}
633+
607634
/// Hidden files (--hidden)
608635
#[test]
609636
fn test_hidden() {

0 commit comments

Comments
 (0)