1
0
mirror of https://github.com/BurntSushi/ripgrep.git synced 2025-04-24 17:12:16 +02:00

More progress. With coloring!

This commit is contained in:
Andrew Gallant 2016-09-05 17:36:41 -04:00
parent d8d7560fd0
commit 7a149c20fe
10 changed files with 676 additions and 67 deletions

View File

@ -23,6 +23,8 @@ crossbeam = "0.2"
docopt = "0.6" docopt = "0.6"
env_logger = "0.3" env_logger = "0.3"
grep = { version = "0.1", path = "grep" } grep = { version = "0.1", path = "grep" }
lazy_static = "0.2"
libc = "0.2"
log = "0.3" log = "0.3"
memchr = "0.1" memchr = "0.1"
memmap = "0.2" memmap = "0.2"
@ -31,15 +33,19 @@ parking_lot = "0.3"
regex = { version = "0.1", path = "/home/andrew/rust/regex" } regex = { version = "0.1", path = "/home/andrew/rust/regex" }
regex-syntax = { version = "0.3.1", path = "/home/andrew/rust/regex/regex-syntax" } regex-syntax = { version = "0.3.1", path = "/home/andrew/rust/regex/regex-syntax" }
rustc-serialize = "0.3" rustc-serialize = "0.3"
term = { version = "0.4", path = "/home/andrew/clones/term" }
thread_local = "0.2" thread_local = "0.2"
walkdir = "0.1" walkdir = "0.1"
[target.'cfg(windows)'.dependencies]
kernel32-sys = "0.2"
winapi = "0.2"
[features] [features]
simd-accel = ["regex/simd-accel"] simd-accel = ["regex/simd-accel"]
[dev-dependencies] [dev-dependencies]
glob = "0.2" glob = "0.2"
lazy_static = "0.2"
[profile.release] [profile.release]
debug = true debug = true

View File

