From bd3e7eedb1d8698a46e18afd2041e675f6597947 Mon Sep 17 00:00:00 2001 From: Daniel Luz Date: Sat, 19 Nov 2016 21:48:59 -0200 Subject: [PATCH] Add --files-without-matches flag. Performs the opposite of --files-with-matches: only shows paths of files that contain zero matches. Closes #138 --- doc/rg.1 | 5 +++++ doc/rg.1.md | 3 +++ src/app.rs | 3 +++ src/args.rs | 5 ++++- src/search_buffer.rs | 20 ++++++++++++++++++++ src/search_stream.rs | 33 +++++++++++++++++++++++++++------ src/worker.rs | 12 ++++++++++++ tests/tests.rs | 18 ++++++++++++++++++ 8 files changed, 92 insertions(+), 7 deletions(-) diff --git a/doc/rg.1 b/doc/rg.1 index bc7563174..2442a43f1 100644 --- a/doc/rg.1 +++ b/doc/rg.1 @@ -182,6 +182,11 @@ Only show path of each file with matches. .RS .RE .TP +.B \-\-files\-without\-matches +Only show path of each file with no matches. +.RS +.RE +.TP .B \-H, \-\-with\-filename Prefix each match with the file name that contains it. This is the default when more than one file is searched. diff --git a/doc/rg.1.md b/doc/rg.1.md index a3d37667b..3980e8737 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -119,6 +119,9 @@ Project home page: https://github.com/BurntSushi/ripgrep -l, --files-with-matches : Only show path of each file with matches. +--files-without-matches +: Only show path of each file with no matches. + -H, --with-filename : Prefix each match with the file name that contains it. This is the default when more than one file is searched. diff --git a/src/app.rs b/src/app.rs index 5edaf999b..a549bc598 100644 --- a/src/app.rs +++ b/src/app.rs @@ -124,6 +124,7 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .value_name("FILE").takes_value(true) .multiple(true).number_of_values(1)) .arg(flag("files-with-matches").short("l")) + .arg(flag("files-without-matches")) .arg(flag("with-filename").short("H")) .arg(flag("no-filename")) .arg(flag("heading")) @@ -304,6 +305,8 @@ lazy_static! { lines, and the newline is not counted as part of the pattern."); doc!(h, "files-with-matches", "Only show the path of each file with at least one match."); + doc!(h, "files-without-matches", + "Only show the path of each file that contains zero matches."); doc!(h, "with-filename", "Show file name for each match.", "Prefix each match with the file name that contains it. This is \ diff --git a/src/args.rs b/src/args.rs index a4955d5f6..58cdee617 100644 --- a/src/args.rs +++ b/src/args.rs @@ -44,6 +44,7 @@ pub struct Args { context_separator: Vec, count: bool, files_with_matches: bool, + files_without_matches: bool, eol: u8, files: bool, follow: bool, @@ -158,7 +159,7 @@ impl Args { /// Retrieve the configured file separator. pub fn file_separator(&self) -> Option> { - if self.heading && !self.count && !self.files_with_matches { + if self.heading && !self.count && !self.files_with_matches && !self.files_without_matches { Some(b"".to_vec()) } else if self.before_context > 0 || self.after_context > 0 { Some(self.context_separator.clone()) @@ -217,6 +218,7 @@ impl Args { .before_context(self.before_context) .count(self.count) .files_with_matches(self.files_with_matches) + .files_without_matches(self.files_without_matches) .eol(self.eol) .line_number(self.line_number) .invert_match(self.invert_match) @@ -314,6 +316,7 @@ impl<'a> ArgMatches<'a> { context_separator: self.context_separator(), count: self.is_present("count"), files_with_matches: self.is_present("files-with-matches"), + files_without_matches: self.is_present("files-without-matches"), eol: b'\n', files: self.is_present("files"), follow: self.is_present("follow"), diff --git a/src/search_buffer.rs b/src/search_buffer.rs index c7c3bca0f..16a161e1b 100644 --- a/src/search_buffer.rs +++ b/src/search_buffer.rs @@ -61,6 +61,15 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> { self } + /// If enabled, searching will print the path of files that *don't* match + /// the given pattern. + /// + /// Disabled by default. + pub fn files_without_matches(mut self, yes: bool) -> Self { + self.opts.files_without_matches = yes; + self + } + /// Set the end-of-line byte used by this searcher. pub fn eol(mut self, eol: u8) -> Self { self.opts.eol = eol; @@ -133,6 +142,9 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> { if self.opts.files_with_matches && self.match_count > 0 { self.printer.path(self.path); } + if self.opts.files_without_matches && self.match_count == 0 { + self.printer.path(self.path); + } self.match_count } @@ -277,6 +289,14 @@ and exhibited clearly, with a label attached.\ assert_eq!(out, "/baz.rs\n"); } + #[test] + fn files_without_matches() { + let (count, out) = search( + "zzzz", SHERLOCK, |s| s.files_without_matches(true)); + assert_eq!(0, count); + assert_eq!(out, "/baz.rs\n"); + } + #[test] fn max_count() { let (count, out) = search( diff --git a/src/search_stream.rs b/src/search_stream.rs index 6ef8c451f..b92fa806b 100644 --- a/src/search_stream.rs +++ b/src/search_stream.rs @@ -82,6 +82,7 @@ pub struct Options { pub before_context: usize, pub count: bool, pub files_with_matches: bool, + pub files_without_matches: bool, pub eol: u8, pub invert_match: bool, pub line_number: bool, @@ -97,6 +98,7 @@ impl Default for Options { before_context: 0, count: false, files_with_matches: false, + files_without_matches: false, eol: b'\n', invert_match: false, line_number: false, @@ -109,16 +111,17 @@ impl Default for Options { } impl Options { - /// Several options (--quiet, --count, --files-with-matches) imply that - /// we shouldn't ever display matches. + /// Several options (--quiet, --count, --files-with-matches, + /// --files-without-matches) imply that we shouldn't ever display matches. pub fn skip_matches(&self) -> bool { - self.count || self.files_with_matches || self.quiet + self.count || self.files_with_matches || self.files_without_matches + || self.quiet } - /// Some options (--quiet, --files-with-matches) imply that we can stop - /// searching after the first match. + /// Some options (--quiet, --files-with-matches, --files-without-matches) + /// imply that we can stop searching after the first match. pub fn stop_after_first_match(&self) -> bool { - self.files_with_matches || self.quiet + self.files_with_matches || self.files_without_matches || self.quiet } /// Returns true if the search should terminate based on the match count. @@ -199,6 +202,14 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> { self } + /// If enabled, searching will print the path of files without any matches. + /// + /// Disabled by default. + pub fn files_without_matches(mut self, yes: bool) -> Self { + self.opts.files_without_matches = yes; + self + } + /// Set the end-of-line byte used by this searcher. pub fn eol(mut self, eol: u8) -> Self { self.opts.eol = eol; @@ -296,6 +307,8 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> { } else if self.opts.files_with_matches { self.printer.path(self.path); } + } else if self.match_count == 0 && self.opts.files_without_matches { + self.printer.path(self.path); } Ok(self.match_count) } @@ -986,6 +999,14 @@ fn main() { assert_eq!(out, "/baz.rs\n"); } + #[test] + fn files_without_matches() { + let (count, out) = search_smallcap( + "zzzz", SHERLOCK, |s| s.files_without_matches(true)); + assert_eq!(0, count); + assert_eq!(out, "/baz.rs\n"); + } + #[test] fn max_count() { let (count, out) = search_smallcap( diff --git a/src/worker.rs b/src/worker.rs index 0ade140a5..23ed75496 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -31,6 +31,7 @@ struct Options { before_context: usize, count: bool, files_with_matches: bool, + files_without_matches: bool, eol: u8, invert_match: bool, line_number: bool, @@ -48,6 +49,7 @@ impl Default for Options { before_context: 0, count: false, files_with_matches: false, + files_without_matches: false, eol: b'\n', invert_match: false, line_number: false, @@ -112,6 +114,14 @@ impl WorkerBuilder { self } + /// If enabled, searching will print the path of files without any matches. + /// + /// Disabled by default. + pub fn files_without_matches(mut self, yes: bool) -> Self { + self.opts.files_without_matches = yes; + self + } + /// Set the end-of-line byte used by this searcher. pub fn eol(mut self, eol: u8) -> Self { self.opts.eol = eol; @@ -230,6 +240,7 @@ impl Worker { .before_context(self.opts.before_context) .count(self.opts.count) .files_with_matches(self.opts.files_with_matches) + .files_without_matches(self.opts.files_without_matches) .eol(self.opts.eol) .line_number(self.opts.line_number) .invert_match(self.opts.invert_match) @@ -260,6 +271,7 @@ impl Worker { Ok(searcher .count(self.opts.count) .files_with_matches(self.opts.files_with_matches) + .files_without_matches(self.opts.files_without_matches) .eol(self.opts.eol) .line_number(self.opts.line_number) .invert_match(self.opts.invert_match) diff --git a/tests/tests.rs b/tests/tests.rs index bdafb2988..66c2e51c1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -339,6 +339,14 @@ sherlock!(files_with_matches, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, expected); }); +sherlock!(files_without_matches, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "foo"); + cmd.arg("--files-without-matches"); + let lines: String = wd.stdout(&mut cmd); + let expected = "file.py\n"; + assert_eq!(lines, expected); +}); + sherlock!(after_context, |wd: WorkDir, mut cmd: Command| { cmd.arg("-A").arg("1"); let lines: String = wd.stdout(&mut cmd); @@ -1058,6 +1066,16 @@ sherlock!(feature_89_files_with_matches, "Sherlock", ".", assert_eq!(lines, "sherlock\x00"); }); +// See: https://github.com/BurntSushi/ripgrep/issues/89 +sherlock!(feature_89_files_without_matches, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "foo"); + cmd.arg("--null").arg("--files-without-matches"); + + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.py\x00"); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/89 sherlock!(feature_89_count, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {