diff --git a/complete/_rg b/complete/_rg index 6fecf6d89..974c6e503 100644 --- a/complete/_rg +++ b/complete/_rg @@ -26,7 +26,8 @@ _rg() { '--column[show column numbers]' '(-A -B -C --after-context --before-context --context)'{-C+,--context=}'[specify lines to show before and after each match]:number of lines' '--context-separator=[specify string used to separate non-continuous context lines in output]:separator' - '(-c --count --passthrough --passthru)'{-c,--count}'[only show count of matches for each file]' + '(-c --count --count-matches --passthrough --passthru)'{-c,--count}'[only show count of matching lines for each file]' + '(--count-matches -c --count --passthrough --passthru)--count-matches[only show count of individual matches for each file]' '--debug[show debug messages]' '--dfa-size-limit=[specify upper size limit of generated DFA]:DFA size' '(-E --encoding)'{-E+,--encoding=}'[specify text encoding of files to search]: :_rg_encodings' diff --git a/src/app.rs b/src/app.rs index ef18fbe18..cb3efe7b4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -516,6 +516,7 @@ pub fn all_args_and_flags() -> Vec { flag_context(&mut args); flag_context_separator(&mut args); flag_count(&mut args); + flag_count_matches(&mut args); flag_debug(&mut args); flag_dfa_size_limit(&mut args); flag_encoding(&mut args); @@ -758,7 +759,7 @@ sequences like \\x7F or \\t may be used. The default value is --. } fn flag_count(args: &mut Vec) { - const SHORT: &str = "Only show the count of matches for each file."; + const SHORT: &str = "Only show the count of matching lines for each file."; const LONG: &str = long!("\ This flag suppresses normal output and shows the number of lines that match the given patterns for each file searched. Each file containing a match has its @@ -768,9 +769,31 @@ that match and not the total number of matches. If only one file is given to ripgrep, then only the count is printed if there is a match. The --with-filename flag can be used to force printing the file path in this case. + +This overrides the --count-matches flag. "); let arg = RGArg::switch("count").short("c") - .help(SHORT).long_help(LONG); + .help(SHORT).long_help(LONG).overrides("count-matches"); + args.push(arg); +} + +fn flag_count_matches(args: &mut Vec) { + const SHORT: &str = "Only show the count of individual matches for each file."; + const LONG: &str = long!("\ +This flag suppresses normal output and shows the number of individual +matches of the given patterns for each file searched. Each file +containing matches has its path and match count printed on each line. +Note that this reports the total number of individual matches and not +the number of lines that match. + +If only one file is given to ripgrep, then only the count is printed if there +is a match. The --with-filename flag can be used to force printing the file +path in this case. + +This overrides the --count flag. +"); + let arg = RGArg::switch("count-matches") + .help(SHORT).long_help(LONG).overrides("count"); args.push(arg); } diff --git a/src/args.rs b/src/args.rs index 0461261bc..f3d2b2142 100644 --- a/src/args.rs +++ b/src/args.rs @@ -40,6 +40,7 @@ pub struct Args { column: bool, context_separator: Vec, count: bool, + count_matches: bool, encoding: Option<&'static Encoding>, files_with_matches: bool, files_without_matches: bool, @@ -199,6 +200,7 @@ impl Args { pub fn file_separator(&self) -> Option> { let contextless = self.count + || self.count_matches || self.files_with_matches || self.files_without_matches; let use_heading_sep = self.heading && !contextless; @@ -260,6 +262,7 @@ impl Args { .after_context(self.after_context) .before_context(self.before_context) .count(self.count) + .count_matches(self.count_matches) .encoding(self.encoding) .files_with_matches(self.files_with_matches) .files_without_matches(self.files_without_matches) @@ -356,6 +359,7 @@ impl<'a> ArgMatches<'a> { let mmap = self.mmap(&paths)?; let with_filename = self.with_filename(&paths); let (before_context, after_context) = self.contexts()?; + let (count, count_matches) = self.counts(); let quiet = self.is_present("quiet"); let args = Args { paths: paths, @@ -365,7 +369,8 @@ impl<'a> ArgMatches<'a> { colors: self.color_specs()?, column: self.column(), context_separator: self.context_separator(), - count: self.is_present("count"), + count: count, + count_matches: count_matches, encoding: self.encoding()?, files_with_matches: self.is_present("files-with-matches"), files_without_matches: self.is_present("files-without-match"), @@ -729,6 +734,22 @@ impl<'a> ArgMatches<'a> { }) } + /// Returns whether the -c/--count or the --count-matches flags were + /// passed from the command line. + /// + /// If --count-matches and --invert-match were passed in, behave + /// as if --count and --invert-match were passed in (i.e. rg will + /// count inverted matches as per existing behavior). + fn counts(&self) -> (bool, bool) { + let count = self.is_present("count"); + let count_matches = self.is_present("count-matches"); + let invert_matches = self.is_present("invert-match"); + if count_matches && invert_matches { + return (true, false); + } + (count, count_matches) + } + /// Returns the user's color choice based on command line parameters and /// environment. fn color_choice(&self) -> termcolor::ColorChoice { diff --git a/src/main.rs b/src/main.rs index b3b192c1a..bc0648160 100644 --- a/src/main.rs +++ b/src/main.rs @@ -88,13 +88,13 @@ fn run_parallel(args: &Arc) -> Result { let bufwtr = Arc::new(args.buffer_writer()); let quiet_matched = args.quiet_matched(); let paths_searched = Arc::new(AtomicUsize::new(0)); - let match_count = Arc::new(AtomicUsize::new(0)); + let match_line_count = Arc::new(AtomicUsize::new(0)); args.walker_parallel().run(|| { let args = Arc::clone(args); let quiet_matched = quiet_matched.clone(); let paths_searched = paths_searched.clone(); - let match_count = match_count.clone(); + let match_line_count = match_line_count.clone(); let bufwtr = Arc::clone(&bufwtr); let mut buf = bufwtr.buffer(); let mut worker = args.worker(); @@ -125,7 +125,7 @@ fn run_parallel(args: &Arc) -> Result { } else { worker.run(&mut printer, Work::DirEntry(dent)) }; - match_count.fetch_add(count as usize, Ordering::SeqCst); + match_line_count.fetch_add(count as usize, Ordering::SeqCst); if quiet_matched.set_match(count > 0) { return Quit; } @@ -141,7 +141,7 @@ fn run_parallel(args: &Arc) -> Result { eprint_nothing_searched(); } } - Ok(match_count.load(Ordering::SeqCst) as u64) + Ok(match_line_count.load(Ordering::SeqCst) as u64) } fn run_one_thread(args: &Arc) -> Result { @@ -149,7 +149,7 @@ fn run_one_thread(args: &Arc) -> Result { let mut stdout = stdout.lock(); let mut worker = args.worker(); let mut paths_searched: u64 = 0; - let mut match_count = 0; + let mut match_line_count = 0; for result in args.walker() { let dent = match get_or_log_dir_entry( result, @@ -161,7 +161,7 @@ fn run_one_thread(args: &Arc) -> Result { Some(dent) => dent, }; let mut printer = args.printer(&mut stdout); - if match_count > 0 { + if match_line_count > 0 { if args.quiet() { break; } @@ -170,7 +170,7 @@ fn run_one_thread(args: &Arc) -> Result { } } paths_searched += 1; - match_count += + match_line_count += if dent.is_stdin() { worker.run(&mut printer, Work::Stdin) } else { @@ -182,7 +182,7 @@ fn run_one_thread(args: &Arc) -> Result { eprint_nothing_searched(); } } - Ok(match_count) + Ok(match_line_count) } fn run_files_parallel(args: Arc) -> Result { diff --git a/src/search_buffer.rs b/src/search_buffer.rs index 11b561ea7..df114f0ad 100644 --- a/src/search_buffer.rs +++ b/src/search_buffer.rs @@ -21,7 +21,8 @@ pub struct BufferSearcher<'a, W: 'a> { grep: &'a Grep, path: &'a Path, buf: &'a [u8], - match_count: u64, + match_line_count: u64, + match_count: Option, line_count: Option, last_line: usize, } @@ -39,7 +40,8 @@ impl<'a, W: WriteColor> BufferSearcher<'a, W> { grep: grep, path: path, buf: buf, - match_count: 0, + match_line_count: 0, + match_count: None, line_count: None, last_line: 0, } @@ -53,6 +55,15 @@ impl<'a, W: WriteColor> BufferSearcher<'a, W> { self } + /// If enabled, searching will print the count of individual matches + /// instead of each match. + /// + /// Disabled by default. + pub fn count_matches(mut self, yes: bool) -> Self { + self.opts.count_matches = yes; + self + } + /// If enabled, searching will print the path instead of each match. /// /// Disabled by default. @@ -118,8 +129,9 @@ impl<'a, W: WriteColor> BufferSearcher<'a, W> { return 0; } - self.match_count = 0; + self.match_line_count = 0; self.line_count = if self.opts.line_number { Some(0) } else { None }; + self.match_count = if self.opts.count_matches { Some(0) } else { None }; let mut last_end = 0; for m in self.grep.iter(self.buf) { if self.opts.invert_match { @@ -128,29 +140,43 @@ impl<'a, W: WriteColor> BufferSearcher<'a, W> { self.print_match(m.start(), m.end()); } last_end = m.end(); - if self.opts.terminate(self.match_count) { + if self.opts.terminate(self.match_line_count) { break; } } - if self.opts.invert_match && !self.opts.terminate(self.match_count) { + if self.opts.invert_match && !self.opts.terminate(self.match_line_count) { let upto = self.buf.len(); self.print_inverted_matches(last_end, upto); } - if self.opts.count && self.match_count > 0 { - self.printer.path_count(self.path, self.match_count); + if self.opts.count && self.match_line_count > 0 { + self.printer.path_count(self.path, self.match_line_count); + } else if self.opts.count_matches + && self.match_count.map_or(false, |c| c > 0) + { + self.printer.path_count(self.path, self.match_count.unwrap()); } - if self.opts.files_with_matches && self.match_count > 0 { + if self.opts.files_with_matches && self.match_line_count > 0 { self.printer.path(self.path); } - if self.opts.files_without_matches && self.match_count == 0 { + if self.opts.files_without_matches && self.match_line_count == 0 { self.printer.path(self.path); } - self.match_count + self.match_line_count + } + + #[inline(always)] + fn count_individual_matches(&mut self, start: usize, end: usize) { + if let Some(ref mut count) = self.match_count { + for _ in self.grep.regex().find_iter(&self.buf[start..end]) { + *count += 1; + } + } } #[inline(always)] pub fn print_match(&mut self, start: usize, end: usize) { - self.match_count += 1; + self.match_line_count += 1; + self.count_individual_matches(start, end); if self.opts.skip_matches() { return; } @@ -166,7 +192,7 @@ impl<'a, W: WriteColor> BufferSearcher<'a, W> { debug_assert!(self.opts.invert_match); let mut it = IterLines::new(self.opts.eol, start); while let Some((s, e)) = it.next(&self.buf[..end]) { - if self.opts.terminate(self.match_count) { + if self.opts.terminate(self.match_line_count) { return; } self.print_match(s, e); @@ -279,6 +305,13 @@ and exhibited clearly, with a label attached.\ assert_eq!(out, "/baz.rs:2\n"); } + #[test] + fn count_matches() { + let (_, out) = search( + "the", SHERLOCK, |s| s.count_matches(true)); + assert_eq!(out, "/baz.rs:4\n"); + } + #[test] fn files_with_matches() { let (count, out) = search( diff --git a/src/search_stream.rs b/src/search_stream.rs index 3d8396cba..126957951 100644 --- a/src/search_stream.rs +++ b/src/search_stream.rs @@ -67,7 +67,8 @@ pub struct Searcher<'a, R, W: 'a> { grep: &'a Grep, path: &'a Path, haystack: R, - match_count: u64, + match_line_count: u64, + match_count: Option, line_count: Option, last_match: Match, last_printed: usize, @@ -81,6 +82,7 @@ pub struct Options { pub after_context: usize, pub before_context: usize, pub count: bool, + pub count_matches: bool, pub files_with_matches: bool, pub files_without_matches: bool, pub eol: u8, @@ -97,6 +99,7 @@ impl Default for Options { after_context: 0, before_context: 0, count: false, + count_matches: false, files_with_matches: false, files_without_matches: false, eol: b'\n', @@ -111,11 +114,11 @@ impl Default for Options { } impl Options { - /// Several options (--quiet, --count, --files-with-matches, + /// Several options (--quiet, --count, --count-matches, --files-with-matches, /// --files-without-match) imply that we shouldn't ever display matches. pub fn skip_matches(&self) -> bool { self.count || self.files_with_matches || self.files_without_matches - || self.quiet + || self.quiet || self.count_matches } /// Some options (--quiet, --files-with-matches, --files-without-match) @@ -124,12 +127,12 @@ impl Options { self.files_with_matches || self.files_without_matches || self.quiet } - /// Returns true if the search should terminate based on the match count. - pub fn terminate(&self, match_count: u64) -> bool { - if match_count > 0 && self.stop_after_first_match() { + /// Returns true if the search should terminate based on the match line count. + pub fn terminate(&self, match_line_count: u64) -> bool { + if match_line_count > 0 && self.stop_after_first_match() { return true; } - if self.max_count.map_or(false, |max| match_count >= max) { + if self.max_count.map_or(false, |max| match_line_count >= max) { return true; } false @@ -163,7 +166,8 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { grep: grep, path: path, haystack: haystack, - match_count: 0, + match_line_count: 0, + match_count: None, line_count: None, last_match: Match::default(), last_printed: 0, @@ -194,6 +198,15 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { self } + /// If enabled, searching will print the count of individual matches + /// instead of each match. + /// + /// Disabled by default. + pub fn count_matches(mut self, yes: bool) -> Self { + self.opts.count_matches = yes; + self + } + /// If enabled, searching will print the path instead of each match. /// /// Disabled by default. @@ -257,8 +270,9 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { #[inline(never)] pub fn run(mut self) -> Result { self.inp.reset(); - self.match_count = 0; + self.match_line_count = 0; self.line_count = if self.opts.line_number { Some(0) } else { None }; + self.match_count = if self.opts.count_matches { Some(0) } else { None }; self.last_match = Match::default(); self.after_context_remaining = 0; while !self.terminate() { @@ -308,21 +322,23 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { self.print_after_context(upto); } } - if self.match_count > 0 { + if self.match_line_count > 0 { if self.opts.count { - self.printer.path_count(self.path, self.match_count); + self.printer.path_count(self.path, self.match_line_count); + } else if self.opts.count_matches { + self.printer.path_count(self.path, self.match_count.unwrap()); } else if self.opts.files_with_matches { self.printer.path(self.path); } } else if self.opts.files_without_matches { self.printer.path(self.path); } - Ok(self.match_count) + Ok(self.match_line_count) } #[inline(always)] fn terminate(&self) -> bool { - self.opts.terminate(self.match_count) + self.opts.terminate(self.match_line_count) } #[inline(always)] @@ -410,7 +426,8 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { #[inline(always)] fn print_match(&mut self, start: usize, end: usize) { - self.match_count += 1; + self.match_line_count += 1; + self.count_individual_matches(start, end); if self.opts.skip_matches() { return; } @@ -447,6 +464,15 @@ impl<'a, R: io::Read, W: WriteColor> Searcher<'a, R, W> { } } + #[inline(always)] + fn count_individual_matches(&mut self, start: usize, end: usize) { + if let Some(ref mut count) = self.match_count { + for _ in self.grep.regex().find_iter(&self.inp.buf[start..end]) { + *count += 1; + } + } + } + #[inline(always)] fn count_lines(&mut self, upto: usize) { if let Some(ref mut line_count) = self.line_count { @@ -1006,6 +1032,13 @@ fn main() { assert_eq!(out, "/baz.rs:2\n"); } + #[test] + fn count_matches() { + let (_, out) = search_smallcap( + "the", SHERLOCK, |s| s.count_matches(true)); + assert_eq!(out, "/baz.rs:4\n"); + } + #[test] fn files_with_matches() { let (count, out) = search_smallcap( diff --git a/src/worker.rs b/src/worker.rs index eee7c67fd..af92536cc 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -34,6 +34,7 @@ struct Options { after_context: usize, before_context: usize, count: bool, + count_matches: bool, files_with_matches: bool, files_without_matches: bool, eol: u8, @@ -54,6 +55,7 @@ impl Default for Options { after_context: 0, before_context: 0, count: false, + count_matches: false, files_with_matches: false, files_without_matches: false, eol: b'\n', @@ -114,6 +116,15 @@ impl WorkerBuilder { self } + /// If enabled, searching will print the count of individual matches + /// instead of each match. + /// + /// Disabled by default. + pub fn count_matches(mut self, yes: bool) -> Self { + self.opts.count_matches = yes; + self + } + /// Set the encoding to use to read each file. /// /// If the encoding is `None` (the default), then the encoding is @@ -284,6 +295,7 @@ impl Worker { .after_context(self.opts.after_context) .before_context(self.opts.before_context) .count(self.opts.count) + .count_matches(self.opts.count_matches) .files_with_matches(self.opts.files_with_matches) .files_without_matches(self.opts.files_without_matches) .eol(self.opts.eol) @@ -323,6 +335,7 @@ impl Worker { let searcher = BufferSearcher::new(printer, &self.grep, path, buf); Ok(searcher .count(self.opts.count) + .count_matches(self.opts.count_matches) .files_with_matches(self.opts.files_with_matches) .files_without_matches(self.opts.files_without_matches) .eol(self.opts.eol) diff --git a/tests/tests.rs b/tests/tests.rs index 34bf08e44..6b44e0883 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -402,6 +402,20 @@ sherlock!(count, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, expected); }); +sherlock!(count_matches, "the", ".", |wd: WorkDir, mut cmd: Command| { + cmd.arg("--count-matches"); + let lines: String = wd.stdout(&mut cmd); + let expected = "sherlock:4\n"; + assert_eq!(lines, expected); +}); + +sherlock!(count_matches_inverted, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + cmd.arg("--count-matches").arg("--invert-match"); + let lines: String = wd.stdout(&mut cmd); + let expected = "sherlock:4\n"; + assert_eq!(lines, expected); +}); + sherlock!(files_with_matches, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { cmd.arg("--files-with-matches"); let lines: String = wd.stdout(&mut cmd);