summaryrefslogtreecommitdiffstats
path: root/grep-printer/src/standard.rs
diff options
context:
space:
mode:
Diffstat (limited to 'grep-printer/src/standard.rs')
-rw-r--r--grep-printer/src/standard.rs540
1 files changed, 443 insertions, 97 deletions
diff --git a/grep-printer/src/standard.rs b/grep-printer/src/standard.rs
index 068f96a4..f21b3675 100644
--- a/grep-printer/src/standard.rs
+++ b/grep-printer/src/standard.rs
@@ -17,10 +17,7 @@ use termcolor::{ColorSpec, NoColor, WriteColor};
use color::ColorSpecs;
use counter::CounterWriter;
use stats::Stats;
-use util::{
- PrinterPath, Replacer, Sunk,
- trim_ascii_prefix, trim_ascii_prefix_range,
-};
+use util::{PrinterPath, Replacer, Sunk, trim_ascii_prefix};
/// The configuration for the standard printer.
///
@@ -37,6 +34,7 @@ struct Config {
per_match: bool,
replacement: Arc<Option<Vec<u8>>>,
max_columns: Option<u64>,
+ max_column_preview: bool,
max_matches: Option<u64>,
column: bool,
byte_offset: bool,
@@ -60,6 +58,7 @@ impl Default for Config {
per_match: false,
replacement: Arc::new(None),
max_columns: None,
+ max_column_preview: false,
max_matches: None,
column: false,
byte_offset: false,
@@ -264,6 +263,21 @@ impl StandardBuilder {
self
}
+ /// When enabled, if a line is found to be over the configured maximum
+ /// column limit (measured in terms of bytes), then a preview of the long
+ /// line will be printed instead.
+ ///
+ /// The preview will correspond to the first `N` *grapheme clusters* of
+ /// the line, where `N` is the limit configured by `max_columns`.
+ ///
+ /// If no limit is set, then enabling this has no effect.
+ ///
+ /// This is disabled by default.
+ pub fn max_column_preview(&mut self, yes: bool) -> &mut StandardBuilder {
+ self.config.max_column_preview = yes;
+ self
+ }
+
/// Set the maximum amount of matching lines that are printed.
///
/// If multi line search is enabled and a match spans multiple lines, then
@@ -1023,43 +1037,11 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
)?;
count += 1;
if self.exceeds_max_columns(&bytes[line]) {
- self.write_exceeded_line()?;
- continue;
- }
- if self.has_line_terminator(&bytes[line]) {
- line = line.with_end(line.end() - 1);
- }
- if self.config().trim_ascii {
- line = self.trim_ascii_prefix_range(bytes, line);
- }
-
- while !line.is_empty() {
- if matches[midx].end() <= line.start() {
- if midx + 1 < matches.len() {
- midx += 1;
- continue;
- } else {
- self.end_color_match()?;
- self.write(&bytes[line])?;
- break;
- }
- }
- let m = matches[midx];
-
- if line.start() < m.start() {
- let upto = cmp::min(line.end(), m.start());
- self.end_color_match()?;
- self.write(&bytes[line.with_end(upto)])?;
- line = line.with_start(upto);
- } else {
- let upto = cmp::min(line.end(), m.end());
- self.start_color_match()?;
- self.write(&bytes[line.with_end(upto)])?;
- line = line.with_start(upto);
- }
+ self.write_exceeded_line(bytes, line, matches, &mut midx)?;
+ } else {
+ self.write_colored_matches(bytes, line, matches, &mut midx)?;
+ self.write_line_term()?;
}
- self.end_color_match()?;
- self.write_line_term()?;
}
Ok(())
}
@@ -1074,12 +1056,8 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
let mut stepper = LineStep::new(line_term, 0, bytes.len());
while let Some((start, end)) = stepper.next(bytes) {
let mut line = Match::new(start, end);
- if self.has_line_terminator(&bytes[line]) {
- line = line.with_end(line.end() - 1);
- }
- if self.config().trim_ascii {
- line = self.trim_ascii_prefix_range(bytes, line);
- }
+ self.trim_line_terminator(bytes, &mut line);
+ self.trim_ascii_prefix(bytes, &mut line);
while !line.is_empty() {
if matches[midx].end() <= line.start() {
if midx + 1 < matches.len() {
@@ -1102,14 +1080,19 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
Some(m.start() as u64 + 1),
)?;
- let buf = &bytes[line.with_end(upto)];
+ let this_line = line.with_end(upto);
line = line.with_start(upto);
- if self.exceeds_max_columns(&buf) {
- self.write_exceeded_line()?;
- continue;
+ if self.exceeds_max_columns(&bytes[this_line]) {
+ self.write_exceeded_line(
+ bytes,
+ this_line,
+ matches,
+ &mut midx,
+ )?;
+ } else {
+ self.write_spec(spec, &bytes[this_line])?;
+ self.write_line_term()?;
}
- self.write_spec(spec, buf)?;
- self.write_line_term()?;
}
}
count += 1;
@@ -1140,15 +1123,11 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
)?;
count += 1;
if self.exceeds_max_columns(&bytes[line]) {
- self.write_exceeded_line()?;
+ self.write_exceeded_line(bytes, line, &[m], &mut 0)?;
continue;
}
- if self.has_line_terminator(&bytes[line]) {
- line = line.with_end(line.end() - 1);
- }
- if self.config().trim_ascii {
- line = self.trim_ascii_prefix_range(bytes, line);
- }
+ self.trim_line_terminator(bytes, &mut line);
+ self.trim_ascii_prefix(bytes, &mut line);
while !line.is_empty() {
if m.end() <= line.start() {
@@ -1205,7 +1184,10 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
line: &[u8],
) -> io::Result<()> {
if self.exceeds_max_columns(line) {
- self.write_exceeded_line()?;
+ let range = Match::new(0, line.len());
+ self.write_exceeded_line(
+ line, range, self.sunk.matches(), &mut 0,
+ )?;
} else {
self.write_trim(line)?;
if !self.has_line_terminator(line) {
@@ -1218,50 +1200,114 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
fn write_colored_line(
&self,
matches: &[Match],
- line: &[u8],
+ bytes: &[u8],
) -> io::Result<()> {
// If we know we aren't going to emit color, then we can go faster.
let spec = self.config().colors.matched();
if !self.wtr().borrow().supports_color() || spec.is_none() {
- return self.write_line(line);
+ return self.write_line(bytes);
}
- if self.exceeds_max_columns(line) {
- return self.write_exceeded_line();
+
+ let line = Match::new(0, bytes.len());
+ if self.exceeds_max_columns(bytes) {
+ self.write_exceeded_line(bytes, line, matches, &mut 0)
+ } else {
+ self.write_colored_matches(bytes, line, matches, &mut 0)?;
+ self.write_line_term()?;
+ Ok(())
}
+ }
- let mut last_written =
- if !self.config().trim_ascii {
- 0
- } else {
- self.trim_ascii_prefix_range(
- line,
- Match::new(0, line.len()),
- ).start()
- };
- for mut m in matches.iter().map(|&m| m) {
- if last_written < m.start() {
+ /// Write the `line` portion of `bytes`, with appropriate coloring for
+ /// each `match`, starting at `match_index`.
+ ///
+ /// This accounts for trimming any whitespace prefix and will *never* print
+ /// a line terminator. If a match exceeds the range specified by `line`,
+ /// then only the part of the match within `line` (if any) is printed.
+ fn write_colored_matches(
+ &self,
+ bytes: &[u8],
+ mut line: Match,
+ matches: &[Match],
+ match_index: &mut usize,
+ ) -> io::Result<()> {
+ self.trim_line_terminator(bytes, &mut line);
+ self.trim_ascii_prefix(bytes, &mut line);
+ if matches.is_empty() {
+ self.write(&bytes[line])?;
+ return Ok(());
+ }
+ while !line.is_empty() {
+ if matches[*match_index].end() <= line.start() {
+ if *match_index + 1 < matches.len() {
+ *match_index += 1;
+ continue;
+ } else {
+ self.end_color_match()?;
+ self.write(&bytes[line])?;
+ break;
+ }
+ }
+
+ let m = matches[*match_index];
+ if line.start() < m.start() {
+ let upto = cmp::min(line.end(), m.start());
self.end_color_match()?;
- self.write(&line[last_written..m.start()])?;
- } else if last_written < m.end() {
- m = m.with_start(last_written);
+ self.write(&bytes[line.with_end(upto)])?;
+ line = line.with_start(upto);
} else {
- continue;
- }
- if !m.is_empty() {
+ let upto = cmp::min(line.end(), m.end());
self.start_color_match()?;
- self.write(&line[m])?;
+ self.write(&bytes[line.with_end(upto)])?;
+ line = line.with_start(upto);
}
- last_written = m.end();
}
self.end_color_match()?;
- self.write(&line[last_written..])?;
- if !self.has_line_terminator(line) {
- self.write_line_term()?;
- }
Ok(())
}
- fn write_exceeded_line(&self) -> io::Result<()> {
+ fn write_exceeded_line(
+ &self,
+ bytes: &[u8],
+ mut line: Match,
+ matches: &[Match],
+ match_index: &mut usize,
+ ) -> io::Result<()> {
+ if self.config().max_column_preview {
+ let original = line;
+ let end = BStr::new(&bytes[line])
+ .grapheme_indices()
+ .map(|(_, end, _)| end)
+ .take(self.config().max_columns.unwrap_or(0) as usize)
+ .last()
+ .unwrap_or(0) + line.start();
+ line = line.with_end(end);
+ self.write_colored_matches(bytes, line, matches, match_index)?;
+
+ if matches.is_empty() {
+ self.write(b" [... omitted end of long line]")?;
+ } else {
+ let remaining = matches
+ .iter()
+ .filter(|m| {
+ m.start() >= line.end() && m.start() < original.end()
+ })
+ .count();
+ let tense =
+ if remaining == 1 {
+ "match"
+ } else {
+ "matches"
+ };
+ write!(
+ self.wtr().borrow_mut(),
+ " [... {} more {}]",
+ remaining, tense,
+ )?;
+ }
+ self.write_line_term()?;
+ return Ok(());
+ }
if self.sunk.original_matches().is_empty() {
if self.is_context() {
self.write(b"[Omitted long context line]")?;
@@ -1444,13 +1490,26 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
if !self.config().trim_ascii {
return self.write(buf);
}
- self.write(self.trim_ascii_prefix(buf))
+ let mut range = Match::new(0, buf.len());
+ self.trim_ascii_prefix(buf, &mut range);
+ self.write(&buf[range])
}
fn write(&self, buf: &[u8]) -> io::Result<()> {
self.wtr().borrow_mut().write_all(buf)
}
+ fn trim_line_terminator(&self, buf: &[u8], line: &mut Match) {
+ let lineterm = self.searcher.line_terminator();
+ if lineterm.is_suffix(&buf[*line]) {
+ let mut end = line.end() - 1;
+ if lineterm.is_crlf() && buf[end - 1] == b'\r' {
+ end -= 1;
+ }
+ *line = line.with_end(end);
+ }
+ }
+
fn has_line_terminator(&self, buf: &[u8]) -> bool {
self.searcher.line_terminator().is_suffix(buf)
}
@@ -1506,14 +1565,12 @@ impl<'a, M: Matcher, W: WriteColor> StandardImpl<'a, M, W> {
///
/// This stops trimming a prefix as soon as it sees non-whitespace or a
/// line terminator.
- fn trim_ascii_prefix_range(&self, slice: &[u8], range: Match) -> Match {
- trim_ascii_prefix_range(self.searcher.line_terminator(), slice, range)
- }
-
- /// Trim prefix ASCII spaces from the given slice and return the
- /// corresponding sub-slice.
- fn trim_ascii_prefix<'s>(&self, slice: &'s [u8]) -> &'s [u8] {
- trim_ascii_prefix(self.searcher.line_terminator(), slice)
+ fn trim_ascii_prefix(&self, slice: &[u8], range: &mut Match) {
+ if !self.config().trim_ascii {
+ return;
+ }
+ let lineterm = self.searcher.line_terminator();
+ *range = trim_ascii_prefix(lineterm, slice, *range)
}
}
@@ -2281,6 +2338,31 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[test]
+ fn max_columns_preview() {
+ let matcher = RegexMatcher::new("exhibited|dusted").unwrap();
+ let mut printer = StandardBuilder::new()
+ .max_columns(Some(46))
+ .max_column_preview(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+but Doctor Watson has to have it taken out for [... omitted end of long line]
+and exhibited clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn max_columns_with_count() {
let matcher = RegexMatcher::new("cigar|ash|dusted").unwrap();
let mut printer = StandardBuilder::new()
@@ -2306,6 +2388,86 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[test]
+ fn max_columns_with_count_preview_no_match() {
+ let matcher = RegexMatcher::new("exhibited|has to have it").unwrap();
+ let mut printer = StandardBuilder::new()
+ .stats(true)
+ .max_columns(Some(46))
+ .max_column_preview(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+but Doctor Watson has to have it taken out for [... 0 more matches]
+and exhibited clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
+ fn max_columns_with_count_preview_one_match() {
+ let matcher = RegexMatcher::new("exhibited|dusted").unwrap();
+ let mut printer = StandardBuilder::new()
+ .stats(true)
+ .max_columns(Some(46))
+ .max_column_preview(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+but Doctor Watson has to have it taken out for [... 1 more match]
+and exhibited clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
+ fn max_columns_with_count_preview_two_matches() {
+ let matcher = RegexMatcher::new(
+ "exhibited|dusted|has to have it",
+ ).unwrap();
+ let mut printer = StandardBuilder::new()
+ .stats(true)
+ .max_columns(Some(46))
+ .max_column_preview(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+but Doctor Watson has to have it taken out for [... 1 more match]
+and exhibited clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn max_columns_multi_line() {
let matcher = RegexMatcher::new("(?s)ash.+dusted").unwrap();
let mut printer = StandardBuilder::new()
@@ -2331,6 +2493,36 @@ but Doctor Watson has to have it taken out for him and dusted,
}
#[test]
+ fn max_columns_multi_line_preview() {
+ let matcher = RegexMatcher::new(
+ "(?s)clew|cigar ash.+have it|exhibited",
+ ).unwrap();
+ let mut printer = StandardBuilder::new()
+ .stats(true)
+ .max_columns(Some(46))
+ .max_column_preview(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .multi_line(true)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+can extract a clew from a wisp of straw or a f [... 1 more match]
+but Doctor Watson has to have it taken out for [... 0 more matches]
+and exhibited clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn max_matches() {
let matcher = RegexMatcher::new("Sherlock").unwrap();
let mut printer = StandardBuilder::new()
@@ -2620,7 +2812,39 @@ Holmeses, success in the province of detective work must always
}
#[test]
+ fn only_matching_max_columns_preview() {
+ let matcher = RegexMatcher::new("Doctor Watsons|Sherlock").unwrap();
+ let mut printer = StandardBuilder::new()
+ .only_matching(true)
+ .max_columns(Some(10))
+ .max_column_preview(true)
+ .column(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(true)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+1:9:Doctor Wat [... 0 more matches]
+1:57:Sherlock
+3:49:Sherlock
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn only_matching_max_columns_multi_line1() {
+ // The `(?s:.{0})` trick fools the matcher into thinking that it
+ // can match across multiple lines without actually doing so. This is
+ // so we can test multi-line handling in the case of a match on only
+ // one line.
let matcher = RegexMatcher::new(
r"(?s:.{0})(Doctor Watsons|Sherlock)"
).unwrap();
@@ -2650,6 +2874,41 @@ Holmeses, success in the province of detective work must always
}
#[test]
+ fn only_matching_max_columns_preview_multi_line1() {
+ // The `(?s:.{0})` trick fools the matcher into thinking that it
+ // can match across multiple lines without actually doing so. This is
+ // so we can test multi-line handling in the case of a match on only
+ // one line.
+ let matcher = RegexMatcher::new(
+ r"(?s:.{0})(Doctor Watsons|Sherlock)"
+ ).unwrap();
+ let mut printer = StandardBuilder::new()
+ .only_matching(true)
+ .max_columns(Some(10))
+ .max_column_preview(true)
+ .column(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .multi_line(true)
+ .line_number(true)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+1:9:Doctor Wat [... 0 more matches]
+1:57:Sherlock
+3:49:Sherlock
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn only_matching_max_columns_multi_line2() {
let matcher = RegexMatcher::new(
r"(?s)Watson.+?(Holmeses|clearly)"
@@ -2681,6 +2940,38 @@ Holmeses, success in the province of detective work must always
}
#[test]
+ fn only_matching_max_columns_preview_multi_line2() {
+ let matcher = RegexMatcher::new(
+ r"(?s)Watson.+?(Holmeses|clearly)"
+ ).unwrap();
+ let mut printer = StandardBuilder::new()
+ .only_matching(true)
+ .max_columns(Some(50))
+ .max_column_preview(true)
+ .column(true)
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .multi_line(true)
+ .line_number(true)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+1:16:Watsons of this world, as opposed to the Sherlock
+2:16:Holmeses
+5:12:Watson has to have it taken out for him and dusted [... 0 more matches]
+6:12:and exhibited clearly
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn per_match() {
let matcher = RegexMatcher::new("Doctor Watsons|Sherlock").unwrap();
let mut printer = StandardBuilder::new()
@@ -2876,6 +3167,61 @@ Holmeses, success in the province of detective work must always
}
#[test]
+ fn replacement_max_columns_preview1() {
+ let matcher = RegexMatcher::new(r"Sherlock|Doctor (\w+)").unwrap();
+ let mut printer = StandardBuilder::new()
+ .max_columns(Some(67))
+ .max_column_preview(true)
+ .replacement(Some(b"doctah $1 MD".to_vec()))
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(true)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+1:For the doctah Watsons MD of this world, as opposed to the doctah [... 0 more matches]
+3:be, to a very large extent, the result of luck. doctah MD Holmes
+5:but doctah Watson MD has to have it taken out for him and dusted,
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
+ fn replacement_max_columns_preview2() {
+ let matcher = RegexMatcher::new(
+ "exhibited|dusted|has to have it",
+ ).unwrap();
+ let mut printer = StandardBuilder::new()
+ .max_columns(Some(43))
+ .max_column_preview(true)
+ .replacement(Some(b"xxx".to_vec()))
+ .build(NoColor::new(vec![]));
+ SearcherBuilder::new()
+ .line_number(false)
+ .build()
+ .search_reader(
+ &matcher,
+ SHERLOCK.as_bytes(),
+ printer.sink(&matcher),
+ )
+ .unwrap();
+
+ let got = printer_contents(&mut printer);
+ let expected = "\
+but Doctor Watson xxx taken out for him and [... 1 more match]
+and xxx clearly, with a label attached.
+";
+ assert_eq_printed!(expected, got);
+ }
+
+ #[test]
fn replacement_only_matching() {
let matcher = RegexMatcher::new(r"Sherlock|Doctor (\w+)").unwrap();
let mut printer = StandardBuilder::new()