summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRalf Jung <post@ralfj.de>2017-02-02 15:29:50 +0100
committerAndrew Gallant <jamslam@gmail.com>2017-03-12 21:21:28 -0400
commitd352b792944af6dc5d818b1ed3119f0914dfce38 (patch)
tree8f199062da76cdee8c7e5558ea9f8795e2160200
parent23aec586696afb5d3cda1081d63f9641caad1045 (diff)
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
-rw-r--r--doc/rg.1.md4
-rw-r--r--src/app.rs8
-rw-r--r--src/args.rs5
-rw-r--r--src/printer.rs90
-rw-r--r--tests/tests.rs30
5 files changed, 121 insertions, 16 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<F>(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<usize>,
max_count: Option<u64>,
max_filesize: Option<u64>,
maxdepth: Option<usize>,
@@ -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<u8>) {
+ *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<W> {
colors: ColorSpecs,
/// The separator to use for file paths. If empty, this is ignored.
path_separator: Option<u8>,
+ /// Restrict lines to this many columns.
+ max_columns: Option<usize>
}
impl<W: WriteColor> Printer<W> {
@@ -65,6 +87,7 @@ impl<W: WriteColor> Printer<W> {
with_filename: false,
colors: ColorSpecs::default(),
path_separator: None,
+ max_columns: None,
}
}
@@ -144,6 +167,12 @@ impl<W: WriteColor> Printer<W> {
self
}
+ /// Configure the max. number of columns used for printing matching lines.
+ pub fn max_columns(mut self, max_columns: Option<usize>) -> Printer<W> {
+ 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<W: WriteColor> Printer<W> {
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.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;
+ }
if !self.wtr.supports_color() || self.colors.matched().is_none() {
self.write(buf);
- return;
+ } 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..]);
}
- 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 buf.last() != Some(&self.eol) {
+ self.write_eol();
}
- self.write(&buf[last_written..]);
}
pub fn context<P: AsRef<Path>>(
@@ -312,6 +367,11 @@ impl<W: WriteColor> Printer<W> {
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");