diff --git a/Cargo.toml b/Cargo.toml index e93f412b..f20847ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ crossbeam = "0.2" docopt = "0.6" env_logger = "0.3" grep = { version = "0.1", path = "grep" } +lazy_static = "0.2" +libc = "0.2" log = "0.3" memchr = "0.1" memmap = "0.2" @@ -31,15 +33,19 @@ parking_lot = "0.3" regex = { version = "0.1", path = "/home/andrew/rust/regex" } regex-syntax = { version = "0.3.1", path = "/home/andrew/rust/regex/regex-syntax" } rustc-serialize = "0.3" +term = { version = "0.4", path = "/home/andrew/clones/term" } thread_local = "0.2" walkdir = "0.1" +[target.'cfg(windows)'.dependencies] +kernel32-sys = "0.2" +winapi = "0.2" + [features] simd-accel = ["regex/simd-accel"] [dev-dependencies] glob = "0.2" -lazy_static = "0.2" [profile.release] debug = true diff --git a/src/args.rs b/src/args.rs index 8344b292..c2b14f7c 100644 --- a/src/args.rs +++ b/src/args.rs @@ -16,6 +16,7 @@ use ignore::Ignore; use out::Out; use printer::Printer; use search::{InputBuffer, Searcher}; +use sys; use types::{FileTypeDef, Types, TypesBuilder}; use walk; @@ -37,6 +38,9 @@ xrep is like the silver searcher and grep, but faster than both. Common options: -a, --text Search binary files as if they were text. -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 match the given glob. This always overrides any other ignore logic. Multiple glob flags may be @@ -44,8 +48,13 @@ Common options: Precede a glob with a '!' to exclude it. -h, --help Show this usage message. -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. + -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 flags may be provided. Use the --type-list flag 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 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 Search hidden directories and files. @@ -93,10 +109,16 @@ Less common options: --no-ignore 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 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 (capped at 6). [default: 0] @@ -123,6 +145,7 @@ pub struct RawArgs { arg_path: Vec, flag_after_context: usize, flag_before_context: usize, + flag_color: String, flag_context: usize, flag_context_separator: String, flag_count: bool, @@ -130,14 +153,20 @@ pub struct RawArgs { flag_files: bool, flag_follow: bool, flag_glob: Vec, + flag_heading: bool, flag_hidden: bool, flag_ignore_case: bool, flag_invert_match: bool, flag_line_number: bool, flag_line_terminator: String, flag_literal: bool, + flag_no_heading: bool, flag_no_ignore: bool, + flag_no_ignore_parent: bool, + flag_no_line_number: bool, + flag_pretty: bool, flag_quiet: bool, + flag_replace: Option, flag_text: bool, flag_threads: usize, flag_type: Vec, @@ -156,18 +185,22 @@ pub struct Args { paths: Vec, after_context: usize, before_context: usize, + color: bool, context_separator: Vec, count: bool, eol: u8, files: bool, follow: bool, glob_overrides: Option, + heading: bool, hidden: bool, ignore_case: bool, invert_match: bool, line_number: bool, no_ignore: bool, + no_ignore_parent: bool, quiet: bool, + replace: Option>, text: bool, threads: usize, type_defs: Vec, @@ -194,7 +227,11 @@ impl RawArgs { }; let paths = if self.arg_path.is_empty() { - vec![Path::new("./").to_path_buf()] + if sys::stdin_is_atty() { + vec![Path::new("./").to_path_buf()] + } else { + vec![Path::new("-").to_path_buf()] + } } else { self.arg_path.iter().map(|p| { Path::new(p).to_path_buf() @@ -232,6 +269,12 @@ impl RawArgs { } else { 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; if !with_filename { with_filename = paths.len() > 1 || paths[0].is_dir(); @@ -240,30 +283,44 @@ impl RawArgs { btypes.add_defaults(); try!(self.add_types(&mut btypes)); let types = try!(btypes.build()); - Ok(Args { + let mut args = Args { pattern: pattern, paths: paths, after_context: after_context, before_context: before_context, + color: color, context_separator: unescape(&self.flag_context_separator), count: self.flag_count, eol: eol, files: self.flag_files, follow: self.flag_follow, glob_overrides: glob_overrides, + heading: !self.flag_no_heading && self.flag_heading, hidden: self.flag_hidden, ignore_case: self.flag_ignore_case, 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_parent: self.flag_no_ignore_parent, quiet: self.flag_quiet, + replace: self.flag_replace.clone().map(|s| s.into_bytes()), text: self.flag_text, threads: threads, type_defs: btypes.definitions(), type_list: self.flag_type_list, types: types, 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<()> { @@ -338,19 +395,26 @@ impl Args { /// Create a new printer of individual search results that writes to the /// writer given. - pub fn printer(&self, wtr: W) -> Printer { - Printer::new(wtr) + pub fn printer(&self, wtr: W) -> Printer { + let mut p = Printer::new(wtr, self.color) .context_separator(self.context_separator.clone()) .eol(self.eol) + .heading(self.heading) .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 /// to the writer given. pub fn out(&self, wtr: W) -> Out { 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 @@ -364,7 +428,7 @@ impl Args { /// Create a new line based searcher whose configuration is taken from the /// command line. This searcher supports a dizzying array of features: /// 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, inp: &'a mut InputBuffer, printer: &'a mut Printer, @@ -399,16 +463,19 @@ impl Args { } /// 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 { let wd = WalkDir::new(path).follow_links(self.follow); let mut ig = Ignore::new(); ig.ignore_hidden(!self.hidden); ig.no_ignore(self.no_ignore); ig.add_types(self.types.clone()); + if !self.no_ignore_parent { + try!(ig.push_parents(path)); + } if let Some(ref overrides) = self.glob_overrides { ig.add_override(overrides.clone()); } - walk::Iter::new(ig, wd) + Ok(walk::Iter::new(ig, wd)) } } diff --git a/src/gitignore.rs b/src/gitignore.rs index 39508005..7ca693b9 100644 --- a/src/gitignore.rs +++ b/src/gitignore.rs @@ -84,6 +84,8 @@ pub struct Gitignore { set: glob::Set, root: PathBuf, patterns: Vec, + num_ignores: u64, + num_whitelist: u64, } impl Gitignore { @@ -152,6 +154,16 @@ impl Gitignore { } 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. @@ -238,10 +250,14 @@ impl GitignoreBuilder { /// /// Once a matcher is built, no new glob patterns can be added to it. pub fn build(self) -> Result { + let nignores = self.patterns.iter().filter(|p| !p.whitelist).count(); + let nwhitelist = self.patterns.iter().filter(|p| p.whitelist).count(); Ok(Gitignore { set: try!(self.builder.build()), root: self.root, patterns: self.patterns, + num_ignores: nignores as u64, + num_whitelist: nwhitelist as u64, }) } diff --git a/src/ignore.rs b/src/ignore.rs index f752fde2..550cbb8c 100644 --- a/src/ignore.rs +++ b/src/ignore.rs @@ -15,21 +15,39 @@ of `IgnoreDir`s for use during directory traversal. use std::error::Error as StdError; use std::fmt; +use std::io; use std::path::{Path, PathBuf}; -use gitignore::{self, Gitignore, GitignoreBuilder, Match}; +use gitignore::{self, Gitignore, GitignoreBuilder, Match, Pattern}; use types::Types; +const IGNORE_NAMES: &'static [&'static str] = &[ + ".gitignore", + ".agignore", + ".xrepignore", +]; + /// Represents an error that can occur when parsing a gitignore file. #[derive(Debug)] pub enum Error { Gitignore(gitignore::Error), + Io { + path: PathBuf, + err: io::Error, + }, +} + +impl Error { + fn from_io>(path: P, err: io::Error) -> Error { + Error::Io { path: path.as_ref().to_path_buf(), err: err } + } } impl StdError for Error { fn description(&self) -> &str { match *self { 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 { match *self { 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>, /// A set of override globs that are always checked first. A match (whether /// it's whitelist or blacklist) trumps anything in stack. - overrides: Option, + overrides: Overrides, /// A file type matcher. - types: Option, + types: Types, ignore_hidden: bool, no_ignore: bool, } @@ -71,8 +92,8 @@ impl Ignore { pub fn new() -> Ignore { Ignore { stack: vec![], - overrides: None, - types: None, + overrides: Overrides::new(None), + types: Types::empty(), ignore_hidden: true, no_ignore: false, } @@ -92,17 +113,51 @@ impl Ignore { /// Add a set of globs that overrides all other match logic. pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore { - self.overrides = Some(gi); + self.overrides = Overrides::new(Some(gi)); self } /// Add a file type matcher. The file type matcher has the lowest /// precedence. pub fn add_types(&mut self, types: Types) -> &mut Ignore { - self.types = Some(types); + self.types = types; self } + /// Push parent directories of `path` on to the stack. + pub fn push_parents>( + &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. /// /// Note that even if this returns an error, the directory is added to the @@ -112,7 +167,17 @@ impl Ignore { self.stack.push(None); 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, Error>, + ) -> Result<(), Error> { + match result { Ok(id) => { self.stack.push(id); Ok(()) @@ -135,11 +200,9 @@ impl Ignore { /// Returns true if and only if the given file path should be ignored. pub fn ignored>(&self, path: P, is_dir: bool) -> bool { let path = path.as_ref(); - if let Some(ref overrides) = self.overrides { - let mat = overrides.matched(path, is_dir).invert(); - if let Some(is_ignored) = self.ignore_match(path, mat) { - return is_ignored; - } + let mat = self.overrides.matched(path, is_dir); + if let Some(is_ignored) = self.ignore_match(path, mat) { + return is_ignored; } if self.ignore_hidden && is_hidden(&path) { debug!("{} ignored because it is hidden", path.display()); @@ -156,11 +219,9 @@ impl Ignore { break; } } - if let Some(ref types) = self.types { - let mat = types.matched(path, is_dir); - if let Some(is_ignored) = self.ignore_match(path, mat) { - return is_ignored; - } + let mat = self.types.matched(path, is_dir); + if let Some(is_ignored) = self.ignore_match(path, mat) { + return is_ignored; } false } @@ -210,6 +271,23 @@ impl IgnoreDir { /// If no ignore glob patterns could be found in the directory then `None` /// is returned. pub fn new>(path: P) -> Result, 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, S, I>( + path: P, + names: I, + ) -> Result, Error> + where P: AsRef, S: AsRef, I: Iterator { let mut id = IgnoreDir { path: path.as_ref().to_path_buf(), gi: None, @@ -217,9 +295,9 @@ impl IgnoreDir { let mut ok = false; let mut builder = GitignoreBuilder::new(&id.path); // The ordering here is important. Later globs have higher precedence. - ok = builder.add_path(id.path.join(".gitignore")).is_ok() || ok; - ok = builder.add_path(id.path.join(".agignore")).is_ok() || ok; - ok = builder.add_path(id.path.join(".xrepignore")).is_ok() || ok; + for name in names { + ok = builder.add_path(id.path.join(name.as_ref())).is_ok() || ok; + } if !ok { Ok(None) } else { @@ -246,6 +324,56 @@ impl IgnoreDir { } } +/// Manages a set of overrides provided explicitly by the end user. +struct Overrides { + gi: Option, + 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) -> Overrides { + Overrides { + gi: gi, + unmatched_pat: Pattern { + from: Path::new("").to_path_buf(), + original: "".to_string(), + pat: "".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>(&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>(path: P) -> bool { if let Some(name) = path.as_ref().file_name() { name.to_str().map(|s| s.starts_with(".")).unwrap_or(false) diff --git a/src/main.rs b/src/main.rs index 0b843eba..164918a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,9 +4,11 @@ extern crate crossbeam; extern crate docopt; extern crate env_logger; extern crate grep; -#[cfg(test)] +#[cfg(windows)] +extern crate kernel32; #[macro_use] extern crate lazy_static; +extern crate libc; #[macro_use] extern crate log; extern crate memchr; @@ -16,12 +18,15 @@ extern crate parking_lot; extern crate regex; extern crate regex_syntax as syntax; extern crate rustc_serialize; +extern crate term; extern crate thread_local; extern crate walkdir; +#[cfg(windows)] +extern crate winapi; use std::error::Error; use std::fs::File; -use std::io::{self, Write}; +use std::io; use std::path::Path; use std::process; use std::result; @@ -58,6 +63,7 @@ mod ignore; mod out; mod printer; mod search; +mod sys; mod types; mod walk; @@ -68,7 +74,7 @@ fn main() { Ok(count) if count == 0 => process::exit(1), Ok(count) => process::exit(0), Err(err) => { - let _ = writeln!(&mut io::stderr(), "{}", err); + eprintln!("{}", err); process::exit(1); } } @@ -105,7 +111,7 @@ fn run(args: Args) -> Result { if p == Path::new("-") { workq.push(Work::Stdin) } else { - for ent in args.walker(p) { + for ent in try!(args.walker(p)) { workq.push(Work::File(ent)); } } @@ -121,14 +127,14 @@ fn run(args: Args) -> Result { } fn run_files(args: Args) -> Result { - 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; for p in args.paths() { if p == Path::new("-") { printer.path(&Path::new("")); file_count += 1; } else { - for ent in args.walker(p) { + for ent in try!(args.walker(p)) { printer.path(ent.path()); file_count += 1; } @@ -138,7 +144,7 @@ fn run_files(args: Args) -> Result { } fn run_types(args: Args) -> Result { - 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; for def in args.type_defs() { printer.type_def(def); @@ -200,7 +206,7 @@ impl Worker { self.match_count } - fn do_work( + fn do_work( &mut self, printer: &mut Printer, work: WorkReady, @@ -229,7 +235,7 @@ impl Worker { } } - fn search( + fn search( &mut self, printer: &mut Printer, path: &Path, diff --git a/src/out.rs b/src/out.rs index cb9d1181..7f502eeb 100644 --- a/src/out.rs +++ b/src/out.rs @@ -9,7 +9,7 @@ use std::io::{self, Write}; pub struct Out { wtr: io::BufWriter, printed: bool, - file_separator: Vec, + file_separator: Option>, } impl Out { @@ -18,7 +18,7 @@ impl Out { Out { wtr: io::BufWriter::new(wtr), printed: false, - file_separator: vec![], + file_separator: None, } } @@ -27,16 +27,18 @@ impl Out { /// /// If sep is empty, then no file separator is printed. pub fn file_separator(mut self, sep: Vec) -> Out { - self.file_separator = sep; + self.file_separator = Some(sep); self } /// Write the search results of a single file to the underlying wtr and /// flush wtr. pub fn write(&mut self, buf: &[u8]) { - if self.printed && !self.file_separator.is_empty() { - let _ = self.wtr.write_all(&self.file_separator); - let _ = self.wtr.write_all(b"\n"); + if let Some(ref sep) = self.file_separator { + if self.printed { + let _ = self.wtr.write_all(sep); + let _ = self.wtr.write_all(b"\n"); + } } let _ = self.wtr.write_all(buf); let _ = self.wtr.flush(); diff --git a/src/printer.rs b/src/printer.rs index 8c53c051..bcb1f468 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -1,8 +1,16 @@ -use std::io; +use std::io::{self, Write}; 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 self::Writer::*; + /// Printer encapsulates all output logic for searching. /// /// 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. pub struct Printer { /// The underlying writer. - wtr: W, + wtr: Writer, /// Whether anything has been printed to wtr yet. has_printed: bool, /// The string to use to separate non-contiguous runs of context lines. @@ -19,21 +27,31 @@ pub struct Printer { /// printed via the match directly, but occasionally we need to insert them /// ourselves (for example, to print a context separator). 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. quiet: bool, + /// A string to use as a replacement of each match in a matching line. + replace: Option>, /// Whether to prefix each match with the corresponding file name. with_filename: bool, } -impl Printer { +impl Printer { /// Create a new printer that writes to wtr. - pub fn new(wtr: W) -> Printer { + /// + /// `color` should be true if the printer should try to use coloring. + pub fn new(wtr: W, color: bool) -> Printer { Printer { - wtr: wtr, + wtr: Writer::new(wtr, color), has_printed: false, context_separator: "--".to_string().into_bytes(), eol: b'\n', + heading: false, quiet: false, + replace: None, with_filename: false, } } @@ -50,12 +68,30 @@ impl Printer { 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 { + self.heading = yes; + self + } + /// When set, all output is suppressed. pub fn quiet(mut self, yes: bool) -> Printer { self.quiet = yes; 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) -> Printer { + self.replace = Some(replacement); + self + } + /// When set, each match is prefixed with the file name that it came from. pub fn with_filename(mut self, yes: bool) -> Printer { self.with_filename = yes; @@ -70,7 +106,7 @@ impl Printer { /// Flushes the underlying writer and returns it. pub fn into_inner(mut self) -> W { let _ = self.wtr.flush(); - self.wtr + self.wtr.into_inner() } /// Prints a type definition. @@ -120,26 +156,51 @@ impl Printer { pub fn matched>( &mut self, + re: &Regex, path: P, buf: &[u8], start: usize, end: usize, line_number: Option, ) { - 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 let Some(line_number) = line_number { - self.write(line_number.to_string().as_bytes()); - self.write(b":"); + self.line_number(line_number, 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) { 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>( &mut self, path: P, @@ -148,13 +209,18 @@ impl Printer { end: usize, line_number: Option, ) { - 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(b"-"); } if let Some(line_number) = line_number { - self.write(line_number.to_string().as_bytes()); - self.write(b"-"); + self.line_number(line_number, b'-'); } self.write(&buf[start..end]); if buf[start..end].last() != Some(&self.eol) { @@ -162,6 +228,28 @@ impl Printer { } } + fn write_heading>(&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]) { if self.quiet { return; @@ -175,3 +263,154 @@ impl Printer { self.write(&[eol]); } } + +enum Writer { + Colored(term::TerminfoTerminal), + NoColor(W), +} + +lazy_static! { + static ref TERMINFO: Option> = { + match term::terminfo::TermInfo::from_env() { + Ok(info) => Some(Arc::new(info)), + Err(err) => { + debug!("error loading terminfo for coloring: {}", err); + None + } + } + }; +} + +impl Writer { + fn new(wtr: W, color: bool) -> Writer { + // 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( + &mut self, + mut f: F, + ) -> term::Result<()> + where F: FnMut(&mut term::TerminfoTerminal) -> term::Result<()> { + match *self { + Colored(ref mut w) => f(w), + NoColor(_) => Err(term::Error::NotSupported), + } + } + + fn map_bool( + &self, + mut f: F, + ) -> bool + where F: FnMut(&term::TerminfoTerminal) -> bool { + match *self { + Colored(ref w) => f(w), + NoColor(_) => false, + } + } +} + +impl io::Write for Writer { + fn write(&mut self, buf: &[u8]) -> io::Result { + 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 term::Terminal for Writer { + 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, + } + } +} diff --git a/src/search.rs b/src/search.rs index d0efb7ae..66706225 100644 --- a/src/search.rs +++ b/src/search.rs @@ -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. /// /// `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.add_line(end); 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.after_context_remaining = self.opts.after_context; } @@ -739,7 +740,7 @@ fn main() { mut map: F, ) -> (u64, String) { 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 count = { let searcher = Searcher::new( @@ -755,7 +756,7 @@ fn main() { mut map: F, ) -> (u64, String) { 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 count = { let searcher = Searcher::new( diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 00000000..ae65f8e8 --- /dev/null +++ b/src/sys.rs @@ -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 { + 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 +} diff --git a/src/types.rs b/src/types.rs index 027697d9..67f643c7 100644 --- a/src/types.rs +++ b/src/types.rs @@ -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. /// /// The path is considered whitelisted if it matches a selected file type.