@ -16,6 +16,7 @@ use ignore::Ignore;
use out::Out; use out::Out;
use printer::Printer; use printer::Printer;
use search::{InputBuffer, Searcher}; use search::{InputBuffer, Searcher};
use sys;
use types::{FileTypeDef, Types, TypesBuilder}; use types::{FileTypeDef, Types, TypesBuilder};
use walk; use walk;
@ -37,6 +38,9 @@ xrep is like the silver searcher and grep, but faster than both.
Common options: Common options:
-a, --text Search binary files as if they were text. -a, --text Search binary files as if they were text.
-c, --count Only show count of line matches for each file. -c, --count Only show count of line matches for each file.
--color WHEN Whether to use coloring in match.
Valid values are never, always or auto.
[default: auto]
-g, --glob GLOB ... Include or exclude files for searching that -g, --glob GLOB ... Include or exclude files for searching that
match the given glob. This always overrides any match the given glob. This always overrides any
other ignore logic. Multiple glob flags may be other ignore logic. Multiple glob flags may be
@ -44,8 +48,13 @@ Common options:
Precede a glob with a '!' to exclude it. Precede a glob with a '!' to exclude it.
-h, --help Show this usage message. -h, --help Show this usage message.
-i, --ignore-case Case insensitive search. -i, --ignore-case Case insensitive search.
-n, --line-number Show line numbers (1-based). -n, --line-number Show line numbers (1-based). This is enabled
by default at a tty.
-N, --no-line-number Suppress line numbers.
-q, --quiet Do not print anything to stdout. -q, --quiet Do not print anything to stdout.
-r, --replace ARG Replace every match with the string given.
Capture group indices (e.g., $5) and names
(e.g., $foo) are supported.
-t, --type TYPE ... Only search files matching TYPE. Multiple type -t, --type TYPE ... Only search files matching TYPE. Multiple type
flags may be provided. Use the --type-list flag flags may be provided. Use the --type-list flag
to list all available types. to list all available types.
@ -80,6 +89,13 @@ Less common options:
Prefix each match with the file name that contains it. This is the Prefix each match with the file name that contains it. This is the
default when more than one file is searched. default when more than one file is searched.
--heading
Show the file name above clusters of matches from each file.
This is the default mode at a tty.
--no-heading
Don't show any file name heading.
--hidden --hidden
Search hidden directories and files. Search hidden directories and files.
@ -93,10 +109,16 @@ Less common options:
--no-ignore --no-ignore
Don't respect ignore files (.gitignore, .xrepignore, etc.) Don't respect ignore files (.gitignore, .xrepignore, etc.)
--no-ignore-parent
Don't respect ignore files in parent directories.
-p, --pretty
Alias for --color=always --heading -n.
-Q, --literal -Q, --literal
Treat the pattern as a literal string instead of a regular expression. Treat the pattern as a literal string instead of a regular expression.
--threads ARG -j, --threads ARG
The number of threads to use. Defaults to the number of logical CPUs The number of threads to use. Defaults to the number of logical CPUs
(capped at 6). [default: 0] (capped at 6). [default: 0]
@ -123,6 +145,7 @@ pub struct RawArgs {
arg_path: Vec<String>, arg_path: Vec<String>,
flag_after_context: usize, flag_after_context: usize,
flag_before_context: usize, flag_before_context: usize,
flag_color: String,
flag_context: usize, flag_context: usize,
flag_context_separator: String, flag_context_separator: String,
flag_count: bool, flag_count: bool,
@ -130,14 +153,20 @@ pub struct RawArgs {
flag_files: bool, flag_files: bool,
flag_follow: bool, flag_follow: bool,
flag_glob: Vec<String>, flag_glob: Vec<String>,
flag_heading: bool,
flag_hidden: bool, flag_hidden: bool,
flag_ignore_case: bool, flag_ignore_case: bool,
flag_invert_match: bool, flag_invert_match: bool,
flag_line_number: bool, flag_line_number: bool,
flag_line_terminator: String, flag_line_terminator: String,
flag_literal: bool, flag_literal: bool,
flag_no_heading: bool,
flag_no_ignore: bool, flag_no_ignore: bool,
flag_no_ignore_parent: bool,
flag_no_line_number: bool,
flag_pretty: bool,
flag_quiet: bool, flag_quiet: bool,
flag_replace: Option<String>,
flag_text: bool, flag_text: bool,
flag_threads: usize, flag_threads: usize,
flag_type: Vec<String>, flag_type: Vec<String>,
@ -156,18 +185,22 @@ pub struct Args {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
after_context: usize, after_context: usize,
before_context: usize, before_context: usize,
color: bool,
context_separator: Vec<u8>, context_separator: Vec<u8>,
count: bool, count: bool,
eol: u8, eol: u8,
files: bool, files: bool,
follow: bool, follow: bool,
glob_overrides: Option<Gitignore>, glob_overrides: Option<Gitignore>,
heading: bool,
hidden: bool, hidden: bool,
ignore_case: bool, ignore_case: bool,
invert_match: bool, invert_match: bool,
line_number: bool, line_number: bool,
no_ignore: bool, no_ignore: bool,
no_ignore_parent: bool,
quiet: bool, quiet: bool,
replace: Option<Vec<u8>>,
text: bool, text: bool,
threads: usize, threads: usize,
type_defs: Vec<FileTypeDef>, type_defs: Vec<FileTypeDef>,
@ -194,7 +227,11 @@ impl RawArgs {
}; };
let paths = let paths =
if self.arg_path.is_empty() { if self.arg_path.is_empty() {
if sys::stdin_is_atty() {
vec![Path::new("./").to_path_buf()] vec![Path::new("./").to_path_buf()]
} else {
vec![Path::new("-").to_path_buf()]
}
} else { } else {
self.arg_path.iter().map(|p| { self.arg_path.iter().map(|p| {
Path::new(p).to_path_buf() Path::new(p).to_path_buf()
@ -232,6 +269,12 @@ impl RawArgs {
} else { } else {
self.flag_threads self.flag_threads
}; };
let color =
if self.flag_color == "auto" {
sys::stdout_is_atty() || self.flag_pretty
} else {
self.flag_color == "always"
};
let mut with_filename = self.flag_with_filename; let mut with_filename = self.flag_with_filename;
if !with_filename { if !with_filename {
with_filename = paths.len() > 1 || paths[0].is_dir(); with_filename = paths.len() > 1 || paths[0].is_dir();
@ -240,30 +283,44 @@ impl RawArgs {
btypes.add_defaults(); btypes.add_defaults();
try!(self.add_types(&mut btypes)); try!(self.add_types(&mut btypes));
let types = try!(btypes.build()); let types = try!(btypes.build());
Ok(Args { let mut args = Args {
pattern: pattern, pattern: pattern,
paths: paths, paths: paths,
after_context: after_context, after_context: after_context,
before_context: before_context, before_context: before_context,
color: color,
context_separator: unescape(&self.flag_context_separator), context_separator: unescape(&self.flag_context_separator),
count: self.flag_count, count: self.flag_count,
eol: eol, eol: eol,
files: self.flag_files, files: self.flag_files,
follow: self.flag_follow, follow: self.flag_follow,
glob_overrides: glob_overrides, glob_overrides: glob_overrides,
heading: !self.flag_no_heading && self.flag_heading,
hidden: self.flag_hidden, hidden: self.flag_hidden,
ignore_case: self.flag_ignore_case, ignore_case: self.flag_ignore_case,
invert_match: self.flag_invert_match, invert_match: self.flag_invert_match,
line_number: self.flag_line_number, line_number: !self.flag_no_line_number && self.flag_line_number,
no_ignore: self.flag_no_ignore, no_ignore: self.flag_no_ignore,
no_ignore_parent: self.flag_no_ignore_parent,
quiet: self.flag_quiet, quiet: self.flag_quiet,
replace: self.flag_replace.clone().map(|s| s.into_bytes()),
text: self.flag_text, text: self.flag_text,
threads: threads, threads: threads,
type_defs: btypes.definitions(), type_defs: btypes.definitions(),
type_list: self.flag_type_list, type_list: self.flag_type_list,
types: types, types: types,
with_filename: with_filename, with_filename: with_filename,
}) };
// If stdout is a tty, then apply some special default options.
if sys::stdout_is_atty() || self.flag_pretty {
if !self.flag_no_line_number && !args.count {
args.line_number = true;
}
if !self.flag_no_heading {
args.heading = true;
}
}
Ok(args)
} }
fn add_types(&self, types: &mut TypesBuilder) -> Result<()> { fn add_types(&self, types: &mut TypesBuilder) -> Result<()> {
@ -338,19 +395,26 @@ impl Args {
/// Create a new printer of individual search results that writes to the /// Create a new printer of individual search results that writes to the
/// writer given. /// writer given.
pub fn printer<W: io::Write>(&self, wtr: W) -> Printer<W> { pub fn printer<W: Send + io::Write>(&self, wtr: W) -> Printer<W> {
Printer::new(wtr) let mut p = Printer::new(wtr, self.color)
.context_separator(self.context_separator.clone()) .context_separator(self.context_separator.clone())
.eol(self.eol) .eol(self.eol)
.heading(self.heading)
.quiet(self.quiet) .quiet(self.quiet)
.with_filename(self.with_filename) .with_filename(self.with_filename);
if let Some(ref rep) = self.replace {
p = p.replace(rep.clone());
}
p
} }
/// Create a new printer of search results for an entire file that writes /// Create a new printer of search results for an entire file that writes
/// to the writer given. /// to the writer given.
pub fn out<W: io::Write>(&self, wtr: W) -> Out<W> { pub fn out<W: io::Write>(&self, wtr: W) -> Out<W> {
let mut out = Out::new(wtr); let mut out = Out::new(wtr);
if self.before_context > 0 || self.after_context > 0 { if self.heading && !self.count {
out = out.file_separator(b"".to_vec());
} else if self.before_context > 0 || self.after_context > 0 {
out = out.file_separator(self.context_separator.clone()); out = out.file_separator(self.context_separator.clone());
} }
out out
@ -364,7 +428,7 @@ impl Args {
/// Create a new line based searcher whose configuration is taken from the /// Create a new line based searcher whose configuration is taken from the
/// command line. This searcher supports a dizzying array of features: /// command line. This searcher supports a dizzying array of features:
/// inverted matching, line counting, context control and more. /// inverted matching, line counting, context control and more.
pub fn searcher<'a, R: io::Read, W: io::Write>( pub fn searcher<'a, R: io::Read, W: Send + io::Write>(
&self, &self,
inp: &'a mut InputBuffer, inp: &'a mut InputBuffer,
printer: &'a mut Printer<W>, printer: &'a mut Printer<W>,
@ -399,16 +463,19 @@ impl Args {
} }
/// Create a new recursive directory iterator at the path given. /// Create a new recursive directory iterator at the path given.
pub fn walker(&self, path: &Path) -> walk::Iter { pub fn walker(&self, path: &Path) -> Result<walk::Iter> {
let wd = WalkDir::new(path).follow_links(self.follow); let wd = WalkDir::new(path).follow_links(self.follow);
let mut ig = Ignore::new(); let mut ig = Ignore::new();
ig.ignore_hidden(!self.hidden); ig.ignore_hidden(!self.hidden);
ig.no_ignore(self.no_ignore); ig.no_ignore(self.no_ignore);
ig.add_types(self.types.clone()); ig.add_types(self.types.clone());
if !self.no_ignore_parent {
try!(ig.push_parents(path));
}
if let Some(ref overrides) = self.glob_overrides { if let Some(ref overrides) = self.glob_overrides {
ig.add_override(overrides.clone()); ig.add_override(overrides.clone());
} }
walk::Iter::new(ig, wd) Ok(walk::Iter::new(ig, wd))
} }
} }

View File

@ -84,6 +84,8 @@ pub struct Gitignore {
set: glob::Set, set: glob::Set,
root: PathBuf, root: PathBuf,
patterns: Vec<Pattern>, patterns: Vec<Pattern>,
num_ignores: u64,
num_whitelist: u64,
} }
impl Gitignore { impl Gitignore {
@ -152,6 +154,16 @@ impl Gitignore {
} }
Match::None Match::None
} }
/// Returns the total number of ignore patterns.
pub fn num_ignores(&self) -> u64 {
self.num_ignores
}
/// Returns the total number of whitelisted patterns.
pub fn num_whitelist(&self) -> u64 {
self.num_whitelist
}
} }
/// The result of a glob match. /// The result of a glob match.
@ -238,10 +250,14 @@ impl GitignoreBuilder {
/// ///
/// Once a matcher is built, no new glob patterns can be added to it. /// Once a matcher is built, no new glob patterns can be added to it.
pub fn build(self) -> Result<Gitignore, Error> { pub fn build(self) -> Result<Gitignore, Error> {
let nignores = self.patterns.iter().filter(|p| !p.whitelist).count();
let nwhitelist = self.patterns.iter().filter(|p| p.whitelist).count();
Ok(Gitignore { Ok(Gitignore {
set: try!(self.builder.build()), set: try!(self.builder.build()),
root: self.root, root: self.root,
patterns: self.patterns, patterns: self.patterns,
num_ignores: nignores as u64,
num_whitelist: nwhitelist as u64,
}) })
} }

View File

@ -15,21 +15,39 @@ of `IgnoreDir`s for use during directory traversal.
use std::error::Error as StdError; use std::error::Error as StdError;
use std::fmt; use std::fmt;
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use gitignore::{self, Gitignore, GitignoreBuilder, Match}; use gitignore::{self, Gitignore, GitignoreBuilder, Match, Pattern};
use types::Types; use types::Types;
const IGNORE_NAMES: &'static [&'static str] = &[
".gitignore",
".agignore",
".xrepignore",
];
/// Represents an error that can occur when parsing a gitignore file. /// Represents an error that can occur when parsing a gitignore file.
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
Gitignore(gitignore::Error), Gitignore(gitignore::Error),
Io {
path: PathBuf,
err: io::Error,
},
}
impl Error {
fn from_io<P: AsRef<Path>>(path: P, err: io::Error) -> Error {
Error::Io { path: path.as_ref().to_path_buf(), err: err }
}
} }
impl StdError for Error { impl StdError for Error {
fn description(&self) -> &str { fn description(&self) -> &str {
match *self { match *self {
Error::Gitignore(ref err) => err.description(), Error::Gitignore(ref err) => err.description(),
Error::Io { ref err, .. } => err.description(),
} }
} }
} }
@ -38,6 +56,9 @@ impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self { match *self {
Error::Gitignore(ref err) => err.fmt(f), Error::Gitignore(ref err) => err.fmt(f),
Error::Io { ref path, ref err } => {
write!(f, "{}: {}", path.display(), err)
}
} }
} }
} }
@ -59,9 +80,9 @@ pub struct Ignore {
stack: Vec<Option<IgnoreDir>>, stack: Vec<Option<IgnoreDir>>,
/// A set of override globs that are always checked first. A match (whether /// A set of override globs that are always checked first. A match (whether
/// it's whitelist or blacklist) trumps anything in stack. /// it's whitelist or blacklist) trumps anything in stack.
overrides: Option<Gitignore>, overrides: Overrides,
/// A file type matcher. /// A file type matcher.
types: Option<Types>, types: Types,
ignore_hidden: bool, ignore_hidden: bool,
no_ignore: bool, no_ignore: bool,
} }
@ -71,8 +92,8 @@ impl Ignore {
pub fn new() -> Ignore { pub fn new() -> Ignore {
Ignore { Ignore {
stack: vec![], stack: vec![],
overrides: None, overrides: Overrides::new(None),
types: None, types: Types::empty(),
ignore_hidden: true, ignore_hidden: true,
no_ignore: false, no_ignore: false,
} }
@ -92,17 +113,51 @@ impl Ignore {
/// Add a set of globs that overrides all other match logic. /// Add a set of globs that overrides all other match logic.
pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore { pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore {
self.overrides = Some(gi); self.overrides = Overrides::new(Some(gi));
self self
} }
/// Add a file type matcher. The file type matcher has the lowest /// Add a file type matcher. The file type matcher has the lowest
/// precedence. /// precedence.
pub fn add_types(&mut self, types: Types) -> &mut Ignore { pub fn add_types(&mut self, types: Types) -> &mut Ignore {
self.types = Some(types); self.types = types;
self self
} }
/// Push parent directories of `path` on to the stack.
pub fn push_parents<P: AsRef<Path>>(
&mut self,
path: P,
) -> Result<(), Error> {
let path = try!(path.as_ref().canonicalize().map_err(|err| {
Error::from_io(path.as_ref(), err)
}));
let mut path = &*path;
let mut saw_git = path.join(".git").is_dir();
let mut ignore_names = IGNORE_NAMES.to_vec();
let mut ignore_dir_results = vec![];
while let Some(parent) = path.parent() {
if self.no_ignore {
ignore_dir_results.push(Ok(None));
} else {
if saw_git {
ignore_names.retain(|&name| name != ".gitignore");
} else {
saw_git = parent.join(".git").is_dir();
}
let ignore_dir_result =
IgnoreDir::with_ignore_names(parent, ignore_names.iter());
ignore_dir_results.push(ignore_dir_result);
}
path = parent;
}
for ignore_dir_result in ignore_dir_results.into_iter().rev() {
try!(self.push_ignore_dir(ignore_dir_result));
}
Ok(())
}
/// Add a directory to the stack. /// Add a directory to the stack.
/// ///
/// Note that even if this returns an error, the directory is added to the /// Note that even if this returns an error, the directory is added to the
@ -112,7 +167,17 @@ impl Ignore {
self.stack.push(None); self.stack.push(None);
return Ok(()); return Ok(());
} }
match IgnoreDir::new(path) { self.push_ignore_dir(IgnoreDir::new(path))
}
/// Pushes the result of building a directory matcher on to the stack.
///
/// If the result given contains an error, then it is returned.
pub fn push_ignore_dir(
&mut self,
result: Result<Option<IgnoreDir>, Error>,
) -> Result<(), Error> {
match result {
Ok(id) => { Ok(id) => {
self.stack.push(id); self.stack.push(id);
Ok(()) Ok(())
@ -135,12 +200,10 @@ impl Ignore {
/// Returns true if and only if the given file path should be ignored. /// Returns true if and only if the given file path should be ignored.
pub fn ignored<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> bool { pub fn ignored<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> bool {
let path = path.as_ref(); let path = path.as_ref();
if let Some(ref overrides) = self.overrides { let mat = self.overrides.matched(path, is_dir);
let mat = overrides.matched(path, is_dir).invert();
if let Some(is_ignored) = self.ignore_match(path, mat) { if let Some(is_ignored) = self.ignore_match(path, mat) {
return is_ignored; return is_ignored;
} }
}
if self.ignore_hidden && is_hidden(&path) { if self.ignore_hidden && is_hidden(&path) {
debug!("{} ignored because it is hidden", path.display()); debug!("{} ignored because it is hidden", path.display());
return true; return true;
@ -156,12 +219,10 @@ impl Ignore {
break; break;
} }
} }
if let Some(ref types) = self.types { let mat = self.types.matched(path, is_dir);
let mat = types.matched(path, is_dir);
if let Some(is_ignored) = self.ignore_match(path, mat) { if let Some(is_ignored) = self.ignore_match(path, mat) {
return is_ignored; return is_ignored;
} }
}
false false
} }
@ -210,6 +271,23 @@ impl IgnoreDir {
/// If no ignore glob patterns could be found in the directory then `None` /// If no ignore glob patterns could be found in the directory then `None`
/// is returned. /// is returned.
pub fn new<P: AsRef<Path>>(path: P) -> Result<Option<IgnoreDir>, Error> { pub fn new<P: AsRef<Path>>(path: P) -> Result<Option<IgnoreDir>, Error> {
IgnoreDir::with_ignore_names(path, IGNORE_NAMES.iter())
}
/// Create a new matcher for the given directory using only the ignore
/// patterns found in the file names given.
///
/// If no ignore glob patterns could be found in the directory then `None`
/// is returned.
///
/// Note that the order of the names given is meaningful. Names appearing
/// later in the list have precedence over names appearing earlier in the
/// list.
pub fn with_ignore_names<P: AsRef<Path>, S, I>(
path: P,
names: I,
) -> Result<Option<IgnoreDir>, Error>
where P: AsRef<Path>, S: AsRef<str>, I: Iterator<Item=S> {
let mut id = IgnoreDir { let mut id = IgnoreDir {
path: path.as_ref().to_path_buf(), path: path.as_ref().to_path_buf(),
gi: None, gi: None,
@ -217,9 +295,9 @@ impl IgnoreDir {
let mut ok = false; let mut ok = false;
let mut builder = GitignoreBuilder::new(&id.path); let mut builder = GitignoreBuilder::new(&id.path);
// The ordering here is important. Later globs have higher precedence. // The ordering here is important. Later globs have higher precedence.
ok = builder.add_path(id.path.join(".gitignore")).is_ok() || ok; for name in names {
ok = builder.add_path(id.path.join(".agignore")).is_ok() || ok; ok = builder.add_path(id.path.join(name.as_ref())).is_ok() || ok;
ok = builder.add_path(id.path.join(".xrepignore")).is_ok() || ok; }
if !ok { if !ok {
Ok(None) Ok(None)
} else { } else {
@ -246,6 +324,56 @@ impl IgnoreDir {
} }
} }
/// Manages a set of overrides provided explicitly by the end user.
struct Overrides {
gi: Option<Gitignore>,
unmatched_pat: Pattern,
}
impl Overrides {
/// Creates a new set of overrides from the gitignore matcher provided.
/// If no matcher is provided, then the resulting overrides have no effect.
fn new(gi: Option<Gitignore>) -> Overrides {
Overrides {
gi: gi,
unmatched_pat: Pattern {
from: Path::new("<argv>").to_path_buf(),
original: "<none>".to_string(),
pat: "<none>".to_string(),
whitelist: false,
only_dir: false,
},
}
}
/// Returns a match for the given path against this set of overrides.
///
/// If there are no overrides, then this always returns Match::None.
///
/// If there is at least one positive override, then this never returns
/// Match::None (and interpreting non-matches as ignored) unless is_dir
/// is true.
pub fn matched<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match {
// File types don't apply to directories.
if is_dir {
return Match::None;
}
let path = path.as_ref();
self.gi.as_ref()
.map(|gi| {
let path = &*path.to_string_lossy();
let mat = gi.matched_utf8(path, is_dir).invert();
if mat.is_none() && !is_dir {
if gi.num_ignores() > 0 {
return Match::Ignored(&self.unmatched_pat);
}
}
mat
})
.unwrap_or(Match::None)
}
}
fn is_hidden<P: AsRef<Path>>(path: P) -> bool { fn is_hidden<P: AsRef<Path>>(path: P) -> bool {
if let Some(name) = path.as_ref().file_name() { if let Some(name) = path.as_ref().file_name() {
name.to_str().map(|s| s.starts_with(".")).unwrap_or(false) name.to_str().map(|s| s.starts_with(".")).unwrap_or(false)

View File

@ -4,9 +4,11 @@ extern crate crossbeam;
extern crate docopt; extern crate docopt;
extern crate env_logger; extern crate env_logger;
extern crate grep; extern crate grep;
#[cfg(test)] #[cfg(windows)]
extern crate kernel32;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
extern crate libc;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate memchr; extern crate memchr;
@ -16,12 +18,15 @@ extern crate parking_lot;
extern crate regex; extern crate regex;
extern crate regex_syntax as syntax; extern crate regex_syntax as syntax;
extern crate rustc_serialize; extern crate rustc_serialize;
extern crate term;
extern crate thread_local; extern crate thread_local;
extern crate walkdir; extern crate walkdir;
#[cfg(windows)]
extern crate winapi;
use std::error::Error; use std::error::Error;
use std::fs::File; use std::fs::File;
use std::io::{self, Write}; use std::io;
use std::path::Path; use std::path::Path;
use std::process; use std::process;
use std::result; use std::result;
@ -58,6 +63,7 @@ mod ignore;
mod out; mod out;
mod printer; mod printer;
mod search; mod search;
mod sys;
mod types; mod types;
mod walk; mod walk;
@ -68,7 +74,7 @@ fn main() {
Ok(count) if count == 0 => process::exit(1), Ok(count) if count == 0 => process::exit(1),
Ok(count) => process::exit(0), Ok(count) => process::exit(0),
Err(err) => { Err(err) => {
let _ = writeln!(&mut io::stderr(), "{}", err); eprintln!("{}", err);
process::exit(1); process::exit(1);
} }
} }
@ -105,7 +111,7 @@ fn run(args: Args) -> Result<u64> {
if p == Path::new("-") { if p == Path::new("-") {
workq.push(Work::Stdin) workq.push(Work::Stdin)
} else { } else {
for ent in args.walker(p) { for ent in try!(args.walker(p)) {
workq.push(Work::File(ent)); workq.push(Work::File(ent));
} }
} }
@ -121,14 +127,14 @@ fn run(args: Args) -> Result<u64> {
} }
fn run_files(args: Args) -> Result<u64> { fn run_files(args: Args) -> Result<u64> {
let mut printer = Printer::new(io::BufWriter::new(io::stdout())); let mut printer = args.printer(io::BufWriter::new(io::stdout()));
let mut file_count = 0; let mut file_count = 0;
for p in args.paths() { for p in args.paths() {
if p == Path::new("-") { if p == Path::new("-") {
printer.path(&Path::new("<stdin>")); printer.path(&Path::new("<stdin>"));
file_count += 1; file_count += 1;
} else { } else {
for ent in args.walker(p) { for ent in try!(args.walker(p)) {
printer.path(ent.path()); printer.path(ent.path());
file_count += 1; file_count += 1;
} }
@ -138,7 +144,7 @@ fn run_files(args: Args) -> Result<u64> {
} }
fn run_types(args: Args) -> Result<u64> { fn run_types(args: Args) -> Result<u64> {
let mut printer = Printer::new(io::BufWriter::new(io::stdout())); let mut printer = args.printer(io::BufWriter::new(io::stdout()));
let mut ty_count = 0; let mut ty_count = 0;
for def in args.type_defs() { for def in args.type_defs() {
printer.type_def(def); printer.type_def(def);
@ -200,7 +206,7 @@ impl Worker {
self.match_count self.match_count
} }
fn do_work<W: io::Write>( fn do_work<W: Send + io::Write>(
&mut self, &mut self,
printer: &mut Printer<W>, printer: &mut Printer<W>,
work: WorkReady, work: WorkReady,
@ -229,7 +235,7 @@ impl Worker {
} }
} }
fn search<R: io::Read, W: io::Write>( fn search<R: io::Read, W: Send + io::Write>(
&mut self, &mut self,
printer: &mut Printer<W>, printer: &mut Printer<W>,
path: &Path, path: &Path,

View File

@ -9,7 +9,7 @@ use std::io::{self, Write};
pub struct Out<W: io::Write> { pub struct Out<W: io::Write> {
wtr: io::BufWriter<W>, wtr: io::BufWriter<W>,
printed: bool, printed: bool,
file_separator: Vec<u8>, file_separator: Option<Vec<u8>>,
} }
impl<W: io::Write> Out<W> { impl<W: io::Write> Out<W> {
@ -18,7 +18,7 @@ impl<W: io::Write> Out<W> {
Out { Out {
wtr: io::BufWriter::new(wtr), wtr: io::BufWriter::new(wtr),
printed: false, printed: false,
file_separator: vec![], file_separator: None,
} }
} }
@ -27,17 +27,19 @@ impl<W: io::Write> Out<W> {
/// ///
/// If sep is empty, then no file separator is printed. /// If sep is empty, then no file separator is printed.
pub fn file_separator(mut self, sep: Vec<u8>) -> Out<W> { pub fn file_separator(mut self, sep: Vec<u8>) -> Out<W> {
self.file_separator = sep; self.file_separator = Some(sep);
self self
} }
/// Write the search results of a single file to the underlying wtr and /// Write the search results of a single file to the underlying wtr and
/// flush wtr. /// flush wtr.
pub fn write(&mut self, buf: &[u8]) { pub fn write(&mut self, buf: &[u8]) {
if self.printed && !self.file_separator.is_empty() { if let Some(ref sep) = self.file_separator {
let _ = self.wtr.write_all(&self.file_separator); if self.printed {
let _ = self.wtr.write_all(sep);
let _ = self.wtr.write_all(b"\n"); let _ = self.wtr.write_all(b"\n");
} }
}
let _ = self.wtr.write_all(buf); let _ = self.wtr.write_all(buf);
let _ = self.wtr.flush(); let _ = self.wtr.flush();
self.printed = true; self.printed = true;

View File

@ -1,8 +1,16 @@
use std::io; use std::io::{self, Write};
use std::path::Path; use std::path::Path;
use std::sync::Arc;
use regex::bytes::Regex;
use term::{self, Terminal};
use term::color::*;
use term::terminfo::TermInfo;
use types::FileTypeDef; use types::FileTypeDef;
use self::Writer::*;
/// Printer encapsulates all output logic for searching. /// Printer encapsulates all output logic for searching.
/// ///
/// Note that we currently ignore all write errors. It's probably worthwhile /// Note that we currently ignore all write errors. It's probably worthwhile
@ -10,7 +18,7 @@ use types::FileTypeDef;
/// writes to memory, neither of which commonly fail. /// writes to memory, neither of which commonly fail.
pub struct Printer<W> { pub struct Printer<W> {
/// The underlying writer. /// The underlying writer.
wtr: W, wtr: Writer<W>,
/// Whether anything has been printed to wtr yet. /// Whether anything has been printed to wtr yet.
has_printed: bool, has_printed: bool,
/// The string to use to separate non-contiguous runs of context lines. /// The string to use to separate non-contiguous runs of context lines.
@ -19,21 +27,31 @@ pub struct Printer<W> {
/// printed via the match directly, but occasionally we need to insert them /// printed via the match directly, but occasionally we need to insert them
/// ourselves (for example, to print a context separator). /// ourselves (for example, to print a context separator).
eol: u8, eol: u8,
/// Whether to show file name as a heading or not.
///
/// N.B. If with_filename is false, then this setting has no effect.
heading: bool,
/// Whether to suppress all output. /// Whether to suppress all output.
quiet: bool, quiet: bool,
/// A string to use as a replacement of each match in a matching line.
replace: Option<Vec<u8>>,
/// Whether to prefix each match with the corresponding file name. /// Whether to prefix each match with the corresponding file name.
with_filename: bool, with_filename: bool,
} }
impl<W: io::Write> Printer<W> { impl<W: Send + io::Write> Printer<W> {
/// Create a new printer that writes to wtr. /// Create a new printer that writes to wtr.
pub fn new(wtr: W) -> Printer<W> { ///
/// `color` should be true if the printer should try to use coloring.
pub fn new(wtr: W, color: bool) -> Printer<W> {
Printer { Printer {
wtr: wtr, wtr: Writer::new(wtr, color),
has_printed: false, has_printed: false,
context_separator: "--".to_string().into_bytes(), context_separator: "--".to_string().into_bytes(),
eol: b'\n', eol: b'\n',
heading: false,
quiet: false, quiet: false,
replace: None,
with_filename: false, with_filename: false,
} }
} }
@ -50,12 +68,30 @@ impl<W: io::Write> Printer<W> {
self self
} }
/// Whether to show file name as a heading or not.
///
/// N.B. If with_filename is false, then this setting has no effect.
pub fn heading(mut self, yes: bool) -> Printer<W> {
self.heading = yes;
self
}
/// When set, all output is suppressed. /// When set, all output is suppressed.
pub fn quiet(mut self, yes: bool) -> Printer<W> { pub fn quiet(mut self, yes: bool) -> Printer<W> {
self.quiet = yes; self.quiet = yes;
self self
} }
/// Replace every match in each matching line with the replacement string
/// given.
///
/// The replacement string syntax is documented here:
/// https://doc.rust-lang.org/regex/regex/bytes/struct.Captures.html#method.expand
pub fn replace(mut self, replacement: Vec<u8>) -> Printer<W> {
self.replace = Some(replacement);
self
}
/// When set, each match is prefixed with the file name that it came from. /// When set, each match is prefixed with the file name that it came from.
pub fn with_filename(mut self, yes: bool) -> Printer<W> { pub fn with_filename(mut self, yes: bool) -> Printer<W> {
self.with_filename = yes; self.with_filename = yes;
@ -70,7 +106,7 @@ impl<W: io::Write> Printer<W> {
/// Flushes the underlying writer and returns it. /// Flushes the underlying writer and returns it.
pub fn into_inner(mut self) -> W { pub fn into_inner(mut self) -> W {
let _ = self.wtr.flush(); let _ = self.wtr.flush();
self.wtr self.wtr.into_inner()
} }
/// Prints a type definition. /// Prints a type definition.
@ -120,26 +156,51 @@ impl<W: io::Write> Printer<W> {
pub fn matched<P: AsRef<Path>>( pub fn matched<P: AsRef<Path>>(
&mut self, &mut self,
re: &Regex,
path: P, path: P,
buf: &[u8], buf: &[u8],
start: usize, start: usize,
end: usize, end: usize,
line_number: Option<u64>, line_number: Option<u64>,
) { ) {
if self.with_filename { if self.heading && self.with_filename && !self.has_printed {
self.write_heading(path.as_ref());
} else if !self.heading && self.with_filename {
self.write(path.as_ref().to_string_lossy().as_bytes()); self.write(path.as_ref().to_string_lossy().as_bytes());
self.write(b":"); self.write(b":");
} }
if let Some(line_number) = line_number { if let Some(line_number) = line_number {
self.write(line_number.to_string().as_bytes()); self.line_number(line_number, b':');
self.write(b":"); }
if self.replace.is_some() {
let line = re.replace_all(
&buf[start..end], &**self.replace.as_ref().unwrap());
self.write(&line);
} else {
self.write_match(re, &buf[start..end]);
} }
self.write(&buf[start..end]);
if buf[start..end].last() != Some(&self.eol) { if buf[start..end].last() != Some(&self.eol) {
self.write_eol(); self.write_eol();
} }
} }
pub fn write_match(&mut self, re: &Regex, buf: &[u8]) {
if !self.wtr.is_color() {
self.write(buf);
return;
}
let mut last_written = 0;
for (s, e) in re.find_iter(buf) {
self.write(&buf[last_written..s]);
let _ = self.wtr.fg(BRIGHT_RED);
let _ = self.wtr.attr(term::Attr::Bold);
self.write(&buf[s..e]);
let _ = self.wtr.reset();
last_written = e;
}
self.write(&buf[last_written..]);
}
pub fn context<P: AsRef<Path>>( pub fn context<P: AsRef<Path>>(
&mut self, &mut self,
path: P, path: P,
@ -148,13 +209,18 @@ impl<W: io::Write> Printer<W> {
end: usize, end: usize,
line_number: Option<u64>, line_number: Option<u64>,
) { ) {
if self.with_filename { if self.heading && self.with_filename && !self.has_printed {
self.write_heading(path.as_ref());
} else if !self.heading && self.with_filename {
self.write(path.as_ref().to_string_lossy().as_bytes());
self.write(b":");
}
if !self.heading && self.with_filename {
self.write(path.as_ref().to_string_lossy().as_bytes()); self.write(path.as_ref().to_string_lossy().as_bytes());
self.write(b"-"); self.write(b"-");
} }
if let Some(line_number) = line_number { if let Some(line_number) = line_number {
self.write(line_number.to_string().as_bytes()); self.line_number(line_number, b'-');
self.write(b"-");
} }
self.write(&buf[start..end]); self.write(&buf[start..end]);
if buf[start..end].last() != Some(&self.eol) { if buf[start..end].last() != Some(&self.eol) {
@ -162,6 +228,28 @@ impl<W: io::Write> Printer<W> {
} }
} }
fn write_heading<P: AsRef<Path>>(&mut self, path: P) {
if self.wtr.is_color() {
let _ = self.wtr.fg(GREEN);
}
self.write(path.as_ref().to_string_lossy().as_bytes());
self.write_eol();
if self.wtr.is_color() {
let _ = self.wtr.reset();
}
}
fn line_number(&mut self, n: u64, sep: u8) {
if self.wtr.is_color() {
let _ = self.wtr.fg(YELLOW);
}
self.write(n.to_string().as_bytes());
if self.wtr.is_color() {
let _ = self.wtr.reset();
}
self.write(&[sep]);
}
fn write(&mut self, buf: &[u8]) { fn write(&mut self, buf: &[u8]) {
if self.quiet { if self.quiet {
return; return;
@ -175,3 +263,154 @@ impl<W: io::Write> Printer<W> {
self.write(&[eol]); self.write(&[eol]);
} }
} }
enum Writer<W> {
Colored(term::TerminfoTerminal<W>),
NoColor(W),
}
lazy_static! {
static ref TERMINFO: Option<Arc<TermInfo>> = {
match term::terminfo::TermInfo::from_env() {
Ok(info) => Some(Arc::new(info)),
Err(err) => {
debug!("error loading terminfo for coloring: {}", err);
None
}
}
};
}
impl<W: Send + io::Write> Writer<W> {
fn new(wtr: W, color: bool) -> Writer<W> {
// If we want color, build a TerminfoTerminal and see if the current
// environment supports coloring. If not, bail with NoColor. To avoid
// losing our writer (ownership), do this the long way.
if !color || TERMINFO.is_none() {
return NoColor(wtr);
}
// Why doesn't TERMINFO.as_ref().unwrap().clone() work?
let info = TERMINFO.clone().unwrap();
// names: TERMINFO.as_ref().unwrap().names.clone(),
// bools: TERMINFO.as_ref().unwrap().bools.clone(),
// numbers: TERMINFO.as_ref().unwrap().numbers.clone(),
// strings: TERMINFO.as_ref().unwrap().strings.clone(),
// };
let tt = term::TerminfoTerminal::new_with_terminfo(wtr, info);
if !tt.supports_color() {
debug!("environment doesn't support coloring");
return NoColor(tt.into_inner());
}
Colored(tt)
}
fn is_color(&self) -> bool {
match *self {
Colored(_) => true,
NoColor(_) => false,
}
}
fn map_result<F>(
&mut self,
mut f: F,
) -> term::Result<()>
where F: FnMut(&mut term::TerminfoTerminal<W>) -> term::Result<()> {
match *self {
Colored(ref mut w) => f(w),
NoColor(_) => Err(term::Error::NotSupported),
}
}
fn map_bool<F>(
&self,
mut f: F,
) -> bool
where F: FnMut(&term::TerminfoTerminal<W>) -> bool {
match *self {
Colored(ref w) => f(w),
NoColor(_) => false,
}
}
}
impl<W: Send + io::Write> io::Write for Writer<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
Colored(ref mut w) => w.write(buf),
NoColor(ref mut w) => w.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
Colored(ref mut w) => w.flush(),
NoColor(ref mut w) => w.flush(),
}
}
}
impl<W: Send + io::Write> term::Terminal for Writer<W> {
type Output = W;
fn fg(&mut self, fg: term::color::Color) -> term::Result<()> {
self.map_result(|w| w.fg(fg))
}
fn bg(&mut self, bg: term::color::Color) -> term::Result<()> {
self.map_result(|w| w.bg(bg))
}
fn attr(&mut self, attr: term::Attr) -> term::Result<()> {
self.map_result(|w| w.attr(attr))
}
fn supports_attr(&self, attr: term::Attr) -> bool {
self.map_bool(|w| w.supports_attr(attr))
}
fn reset(&mut self) -> term::Result<()> {
self.map_result(|w| w.reset())
}
fn supports_reset(&self) -> bool {
self.map_bool(|w| w.supports_reset())
}
fn supports_color(&self) -> bool {
self.map_bool(|w| w.supports_color())
}
fn cursor_up(&mut self) -> term::Result<()> {
self.map_result(|w| w.cursor_up())
}
fn delete_line(&mut self) -> term::Result<()> {
self.map_result(|w| w.delete_line())
}
fn carriage_return(&mut self) -> term::Result<()> {
self.map_result(|w| w.carriage_return())
}
fn get_ref(&self) -> &W {
match *self {
Colored(ref w) => w.get_ref(),
NoColor(ref w) => w,
}
}
fn get_mut(&mut self) -> &mut W {
match *self {
Colored(ref mut w) => w.get_mut(),
NoColor(ref mut w) => w,
}
}
fn into_inner(self) -> W {
match self {
Colored(w) => w.into_inner(),
NoColor(w) => w,
}
}
}

View File

@ -98,7 +98,7 @@ impl Default for Options {
} }
} }
impl<'a, R: io::Read, W: io::Write> Searcher<'a, R, W> { impl<'a, R: io::Read, W: Send + io::Write> Searcher<'a, R, W> {
/// Create a new searcher. /// Create a new searcher.
/// ///
/// `inp` is a reusable input buffer that is used as scratch space by this /// `inp` is a reusable input buffer that is used as scratch space by this
@ -329,7 +329,8 @@ impl<'a, R: io::Read, W: io::Write> Searcher<'a, R, W> {
self.count_lines(start); self.count_lines(start);
self.add_line(end); self.add_line(end);
self.printer.matched( self.printer.matched(
&self.path, &self.inp.buf, start, end, self.line_count); &self.grep.regex(), &self.path,
&self.inp.buf, start, end, self.line_count);
self.last_printed = end; self.last_printed = end;
self.after_context_remaining = self.opts.after_context; self.after_context_remaining = self.opts.after_context;
} }
@ -739,7 +740,7 @@ fn main() {
mut map: F, mut map: F,
) -> (u64, String) { ) -> (u64, String) {
let mut inp = InputBuffer::with_capacity(1); let mut inp = InputBuffer::with_capacity(1);
let mut pp = Printer::new(vec![]).with_filename(true); let mut pp = Printer::new(vec![], false).with_filename(true);
let grep = GrepBuilder::new(pat).build().unwrap(); let grep = GrepBuilder::new(pat).build().unwrap();
let count = { let count = {
let searcher = Searcher::new( let searcher = Searcher::new(
@ -755,7 +756,7 @@ fn main() {
mut map: F, mut map: F,
) -> (u64, String) { ) -> (u64, String) {
let mut inp = InputBuffer::with_capacity(4096); let mut inp = InputBuffer::with_capacity(4096);
let mut pp = Printer::new(vec![]).with_filename(true); let mut pp = Printer::new(vec![], false).with_filename(true);
let grep = GrepBuilder::new(pat).build().unwrap(); let grep = GrepBuilder::new(pat).build().unwrap();
let count = { let count = {
let searcher = Searcher::new( let searcher = Searcher::new(

139
src/sys.rs Normal file
View File

@ -0,0 +1,139 @@
/*!
This io module contains various platform specific functions for detecting
how xrep is being used. e.g., Is stdin being piped into it? Is stdout being
redirected to a file? etc... We use this information to tweak various default
configuration parameters such as colors and match formatting.
*/
use std::fs::{File, Metadata};
use std::io;
use libc;
#[cfg(unix)]
pub fn stdin_is_atty() -> bool {
0 < unsafe { libc::isatty(libc::STDIN_FILENO) }
}
#[cfg(unix)]
pub fn stdout_is_atty() -> bool {
0 < unsafe { libc::isatty(libc::STDOUT_FILENO) }
}
#[cfg(windows)]
pub fn stdin_is_atty() -> bool {
use kernel32;
use winapi;
unsafe {
let fd = winapi::winbase::STD_INPUT_HANDLE;
let mut out = 0;
kernel32::GetConsoleMode(kernel32::GetStdHandle(fd), &mut out) != 0
}
}
#[cfg(windows)]
pub fn stdout_is_atty() -> bool {
use kernel32;
use winapi;
unsafe {
let fd = winapi::winbase::STD_OUTPUT_HANDLE;
let mut out = 0;
kernel32::GetConsoleMode(handle, &mut out) != 0
}
}
// Probably everything below isn't actually needed. ---AG
#[cfg(unix)]
pub fn metadata(fd: libc::c_int) -> Result<Metadata, io::Error> {
use std::os::unix::io::{FromRawFd, IntoRawFd};
let f = unsafe { File::from_raw_fd(fd) };
let md = f.metadata();
// Be careful to transfer ownership back to a simple descriptor. Dropping
// the File itself would close the descriptor, which would be quite bad!
drop(f.into_raw_fd());
md
}
#[cfg(unix)]
pub fn stdin_is_file() -> bool {
metadata(libc::STDIN_FILENO)
.map(|md| md.file_type().is_file())
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stdout_is_file() -> bool {
metadata(libc::STDOUT_FILENO)
.map(|md| md.file_type().is_file())
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stdin_is_char_device() -> bool {
use std::os::unix::fs::FileTypeExt;
metadata(libc::STDIN_FILENO)
.map(|md| md.file_type().is_char_device())
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stdout_is_char_device() -> bool {
use std::os::unix::fs::FileTypeExt;
metadata(libc::STDOUT_FILENO)
.map(|md| md.file_type().is_char_device())
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stdin_is_fifo() -> bool {
use std::os::unix::fs::FileTypeExt;
metadata(libc::STDIN_FILENO)
.map(|md| md.file_type().is_fifo())
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stdout_is_fifo() -> bool {
use std::os::unix::fs::FileTypeExt;
metadata(libc::STDOUT_FILENO)
.map(|md| md.file_type().is_fifo())
.unwrap_or(false)
}
#[cfg(windows)]
pub fn stdin_is_file() -> bool {
false
}
#[cfg(windows)]
pub fn stdout_is_file() -> bool {
false
}
#[cfg(windows)]
pub fn stdin_is_char_device() -> bool {
false
}
#[cfg(windows)]
pub fn stdout_is_char_device() -> bool {
false
}
#[cfg(windows)]
pub fn stdin_is_fifo() -> bool {
false
}
#[cfg(windows)]
pub fn stdout_is_fifo() -> bool {
false
}

View File

@ -163,6 +163,11 @@ impl Types {
} }
} }
/// Creates a new file type matcher that never matches.
pub fn empty() -> Types {
Types::new(None, false)
}
/// Returns a match for the given path against this file type matcher. /// Returns a match for the given path against this file type matcher.
/// ///
/// The path is considered whitelisted if it matches a selected file type. /// The path is considered whitelisted if it matches a selected file type.