From d352b792944af6dc5d818b1ed3119f0914dfce38 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Thu, 2 Feb 2017 15:29:50 +0100 Subject: [PATCH] Add new -M/--max-columns option. This permits setting the maximum line width with respect to the number of bytes in a line. Omitted lines (whether part of a match, replacement or context) are replaced with a message stating that the line was elided. Fixes #129 --- doc/rg.1.md | 4 +++ src/app.rs | 8 +++++ src/args.rs | 5 ++- src/printer.rs | 92 +++++++++++++++++++++++++++++++++++++++++--------- tests/tests.rs | 30 ++++++++++++++++ 5 files changed, 122 insertions(+), 17 deletions(-) diff --git a/doc/rg.1.md b/doc/rg.1.md index d8e498c0..8e8ca289 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -203,6 +203,10 @@ Project home page: https://github.com/BurntSushi/ripgrep -L, --follow : Follow symlinks. +-M, --max-columns *NUM* +: Don't print lines longer than this limit in bytes. Longer lines are omitted, + and only the number of matches in that line is printed. + -m, --max-count *NUM* : Limit the number of matching lines per file searched to NUM. diff --git a/src/app.rs b/src/app.rs index c285ab03..21cf2410 100644 --- a/src/app.rs +++ b/src/app.rs @@ -169,6 +169,9 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .short("j").value_name("ARG").takes_value(true) .validator(validate_number)) .arg(flag("vimgrep")) + .arg(flag("max-columns").short("M") + .value_name("NUM").takes_value(true) + .validator(validate_number)) .arg(flag("type-add") .value_name("TYPE").takes_value(true) .multiple(true).number_of_values(1)) @@ -473,6 +476,11 @@ lazy_static! { "Show results with every match on its own line, including \ line numbers and column numbers. With this option, a line with \ more than one match will be printed more than once."); + doc!(h, "max-columns", + "Don't print lines longer than this limit in bytes.", + "Don't print lines longer than this limit in bytes. Longer lines \ + are omitted, and only the number of matches in that line is \ + printed."); doc!(h, "type-add", "Add a new glob for a file type.", diff --git a/src/args.rs b/src/args.rs index cc48b7ad..148ae8b7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -56,6 +56,7 @@ pub struct Args { invert_match: bool, line_number: bool, line_per_match: bool, + max_columns: Option, max_count: Option, max_filesize: Option, maxdepth: Option, @@ -156,7 +157,8 @@ impl Args { .line_per_match(self.line_per_match) .null(self.null) .path_separator(self.path_separator) - .with_filename(self.with_filename); + .with_filename(self.with_filename) + .max_columns(self.max_columns); if let Some(ref rep) = self.replace { p = p.replace(rep.clone()); } @@ -348,6 +350,7 @@ impl<'a> ArgMatches<'a> { invert_match: self.is_present("invert-match"), line_number: line_number, line_per_match: self.is_present("vimgrep"), + max_columns: try!(self.usize_of("max-columns")), max_count: try!(self.usize_of("max-count")).map(|max| max as u64), max_filesize: try!(self.max_filesize()), maxdepth: try!(self.usize_of("maxdepth")), diff --git a/src/printer.rs b/src/printer.rs index 8c04dd1a..809e0d75 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -3,12 +3,32 @@ use std::fmt; use std::path::Path; use std::str::FromStr; -use regex::bytes::Regex; +use regex::bytes::{Regex, Replacer, Captures}; use termcolor::{Color, ColorSpec, ParseColorError, WriteColor}; use pathutil::strip_prefix; use ignore::types::FileTypeDef; +/// CountingReplacer implements the Replacer interface for Regex, +/// and counts how often replacement is being performed. +struct CountingReplacer<'r> { + replace: &'r [u8], + count: &'r mut usize, +} + +impl<'r> CountingReplacer<'r> { + fn new(replace: &'r [u8], count: &'r mut usize) -> CountingReplacer<'r> { + CountingReplacer { replace: replace, count: count } + } +} + +impl<'r> Replacer for CountingReplacer<'r> { + fn replace_append(&mut self, caps: &Captures, dst: &mut Vec) { + *self.count += 1; + caps.expand(self.replace, dst); + } +} + /// Printer encapsulates all output logic for searching. /// /// Note that we currently ignore all write errors. It's probably worthwhile @@ -46,6 +66,8 @@ pub struct Printer { colors: ColorSpecs, /// The separator to use for file paths. If empty, this is ignored. path_separator: Option, + /// Restrict lines to this many columns. + max_columns: Option } impl Printer { @@ -65,6 +87,7 @@ impl Printer { with_filename: false, colors: ColorSpecs::default(), path_separator: None, + max_columns: None, } } @@ -144,6 +167,12 @@ impl Printer { self } + /// Configure the max. number of columns used for printing matching lines. + pub fn max_columns(mut self, max_columns: Option) -> Printer { + self.max_columns = max_columns; + self + } + /// Returns true if and only if something has been printed. pub fn has_printed(&self) -> bool { self.has_printed @@ -263,31 +292,57 @@ impl Printer { self.write(b":"); } if self.replace.is_some() { - let line = re.replace_all( - &buf[start..end], &**self.replace.as_ref().unwrap()); + let mut count = 0; + let line = { + let replacer = CountingReplacer::new( + self.replace.as_ref().unwrap(), &mut count); + re.replace_all(&buf[start..end], replacer) + }; + if self.max_columns.map_or(false, |m| line.len() > m) { + let _ = self.wtr.set_color(self.colors.matched()); + let msg = format!( + "[Omitted long line with {} replacements]", count); + self.write(msg.as_bytes()); + let _ = self.wtr.reset(); + self.write_eol(); + return; + } self.write(&line); + if line.last() != Some(&self.eol) { + self.write_eol(); + } } else { self.write_matched_line(re, &buf[start..end]); - } - if buf[start..end].last() != Some(&self.eol) { - self.write_eol(); + // write_matched_line guarantees to write a newline. } } fn write_matched_line(&mut self, re: &Regex, buf: &[u8]) { - if !self.wtr.supports_color() || self.colors.matched().is_none() { - self.write(buf); + if self.max_columns.map_or(false, |m| buf.len() > m) { + let count = re.find_iter(buf).count(); + let _ = self.wtr.set_color(self.colors.matched()); + let msg = format!("[Omitted long line with {} matches]", count); + self.write(msg.as_bytes()); + let _ = self.wtr.reset(); + self.write_eol(); return; } - let mut last_written = 0; - for m in re.find_iter(buf) { - self.write(&buf[last_written..m.start()]); - let _ = self.wtr.set_color(self.colors.matched()); - self.write(&buf[m.start()..m.end()]); - let _ = self.wtr.reset(); - last_written = m.end(); + if !self.wtr.supports_color() || self.colors.matched().is_none() { + self.write(buf); + } else { + let mut last_written = 0; + for m in re.find_iter(buf) { + self.write(&buf[last_written..m.start()]); + let _ = self.wtr.set_color(self.colors.matched()); + self.write(&buf[m.start()..m.end()]); + let _ = self.wtr.reset(); + last_written = m.end(); + } + self.write(&buf[last_written..]); + } + if buf.last() != Some(&self.eol) { + self.write_eol(); } - self.write(&buf[last_written..]); } pub fn context>( @@ -312,6 +367,11 @@ impl Printer { if let Some(line_number) = line_number { self.line_number(line_number, b'-'); } + if self.max_columns.map_or(false, |m| end - start > m) { + self.write(format!("[Omitted long context line]").as_bytes()); + self.write_eol(); + return; + } self.write(&buf[start..end]); if buf[start..end].last() != Some(&self.eol) { self.write_eol(); diff --git a/tests/tests.rs b/tests/tests.rs index 9e216b52..9477f32e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1314,6 +1314,36 @@ clean!(feature_109_case_sensitive_part2, "test", ".", wd.assert_err(&mut cmd); }); +// See: https://github.com/BurntSushi/ripgrep/issues/129 +clean!(feature_129_matches, "test", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("foo", "test\ntest abcdefghijklmnopqrstuvwxyz test"); + cmd.arg("-M26"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "foo:test\nfoo:[Omitted long line with 2 matches]\n"; + assert_eq!(lines, expected); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/129 +clean!(feature_129_context, "test", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("foo", "test\nabcdefghijklmnopqrstuvwxyz"); + cmd.arg("-M20").arg("-C1"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "foo:test\nfoo-[Omitted long context line]\n"; + assert_eq!(lines, expected); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/129 +clean!(feature_129_replace, "test", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("foo", "test\ntest abcdefghijklmnopqrstuvwxyz test"); + cmd.arg("-M26").arg("-rfoo"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "foo:foo\nfoo:[Omitted long line with 2 replacements]\n"; + assert_eq!(lines, expected); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/159 clean!(feature_159_works, "test", ".", |wd: WorkDir, mut cmd: Command| { wd.create("foo", "test\ntest");