Skip to content

Commit

Permalink
Add new -M/--max-columns option.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
RalfJung authored and BurntSushi committed Mar 13, 2017
1 parent 23aec58 commit eece007
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 16 deletions.
4 changes: 4 additions & 0 deletions doc/rg.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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.",
Expand Down
5 changes: 4 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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());
}
Expand Down Expand Up @@ -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")),
Expand Down
90 changes: 75 additions & 15 deletions src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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> {
Expand All @@ -65,6 +87,7 @@ impl<W: WriteColor> Printer<W> {
with_filename: false,
colors: ColorSpecs::default(),
path_separator: None,
max_columns: None,
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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>>(
Expand All @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit eece007

Please sign in to comment.