From 7bf7ceb5d3953a38a6fad45110af2e4117c778ca Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Thu, 25 Apr 2019 07:20:24 -0400 Subject: progress --- Cargo.lock | 52 +++++++++++ Cargo.toml | 3 +- src/args.rs | 182 ++++++++++++++++---------------------- src/config.rs | 23 ++--- src/err.rs | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/logger.rs | 7 +- src/main.rs | 45 ++++++---- src/path_printer.rs | 9 +- tests/regression.rs | 4 +- 9 files changed, 438 insertions(+), 137 deletions(-) create mode 100644 src/err.rs diff --git a/Cargo.lock b/Cargo.lock index 49dbc721..53082d76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,28 @@ name = "autocfg" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "backtrace" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)", + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "backtrace-sys" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.35 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "base64" version = "0.10.1" @@ -540,9 +562,15 @@ dependencies = [ "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "termcolor 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "rustc-demangle" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ryu" version = "0.2.7" @@ -586,6 +614,25 @@ name = "smallvec" version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "snafu" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "backtrace 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu-derive 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "snafu-derive" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.31 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "strsim" version = "0.8.0" @@ -718,6 +765,8 @@ dependencies = [ "checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c" "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652" "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" +"checksum backtrace 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f106c02a3604afcdc0df5d36cc47b44b55917dbaf3d808f71c163a0ddba64637" +"checksum backtrace-sys 0.1.28 (registry+https://github.com/rust-lang/crates.io-index)" = "797c830ac25ccc92a7f8a7b9862bde440715531514594a6154e3d4a54dd769b6" "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" "checksum bstr 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6c8203ca06c502958719dae5f653a79e0cc6ba808ed02beffbf27d09610f2143" @@ -767,12 +816,15 @@ dependencies = [ "checksum regex-automata 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a25a7daa2eea48550e9946133d6cc9621020d29cc7069089617234bf8b6a8693" "checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96" "checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" +"checksum rustc-demangle 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "ccc78bfd5acd7bf3e89cffcf899e5cb1a52d6fafa8dec2739ad70c9577a57288" "checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" "checksum same-file 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8f20c4be53a8a1ff4c1f1b2bd14570d2f634628709752f0702ecdd2b3f9a5267" "checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4" "checksum serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "58fc82bec244f168b23d1963b45c8bf5726e9a15a9d146a067f9081aeed2de79" "checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" "checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be" +"checksum snafu 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "67a2e16c9b74a09d4bc84ea6e5eb68fef70b4cb14e8ccd26f2de76af2e9c2e8a" +"checksum snafu-derive 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "bf96650c2b31fa949780f792025f4ca79fbe87bd723a8b3573dce37be7193b08" "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" "checksum syn 0.15.31 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b4cfac95805274c6afdb12d8f770fa2d27c045953e7b630a81801953699a9a" "checksum tempfile 3.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "b86c784c88d98c801132806dadd3819ed29d8600836c4088e855cdf3e178ed8a" diff --git a/Cargo.toml b/Cargo.toml index 7bd62d4f..1a0ad2e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,10 +50,11 @@ bstr = "0.1.2" grep = { version = "0.2.4", path = "grep" } ignore = { version = "0.4.7", path = "ignore" } lazy_static = "1.1.0" -log = "0.4.5" +log = { version = "0.4.5", features = ["std"] } num_cpus = "1.8.0" regex = "1.0.5" serde_json = "1.0.23" +snafu = "0.2.3" termcolor = "1.0.3" [dependencies.clap] diff --git a/src/args.rs b/src/args.rs index cd217b4b..4ba0e540 100644 --- a/src/args.rs +++ b/src/args.rs @@ -36,6 +36,7 @@ use ignore::{Walk, WalkBuilder, WalkParallel}; use log; use num_cpus; use regex; +use snafu::{self, ResultExt}; use termcolor::{ WriteColor, BufferWriter, ColorChoice, @@ -43,6 +44,7 @@ use termcolor::{ use crate::app; use crate::config; +use crate::err::{self, Result}; use crate::logger::Logger; use crate::messages::{set_messages, set_ignore_messages}; use crate::path_printer::{PathPrinter, PathPrinterBuilder}; @@ -50,7 +52,6 @@ use crate::search::{ PatternMatcher, Printer, SearchWorker, SearchWorkerBuilder, }; use crate::subject::SubjectBuilder; -use crate::Result; /// The command that ripgrep should execute based on the command line /// configuration. @@ -141,9 +142,7 @@ impl Args { set_messages(!early_matches.is_present("no-messages")); set_ignore_messages(!early_matches.is_present("no-ignore-messages")); - if let Err(err) = Logger::init() { - return Err(format!("failed to initialize logger: {}", err).into()); - } + Logger::init()?; if early_matches.is_present("trace") { log::set_max_level(log::LevelFilter::Trace); } else if early_matches.is_present("debug") { @@ -422,13 +421,22 @@ impl SortBy { match self.kind { SortByKind::None | SortByKind::Path => {} SortByKind::LastModified => { - env::current_exe()?.metadata()?.modified()?; + env::current_exe() + .and_then(|x| x.metadata()) + .and_then(|x| x.modified()) + .context(err::UnsupportedSortModified)?; } SortByKind::LastAccessed => { - env::current_exe()?.metadata()?.accessed()?; + env::current_exe() + .and_then(|x| x.metadata()) + .and_then(|x| x.accessed()) + .context(err::UnsupportedSortAccess)?; } SortByKind::Created => { - env::current_exe()?.metadata()?.created()?; + env::current_exe() + .and_then(|x| x.metadata()) + .and_then(|x| x.created()) + .context(err::UnsupportedSortCreation)?; } } Ok(()) @@ -611,21 +619,12 @@ impl ArgMatches { Ok(matcher) => return Ok(PatternMatcher::PCRE2(matcher)), Err(err) => err, }; - Err(From::from(format!( - "regex could not be compiled with either the default regex \ - engine or with PCRE2.\n\n\ - default regex engine error:\n{}\n{}\n{}\n\n\ - PCRE2 regex engine error:\n{}", - "~".repeat(79), rust_err, "~".repeat(79), pcre_err, - ))) + err::Hybrid { + rust_err: Box::new(rust_err), + pcre_err: Box::new(pcre_err), + }.fail() } else { - let matcher = match self.matcher_rust(patterns) { - Ok(matcher) => matcher, - Err(err) => { - return Err(From::from(suggest_pcre2(err.to_string()))); - } - }; - Ok(PatternMatcher::RustRegex(matcher)) + Ok(PatternMatcher::RustRegex(self.matcher_rust(patterns)?)) } } @@ -635,11 +634,7 @@ impl ArgMatches { /// then this returns an error. #[cfg(not(feature = "pcre2"))] fn matcher(&self, patterns: &[String]) -> Result { - if self.is_present("pcre2") { - return Err(From::from( - "PCRE2 is not available in this build of ripgrep", - )); - } + snafu::ensure!(!self.is_present("pcre2"), err::PCRE2Unavailable); let matcher = self.matcher_rust(patterns)?; Ok(PatternMatcher::RustRegex(matcher)) } @@ -692,10 +687,7 @@ impl ArgMatches { } else { builder.build(&patterns.join("|")) }; - match res { - Ok(m) => Ok(m), - Err(err) => Err(From::from(suggest_multiline(err.to_string()))), - } + res.eager_context(err::RustRegex) } /// Build a matcher using PCRE2. @@ -737,7 +729,7 @@ impl ArgMatches { if self.is_present("crlf") { builder.crlf(true); } - Ok(builder.build(&patterns.join("|"))?) + Ok(builder.build(&patterns.join("|")).context(err::PCRE2Regex)?) } /// Build a JSON printer that writes results to the given writer. @@ -987,7 +979,7 @@ impl ArgMatches { // Start with a default set of color specs. let mut specs = default_color_specs(); for spec_str in self.values_of_lossy_vec("colors") { - specs.push(spec_str.parse()?); + specs.push(spec_str.parse().context(err::InvalidColorSpec)?); } Ok(ColorSpecs::new(&specs)) } @@ -1078,7 +1070,8 @@ impl ArgMatches { return Ok(EncodingMode::Disabled); } - Ok(EncodingMode::Some(Encoding::new(&label)?)) + let enc = Encoding::new(&label).context(err::SearchConfig)?; + Ok(EncodingMode::Some(enc)) } /// Return the file separator to use based on the CLI configuration. @@ -1267,16 +1260,17 @@ impl ArgMatches { /// Builds the set of glob overrides from the command line flags. fn overrides(&self) -> Result { - let mut builder = OverrideBuilder::new(env::current_dir()?); + let cwd = env::current_dir().context(err::CurrentDir)?; + let mut builder = OverrideBuilder::new(cwd); for glob in self.values_of_lossy_vec("glob") { - builder.add(&glob)?; + builder.add(&glob).context(err::InvalidGlobFlag)?; } // This only enables case insensitivity for subsequent globs. builder.case_insensitive(true).unwrap(); for glob in self.values_of_lossy_vec("iglob") { - builder.add(&glob)?; + builder.add(&glob).context(err::InvalidIGlobFlag)?; } - Ok(builder.build()?) + Ok(builder.build().context(err::GlobBuild)?) } /// Return all file paths that ripgrep should search. @@ -1330,16 +1324,10 @@ impl ArgMatches { }; if sep.is_empty() { Ok(None) - } else if sep.len() > 1 { - Err(From::from(format!( - "A path separator must be exactly one byte, but \ - the given separator is {} bytes: {}\n\ - In some shells on Windows '/' is automatically \ - expanded. Use '//' instead.", - sep.len(), - cli::escape(&sep), - ))) } else { + snafu::ensure!(sep.len() == 1, err::InvalidPathSeparator { + separator: sep.clone(), + }); Ok(Some(sep[0])) } } @@ -1386,17 +1374,17 @@ impl ArgMatches { } if let Some(paths) = self.values_of_os("file") { for path in paths { - if path == "-" { - pats.extend(cli::patterns_from_stdin()? - .into_iter() - .map(|p| self.pattern_from_string(p)) - ); - } else { - pats.extend(cli::patterns_from_path(path)? - .into_iter() - .map(|p| self.pattern_from_string(p)) - ); - } + let some_pats = + if path == "-" { + cli::patterns_from_stdin() + .context(err::ReadManyPatterns)? + } else { + cli::patterns_from_path(path) + .context(err::ReadManyPatterns)? + }; + pats.extend( + some_pats.into_iter().map(|p| self.pattern_from_string(p)) + ); } } Ok(pats) @@ -1417,7 +1405,7 @@ impl ArgMatches { /// /// If the pattern is not valid UTF-8, then an error is returned. fn pattern_from_os_str(&self, pat: &OsStr) -> Result { - let s = cli::pattern_from_os(pat)?; + let s = cli::pattern_from_os(pat).context(err::ReadOSPattern)?; Ok(self.pattern_from_str(s)) } @@ -1475,11 +1463,12 @@ impl ArgMatches { /// flag. If no --pre-globs are available, then this always returns an /// empty set of globs. fn preprocessor_globs(&self) -> Result { - let mut builder = OverrideBuilder::new(env::current_dir()?); + let cwd = env::current_dir().context(err::CurrentDir)?; + let mut builder = OverrideBuilder::new(cwd); for glob in self.values_of_lossy_vec("pre-glob") { - builder.add(&glob)?; + builder.add(&glob).context(err::InvalidPreGlob)?; } - Ok(builder.build()?) + Ok(builder.build().context(err::PreGlobBuild)?) } /// Parse the regex-size-limit argument option into a byte count. @@ -1561,7 +1550,7 @@ impl ArgMatches { builder.clear(&ty); } for def in self.values_of_lossy_vec("type-add") { - builder.add_def(&def)?; + builder.add_def(&def).context(err::InvalidTypeDefinition)?; } for ty in self.values_of_lossy_vec("type") { builder.select(&ty); @@ -1569,7 +1558,7 @@ impl ArgMatches { for ty in self.values_of_lossy_vec("type-not") { builder.negate(&ty); } - builder.build().map_err(From::from) + builder.build().eager_context(err::TypeDefinitionBuild) } /// Returns the number of times the `unrestricted` flag is provided. @@ -1628,7 +1617,11 @@ impl ArgMatches { fn usize_of(&self, name: &str) -> Result> { match self.value_of_lossy(name) { None => Ok(None), - Some(v) => v.parse().map(Some).map_err(From::from), + Some(v) => { + v.parse() + .map(Some) + .eager_context(err::InvalidNumber { flag: name.to_string() }) + } } } @@ -1636,15 +1629,17 @@ impl ArgMatches { /// /// If the aforementioned format is not recognized, then this returns an /// error. - fn parse_human_readable_size( - &self, - arg_name: &str, - ) -> Result> { - let size = match self.value_of_lossy(arg_name) { - None => return Ok(None), - Some(size) => size, - }; - Ok(Some(cli::parse_human_readable_size(&size)?)) + fn parse_human_readable_size(&self, name: &str) -> Result> { + match self.value_of_lossy(name) { + None => Ok(None), + Some(size) => { + cli::parse_human_readable_size(&size) + .map(Some) + .eager_context(err::InvalidHumanSize { + flag: name.to_string() + }) + } + } } } @@ -1678,32 +1673,6 @@ impl ArgMatches { } } -/// Inspect an error resulting from building a Rust regex matcher, and if it's -/// believed to correspond to a syntax error that PCRE2 could handle, then -/// add a message to suggest the use of -P/--pcre2. -#[cfg(feature = "pcre2")] -fn suggest_pcre2(msg: String) -> String { - if !msg.contains("backreferences") && !msg.contains("look-around") { - msg - } else { - format!("{} - -Consider enabling PCRE2 with the --pcre2 flag, which can handle backreferences -and look-around.", msg) - } -} - -fn suggest_multiline(msg: String) -> String { - if msg.contains("the literal") && msg.contains("not allowed") { - format!("{} - -Consider enabling multiline mode with the --multiline flag (or -U for short). -When multiline mode is enabled, new line characters can be matched.", msg) - } else { - msg - } -} - /// Convert the result of parsing a human readable file size to a `usize`, /// failing if the type does not fit. fn u64_to_usize( @@ -1716,11 +1685,14 @@ fn u64_to_usize( None => return Ok(None), Some(value) => value, }; - if value <= usize::MAX as u64 { - Ok(Some(value as usize)) - } else { - Err(From::from(format!("number too large for {}", arg_name))) - } + snafu::ensure!( + value <= usize::MAX as u64, + err::NumberTooBig { + flag: arg_name.to_string(), + limit: usize::MAX as u64, + } + ); + Ok(Some(value as usize)) } /// Builds a comparator for sorting two files according to a system time @@ -1768,7 +1740,7 @@ where I: IntoIterator, Err(err) => err, }; if err.use_stderr() { - return Err(err.into()); + return Err(snafu::Context { error: err, context: err::Clap }.into()); } // Explicitly ignore any error returned by write!. The most likely error // at this point is a broken pipe error, in which case, we want to ignore diff --git a/src/config.rs b/src/config.rs index a5e492ec..6c9b9b32 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,6 @@ // argument corresponds precisely to one shell argument. use std::env; -use std::error::Error; use std::fs::File; use std::io; use std::ffi::OsString; @@ -11,8 +10,9 @@ use std::path::{Path, PathBuf}; use bstr::io::BufReadExt; use log; +use snafu::{self, ResultExt}; -use crate::Result; +use crate::err::{self, Error, Result}; /// Return a sequence of arguments derived from ripgrep rc configuration files. pub fn args() -> Vec { @@ -55,12 +55,11 @@ pub fn args() -> Vec { /// for each line in addition to successfully parsed arguments. fn parse>( path: P, -) -> Result<(Vec, Vec>)> { +) -> Result<(Vec, Vec)> { let path = path.as_ref(); - match File::open(&path) { - Ok(file) => parse_reader(file), - Err(err) => Err(From::from(format!("{}: {}", path.display(), err))), - } + let file = File::open(&path).context(err::ConfigIO { path })?; + let res = parse_reader(file).context(err::ConfigIO { path })?; + Ok(res) } /// Parse a single ripgrep rc file from the given reader. @@ -76,10 +75,10 @@ fn parse>( /// in addition to successfully parsed arguments. fn parse_reader( rdr: R, -) -> Result<(Vec, Vec>)> { +) -> io::Result<(Vec, Vec)> { let bufrdr = io::BufReader::new(rdr); let (mut args, mut errs) = (vec![], vec![]); - let mut line_number = 0; + let mut line_number = 0u64; bufrdr.for_byte_line_with_terminator(|line| { line_number += 1; @@ -92,7 +91,11 @@ fn parse_reader( args.push(osstr.to_os_string()); } Err(err) => { - errs.push(format!("{}: {}", line_number, err).into()); + let ctx = snafu::Context { + error: err, + context: err::ConfigInvalidUTF8 { line_number }, + }; + errs.push(ctx.into()); } } Ok(true) diff --git a/src/err.rs b/src/err.rs new file mode 100644 index 00000000..26919593 --- /dev/null +++ b/src/err.rs @@ -0,0 +1,250 @@ +use std::io; +use std::path::{Path, PathBuf}; +use std::result; + +use snafu::{Backtrace, Context, Snafu}; + +pub type Result = result::Result; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub enum Error { + #[snafu(display("I/O error: {}", source))] + IO { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display( + "failed to retrieve current working directory: {}", source, + ))] + CurrentDir { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display("sorting by modified time is unsupported: {}", source))] + UnsupportedSortModified { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display("sorting by access time is unsupported: {}", source))] + UnsupportedSortAccess { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display("sorting by creation time is unsupported: {}", source))] + UnsupportedSortCreation { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display( + "I/O error parsing config in {}: {}", + path.display(), + source, + ))] + ConfigIO { + backtrace: Backtrace, + source: io::Error, + path: PathBuf, + }, + #[snafu(display( + "config parse error on line {}: {}", + line_number, + source, + ))] + ConfigInvalidUTF8 { + backtrace: Backtrace, + source: bstr::Utf8Error, + line_number: u64, + }, + #[snafu(display("failed to initialize logger: {}", source))] + LoggerInit { + backtrace: Backtrace, + source: log::SetLoggerError, + }, + #[snafu(display("{}", source))] + Clap { + backtrace: Backtrace, + source: clap::Error, + }, + #[snafu(display("{}", source))] + ReadManyPatterns { + backtrace: Backtrace, + source: io::Error, + }, + #[snafu(display("{}", source))] + ReadOSPattern { + backtrace: Backtrace, + source: grep::cli::InvalidPatternError, + }, + #[snafu(display( + "A path separator must be exactly one byte, but \ + the given separator is {} bytes: {}\n\ + In some shells on Windows '/' is automatically \ + expanded. Use '//' instead.", + separator.len(), grep::cli::escape(&separator), + ))] + InvalidPathSeparator { + backtrace: Backtrace, + separator: Vec, + }, + #[snafu(display("error parsing -g/--glob: {}", source))] + InvalidGlobFlag { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error parsing --iglob: {}", source))] + InvalidIGlobFlag { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error building --glob matcher: {}", source))] + GlobBuild { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error parsing --pre-glob: {}", source))] + InvalidPreGlob { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error building --pre-glob matcher: {}", source))] + PreGlobBuild { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error parsing --type-add value: {}", source))] + InvalidTypeDefinition { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("error building type matcher: {}", source))] + TypeDefinitionBuild { + backtrace: Backtrace, + source: ignore::Error, + }, + #[snafu(display("failed to parse {} value as a number: {}", flag, source))] + InvalidNumber { + backtrace: Backtrace, + source: std::num::ParseIntError, + flag: String, + }, + #[snafu(display( + "failed to parse {} value as a file size: {}", flag, source, + ))] + InvalidHumanSize { + backtrace: Backtrace, + source: grep::cli::ParseSizeError, + flag: String, + }, + #[snafu(display( + "number given to {} is too large (limit is {})", + flag, limit, + ))] + NumberTooBig { + backtrace: Backtrace, + flag: String, + limit: u64, + }, + #[snafu(display("{}", source))] + SearchConfig { + backtrace: Backtrace, + source: grep::searcher::ConfigError, + }, + #[snafu(display("invalid --colors spec: {}", source))] + InvalidColorSpec { + backtrace: Backtrace, + source: grep::printer::ColorError, + }, + #[snafu(display("{}", suggest(source)))] + RustRegex { + backtrace: Backtrace, + source: grep::regex::Error, + }, + #[snafu(display( + "regex could not be compiled with either the default regex \ + engine or with PCRE2.\n\n\ + default regex engine error:\n{}\n{}\n{}\n\n\ + PCRE2 regex engine error:\n{}", + "~".repeat(79), + rust_err, + "~".repeat(79), + pcre_err, + ))] + Hybrid { + backtrace: Backtrace, + rust_err: Box, + pcre_err: Box, + }, + #[snafu(display("failed to write type definitions to stdout: {}", source))] + WriteTypes { + backtrace: Backtrace, + source: io::Error, + }, + #[cfg(feature = "pcre2")] + #[snafu(display("{}", suggest(source)))] + PCRE2Regex { + backtrace: Backtrace, + source: grep::pcre2::Error, + }, + #[snafu(display("PCRE2 is not available in this build of ripgrep"))] + PCRE2Unavailable { + backtrace: Backtrace, + }, + #[snafu(display("failed to write PCRE2 version to stdout: {}", source))] + PCRE2Version { + backtrace: Backtrace, + source: io::Error, + }, +} + +impl Error { + /// Return true if and only if this corresponds to an I/O error generated + /// by a broken pipe. + pub fn is_broken_pipe(&self) -> bool { + self.io_err().map_or(false, |e| e.kind() == io::ErrorKind::BrokenPipe) + } + + /// Return a reference to this error's underlying I/O error, if one exists. + pub fn io_err(&self) -> Option<&io::Error> { + match *self { + Error::IO { ref source, .. } => Some(source), + Error::CurrentDir { ref source, .. } => Some(source), + Error::UnsupportedSortModified { ref source, .. } => Some(source), + Error::UnsupportedSortAccess { ref source, .. } => Some(source), + Error::UnsupportedSortCreation { ref source, .. } => Some(source), + Error::ConfigIO { ref source, .. } => Some(source), + Error::ReadManyPatterns { ref source, .. } => Some(source), + Error::WriteTypes { ref source, .. } => Some(source), + Error::PCRE2Version { ref source, .. } => Some(source), + _ => None, + } + } +} + +/// Inspect an error's display string and look for potentially suggestions +/// to give to an end user. +/// +/// These include: +/// +/// 1. If the error results from the use of a new line literal, then return a +/// new message suggesting the use of the -U/--multiline flag. +/// 2. If the error correspond to a syntax error that PCRE2 could handle, then +/// add a message to suggest the use of -P/--pcre2. +fn suggest(err: &E) -> String { + let msg = err.to_string(); + if msg.contains("the literal") && msg.contains("not allowed") { + format!("{} + +Consider enabling multiline mode with the --multiline flag (or -U for short). +When multiline mode is enabled, new line characters can be matched.", msg) + } else if cfg!(feature = "pcre2") && + (msg.contains("backreferences") || msg.contains("look-around")) + { + format!("{} + +Consider enabling PCRE2 with the --pcre2 flag, which can handle backreferences +and look-around.", msg) + } else { + msg + } +} diff --git a/src/logger.rs b/src/logger.rs index f12f0b19..bf3f677c 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -4,6 +4,9 @@ // for this functionality. use log::{self, Log}; +use snafu::ResultExt; + +use crate::err::{self, Result}; /// The simplest possible logger that logs to stderr. /// @@ -18,8 +21,8 @@ impl Logger { /// Create a new logger that logs to stderr and initialize it as the /// global logger. If there was a problem setting the logger, then an /// error is returned. - pub fn init() -> Result<(), log::SetLoggerError> { - log::set_logger(LOGGER) + pub fn init() -> Result<()> { + log::set_logger(LOGGER).eager_context(err::LoggerInit) } } diff --git a/src/main.rs b/src/main.rs index bed33296..a0a69ccf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,15 @@ +#![allow(warnings)] + use std::io::{self, Write}; use std::process; use std::sync::{Arc, Mutex}; use std::time::Instant; use ignore::WalkState; +use snafu::ResultExt; use args::Args; +use err::Result; use subject::Subject; #[macro_use] @@ -14,6 +18,7 @@ mod messages; mod app; mod args; mod config; +mod err; mod logger; mod path_printer; mod search; @@ -42,7 +47,8 @@ mod subject; #[global_allocator] static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; -type Result = ::std::result::Result>; +// type Result = ::std::result::Result>; +// type Result = fn main() { if let Err(err) = Args::parse().and_then(try_main) { @@ -213,12 +219,12 @@ fn files(args: &Args) -> Result { } if let Err(err) = path_printer.write_path(subject.path()) { // A broken pipe means graceful termination. - if err.kind() == io::ErrorKind::BrokenPipe { + if err.is_broken_pipe() { break; } // Otherwise, we have some other error that's preventing us from // writing to stdout, so we should bubble it up. - return Err(err.into()); + return Err(err); } } Ok(matched) @@ -239,7 +245,7 @@ fn files_parallel(args: &Args) -> Result { let matched = Arc::new(AtomicBool::new(false)); let (tx, rx) = mpsc::channel::(); - let print_thread = thread::spawn(move || -> io::Result<()> { + let print_thread = thread::spawn(move || -> Result<()> { for subject in rx.iter() { path_printer.write_path(subject.path())?; } @@ -271,8 +277,8 @@ fn files_parallel(args: &Args) -> Result { // A broken pipe means graceful termination, so fall through. // Otherwise, something bad happened while writing to stdout, so bubble // it up. - if err.kind() != io::ErrorKind::BrokenPipe { - return Err(err.into()); + if !err.is_broken_pipe() { + return Err(err); } } Ok(matched.load(SeqCst)) @@ -280,22 +286,29 @@ fn files_parallel(args: &Args) -> Result { /// The top-level entry point for --type-list. fn types(args: &Args) -> Result { + write_types(args.stdout(), &args.type_defs()?) + .eager_context(err::WriteTypes) +} + +fn write_types( + mut wtr: W, + defs: &[ignore::types::FileTypeDef], +) -> io::Result { let mut count = 0; - let mut stdout = args.stdout(); - for def in args.type_defs()? { + for def in defs { count += 1; - stdout.write_all(def.name().as_bytes())?; - stdout.write_all(b": ")?; + wtr.write_all(def.name().as_bytes())?; + wtr.write_all(b": ")?; let mut first = true; for glob in def.globs() { if !first { - stdout.write_all(b", ")?; + wtr.write_all(b", ")?; } - stdout.write_all(glob.as_bytes())?; + wtr.write_all(glob.as_bytes())?; first = false; } - stdout.write_all(b"\n")?; + wtr.write_all(b"\n")?; } Ok(count > 0) } @@ -303,7 +316,7 @@ fn types(args: &Args) -> Result { /// The top-level entry point for --pcre2-version. fn pcre2_version(args: &Args) -> Result { #[cfg(feature = "pcre2")] - fn imp(args: &Args) -> Result { + fn imp(args: &Args) -> io::Result { use grep::pcre2; let mut stdout = args.stdout(); @@ -318,11 +331,11 @@ fn pcre2_version(args: &Args) -> Result { } #[cfg(not(feature = "pcre2"))] - fn imp(args: &Args) -> Result { + fn imp(args: &Args) -> io::Result { let mut stdout = args.stdout(); writeln!(stdout, "PCRE2 is not available in this build of ripgrep.")?; Ok(false) } - imp(args) + imp(args).eager_context(err::PCRE2Version) } diff --git a/src/path_printer.rs b/src/path_printer.rs index 324a27c4..6307c288 100644 --- a/src/path_printer.rs +++ b/src/path_printer.rs @@ -2,8 +2,11 @@ use std::io; use std::path::Path; use grep::printer::{ColorSpecs, PrinterPath}; +use snafu::ResultExt; use termcolor::WriteColor; +use crate::err::{self, Result}; + /// A configuration for describing how paths should be written. #[derive(Clone, Debug)] struct Config { @@ -87,7 +90,11 @@ pub struct PathPrinter { impl PathPrinter { /// Write the given path to the underlying writer. - pub fn write_path(&mut self, path: &Path) -> io::Result<()> { + pub fn write_path(&mut self, path: &Path) -> Result<()> { + self.write(path).eager_context(err::IO) + } + + fn write(&mut self, path: &Path) -> io::Result<()> { let ppath = PrinterPath::with_separator(path, self.config.separator); if !self.wtr.supports_color() { self.wtr.write_all(ppath.as_bytes())?; diff --git a/tests/regression.rs b/tests/regression.rs index 40a84654..f54c73be 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -392,8 +392,8 @@ rgtest!(r428_unrecognized_style, |_: Dir, mut cmd: TestCommand| { let output = cmd.cmd().output().unwrap(); let stderr = String::from_utf8_lossy(&output.stderr); let expected = "\ -unrecognized style attribute ''. Choose from: nobold, bold, nointense, \ -intense, nounderline, underline. +invalid --colors spec: unrecognized style attribute ''. \ +Choose from: nobold, bold, nointense, intense, nounderline, underline. "; eqnice!(expected, stderr); }); -- cgit v1.2.3