diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8955fe8..6652ded57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +0.5.1 (2017-04-09) +================== +Feature enhancements: + +* Added or improved file type filtering for vim. +* [FEATURE #34](https://github.com/BurntSushi/ripgrep/issues/34): + Add a `-o/--only-matching` flag. +* [FEATURE #377](https://github.com/BurntSushi/ripgrep/issues/377): + Column numbers can now be customized with a color. (The default is + no color.) +* [FEATURE #419](https://github.com/BurntSushi/ripgrep/issues/419): + Added `-0` short flag option for `--null`. + +Bug fixes: + +* [BUG #381](https://github.com/BurntSushi/ripgrep/issues/381): + Include license text in all subcrates. +* [BUG #418](https://github.com/BurntSushi/ripgrep/issues/418), + [BUG #426](https://github.com/BurntSushi/ripgrep/issues/426), + [BUG #439](https://github.com/BurntSushi/ripgrep/issues/439): + Fix a few bugs with `-h/--help` output. + + 0.5.0 (2017-03-12) ================== This is a new minor version release of ripgrep that includes one minor breaking diff --git a/Cargo.lock b/Cargo.lock index 5d11c809e..eb5cb7e98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,15 +1,15 @@ [root] name = "ripgrep" -version = "0.4.0" +version = "0.5.1" dependencies = [ "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "bytecount 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.23.1 (registry+https://github.com/rust-lang/crates.io-index)", "encoding_rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "grep 0.1.6", - "ignore 0.1.8", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ignore 0.1.9", + "lazy_static 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -17,12 +17,12 @@ dependencies = [ "num_cpus 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", - "termcolor 0.3.1", + "termcolor 0.3.2", ] [[package]] name = "aho-corasick" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "bitflags" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -63,12 +63,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "clap" -version = "2.21.1" +version = "2.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", "atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)", "strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "term_size 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", "unicode-segmentation 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -116,7 +116,7 @@ dependencies = [ name = "globset" version = "0.1.4" dependencies = [ - "aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -135,11 +135,11 @@ dependencies = [ [[package]] name = "ignore" -version = "0.1.8" +version = "0.1.9" dependencies = [ "crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)", "globset 0.1.4", - "lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -158,7 +158,7 @@ dependencies = [ [[package]] name = "lazy_static" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -203,7 +203,7 @@ name = "regex" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)", "memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -247,7 +247,7 @@ dependencies = [ [[package]] name = "termcolor" -version = "0.3.1" +version = "0.3.2" dependencies = [ "wincolor 0.1.3", ] @@ -332,20 +332,20 @@ dependencies = [ ] [metadata] -"checksum aho-corasick 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0638fd549427caa90c499814196d1b9e3725eb4d15d7339d6de073a680ed0ca2" +"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699" "checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6" "checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" -"checksum bitflags 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e1ab483fc81a8143faa7203c4a3c02888ebd1a782e37e41fa34753ba9a162" +"checksum bitflags 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1370e9fc2a6ae53aea8b7a5110edbd08836ed87c88736dfabccade1c2b44bff4" "checksum bytecount 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1e8f09fbc8c6726a4b616dcfbd4f54491068d6bb1b93ac03c78ac18ff9a5924a" "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" -"checksum clap 2.21.1 (registry+https://github.com/rust-lang/crates.io-index)" = "74a80f603221c9cd9aa27a28f52af452850051598537bb6b359c38a7d61e5cda" +"checksum clap 2.23.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d480c39a2e5f9b3a3798c661613e1b0e7a7ae71e005102d4aa910fc3289df484" "checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97" "checksum encoding_rs 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a1cca0a26f904955d80d70b9bff1019e4f4cbc06f2fcbccf8bd3d889cc1c9b7" "checksum env_logger 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e3856f1697098606fc6cb97a93de88ca3f3bc35bb878c725920e6e82ecf05e83" "checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344" "checksum fs2 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "34edaee07555859dc13ca387e6ae05686bb4d0364c95d649b6dab959511f4baf" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" -"checksum lazy_static 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7291b1dd97d331f752620b02dfdbc231df7fc01bf282a00769e1cdb963c460dc" +"checksum lazy_static 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2f61b8421c7a4648c391611625d56fdd5c7567da05af1be655fd8cacc643abb3" "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" "checksum log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "5141eca02775a762cc6cd564d8d2c50f67c0ea3a372cbf1c51592b3e029e10ad" "checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4" diff --git a/Cargo.toml b/Cargo.toml index d080577d3..c512f5505 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ripgrep" -version = "0.5.0" #:version +version = "0.5.1" #:version authors = ["Andrew Gallant "] description = """ Line oriented search tool using Rust's regex library. Combines the raw @@ -28,11 +28,11 @@ path = "tests/tests.rs" [dependencies] atty = "0.2.2" bytecount = "0.1.4" -clap = "2.20.5" +clap = "2.23.1" encoding_rs = "0.5.0" env_logger = { version = "0.4", default-features = false } grep = { version = "0.1.5", path = "grep" } -ignore = { version = "0.1.7", path = "ignore" } +ignore = { version = "0.1.9", path = "ignore" } lazy_static = "0.2" libc = "0.2" log = "0.3" @@ -44,7 +44,7 @@ same-file = "0.1.1" termcolor = { version = "0.3.0", path = "termcolor" } [build-dependencies] -clap = "2.18" +clap = "2.23.1" lazy_static = "0.2" [features] diff --git a/README.md b/README.md index c8d0c4ccf..48259b70b 100644 --- a/README.md +++ b/README.md @@ -230,11 +230,10 @@ colorize your output and show line numbers, just like The Silver Searcher. Coloring works on Windows too! Colors can be controlled more granularly with the `--color` flag. -One last thing before we get started: `ripgrep` assumes UTF-8 *everywhere*. It -can still search files that are invalid UTF-8 (like, say, latin-1), but it will -simply not work on UTF-16 encoded files or other more exotic encodings. -[Support for other encodings may -happen.](https://github.com/BurntSushi/ripgrep/issues/1) +One last thing before we get started: generally speaking, `ripgrep` assumes the +input is reading is UTF-8. However, if ripgrep notices a file is encoded as +UTF-16, then it will know how to search it. For other encodings, you'll need to +explicitly specify them with the `-E/--encoding` flag. To recursively search the current directory, while respecting all `.gitignore` files, ignore hidden files and directories and skip binary files: @@ -379,6 +378,51 @@ $ cargo test from the repository root. +### Tips + +#### Windows Powershell + +##### Powershell Profile + +To customize powershell on start-up there is a special powershell script that has to be created. +In order to find its location run command `Get-Command $profile | Select-Object -ExpandProperty Definition` +See [more](https://technet.microsoft.com/en-us/library/bb613488(v=vs.85).aspx) for profile details. + +Any powershell code in this file gets evaluated at the start of console. +This way you can have own aliases to be created at start. + +##### Setup function alias + +Often you can find a need to make alias for the favourite utility. + +But powershell function aliases do not behave like your typical linux shell alias. + +You always need to propagate arguments and **Stdin** input. +But it cannot be done simply as `function grep() { $input | rg.exe --hidden $args }` + +Use below example as reference to how setup alias in powershell. + +```powershell +function grep { + $count = @($input).Count + $input.Reset() + + if ($count) { + $input | rg.exe --hidden $args + } + else { + rg.exe --hidden $args + } +} +``` + +Powershell special variables: +* input - is powershell **Stdin** object that allows you to access its content. +* args - is array of arguments passed to this function. + +This alias checks whether there is **Stdin** input and propagates only if there is some lines. +Otherwise empty `$input` will make powershell to trigger `rg` to search empty **Stdin** + ### Known issues #### I just hit Ctrl+C in the middle of ripgrep's output and now my terminal's foreground color is wrong! diff --git a/appveyor.yml b/appveyor.yml index 43adc42c3..d2de7e3b2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,7 +40,7 @@ before_deploy: - cargo build --release - mkdir staging - copy target\release\rg.exe staging - - copy target\release\build\ripgrep-*\out\_rg.ps1 staging + - ps: copy target\release\build\ripgrep-*\out\_rg.ps1 staging - cd staging # release zipfile will look like 'rust-everywhere-v1.2.3-x86_64-pc-windows-msvc' - 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip * diff --git a/build.rs b/build.rs index 8a7c49007..d084eac8b 100644 --- a/build.rs +++ b/build.rs @@ -19,7 +19,7 @@ fn main() { }; fs::create_dir_all(&outdir).unwrap(); - let mut app = app::app_short(); + let mut app = app::app(); app.gen_completions("rg", Shell::Bash, &outdir); app.gen_completions("rg", Shell::Fish, &outdir); app.gen_completions("rg", Shell::Zsh, &outdir); diff --git a/ci/install.sh b/ci/install.sh index 3b3f28d47..a4d8c7b43 100644 --- a/ci/install.sh +++ b/ci/install.sh @@ -17,9 +17,6 @@ install_c_toolchain() { } install_rustup() { - # uninstall the rust toolchain installed by travis, we are going to use rustup - sh ~/rust/lib/rustlib/uninstall.sh - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain=$TRAVIS_RUST_VERSION rustc -V diff --git a/doc/rg.1 b/doc/rg.1 index e5d4323c8..5e068c28a 100644 --- a/doc/rg.1 +++ b/doc/rg.1 @@ -181,7 +181,7 @@ Styles are limited to nobold, bold, nointense or intense. .RS .PP The format of the flag is {type}:{attribute}:{value}. -{type} should be one of path, line or match. +{type} should be one of path, line, column or match. {attribute} can be fg, bg or style. Value is either a color (for fg and bg) or a text style. A special format, {type}:none, will clear all color settings for {type}. @@ -299,6 +299,13 @@ Follow symlinks. .RS .RE .TP +.B \-M, \-\-max\-columns \f[I]NUM\f[] +Don\[aq]t print lines longer than this limit in bytes. +Longer lines are omitted, and only the number of matches in that line is +printed. +.RS +.RE +.TP .B \-m, \-\-max\-count \f[I]NUM\f[] Limit the number of matching lines per file searched to NUM. .RS @@ -355,7 +362,7 @@ Note that .ignore files will continue to be respected. .RS .RE .TP -.B \-\-null +.B \-0, \-\-null Whenever a file name is printed, follow it with a NUL byte. This includes printing filenames before matches, and when printing a list of matching files such as with \-\-count, \-\-files\-with\-matches @@ -363,6 +370,12 @@ and \-\-files. .RS .RE .TP +.B \-o, \-\-only\-matching +Print only the matched (non\-empty) parts of a matching line, with each +such part on a separate output line. +.RS +.RE +.TP .B \-\-path\-separator \f[I]SEPARATOR\f[] The path separator to use when printing file paths. This defaults to your platform\[aq]s path separator, which is / on Unix diff --git a/doc/rg.1.md b/doc/rg.1.md index 8e8ca2892..1cd9c771f 100644 --- a/doc/rg.1.md +++ b/doc/rg.1.md @@ -123,9 +123,9 @@ Project home page: https://github.com/BurntSushi/ripgrep black. Styles are limited to nobold, bold, nointense or intense. The format of the flag is {type}:{attribute}:{value}. {type} should be one - of path, line or match. {attribute} can be fg, bg or style. Value is either - a color (for fg and bg) or a text style. A special format, {type}:none, - will clear all color settings for {type}. + of path, line, column or match. {attribute} can be fg, bg or style. Value + is either a color (for fg and bg) or a text style. A special format, + {type}:none, will clear all color settings for {type}. For example, the following command will change the match color to magenta and the background color for line numbers to yellow: @@ -243,12 +243,16 @@ Project home page: https://github.com/BurntSushi/ripgrep : Don't respect version control ignore files (e.g., .gitignore). Note that .ignore files will continue to be respected. ---null +-0, --null : Whenever a file name is printed, follow it with a NUL byte. This includes printing filenames before matches, and when printing a list of matching files such as with --count, --files-with-matches and --files. +-o, --only-matching +: Print only the matched (non-empty) parts of a matching line, with each such + part on a separate output line. + --path-separator *SEPARATOR* : The path separator to use when printing file paths. This defaults to your platform's path separator, which is / on Unix and \\ on Windows. This flag is diff --git a/globset/src/glob.rs b/globset/src/glob.rs index fbfc7d9ba..bb7b0602e 100644 --- a/globset/src/glob.rs +++ b/globset/src/glob.rs @@ -9,7 +9,7 @@ use std::str; use regex; use regex::bytes::Regex; -use {Candidate, Error, new_regex}; +use {Candidate, Error, ErrorKind, new_regex}; /// Describes a matching strategy for a particular pattern. /// @@ -544,6 +544,7 @@ impl<'a> GlobBuilder<'a> { /// Parses and builds the pattern. pub fn build(&self) -> Result { let mut p = Parser { + glob: &self.glob, stack: vec![Tokens::default()], chars: self.glob.chars().peekable(), prev: None, @@ -551,9 +552,15 @@ impl<'a> GlobBuilder<'a> { }; try!(p.parse()); if p.stack.is_empty() { - Err(Error::UnopenedAlternates) + Err(Error { + glob: Some(self.glob.to_string()), + kind: ErrorKind::UnopenedAlternates, + }) } else if p.stack.len() > 1 { - Err(Error::UnclosedAlternates) + Err(Error { + glob: Some(self.glob.to_string()), + kind: ErrorKind::UnclosedAlternates, + }) } else { let tokens = p.stack.pop().unwrap(); Ok(Glob { @@ -698,6 +705,7 @@ fn bytes_to_escaped_literal(bs: &[u8]) -> String { } struct Parser<'a> { + glob: &'a str, stack: Vec, chars: iter::Peekable>, prev: Option, @@ -705,6 +713,10 @@ struct Parser<'a> { } impl<'a> Parser<'a> { + fn error(&self, kind: ErrorKind) -> Error { + Error { glob: Some(self.glob.to_string()), kind: kind } + } + fn parse(&mut self) -> Result<(), Error> { while let Some(c) = self.bump() { match c { @@ -729,7 +741,7 @@ impl<'a> Parser<'a> { fn push_alternate(&mut self) -> Result<(), Error> { if self.stack.len() > 1 { - return Err(Error::NestedAlternates); + return Err(self.error(ErrorKind::NestedAlternates)); } Ok(self.stack.push(Tokens::default())) } @@ -743,22 +755,22 @@ impl<'a> Parser<'a> { } fn push_token(&mut self, tok: Token) -> Result<(), Error> { - match self.stack.last_mut() { - None => Err(Error::UnopenedAlternates), - Some(ref mut pat) => Ok(pat.push(tok)), + if let Some(ref mut pat) = self.stack.last_mut() { + return Ok(pat.push(tok)); } + Err(self.error(ErrorKind::UnopenedAlternates)) } fn pop_token(&mut self) -> Result { - match self.stack.last_mut() { - None => Err(Error::UnopenedAlternates), - Some(ref mut pat) => Ok(pat.pop().unwrap()), + if let Some(ref mut pat) = self.stack.last_mut() { + return Ok(pat.pop().unwrap()); } + Err(self.error(ErrorKind::UnopenedAlternates)) } fn have_tokens(&self) -> Result { match self.stack.last() { - None => Err(Error::UnopenedAlternates), + None => Err(self.error(ErrorKind::UnopenedAlternates)), Some(ref pat) => Ok(!pat.is_empty()), } } @@ -785,7 +797,7 @@ impl<'a> Parser<'a> { try!(self.push_token(Token::RecursivePrefix)); let next = self.bump(); if !next.map(is_separator).unwrap_or(true) { - return Err(Error::InvalidRecursive); + return Err(self.error(ErrorKind::InvalidRecursive)); } return Ok(()); } @@ -793,7 +805,7 @@ impl<'a> Parser<'a> { if !prev.map(is_separator).unwrap_or(false) { if self.stack.len() <= 1 || (prev != Some(',') && prev != Some('{')) { - return Err(Error::InvalidRecursive); + return Err(self.error(ErrorKind::InvalidRecursive)); } } match self.chars.peek() { @@ -808,18 +820,22 @@ impl<'a> Parser<'a> { assert!(self.bump().map(is_separator).unwrap_or(false)); self.push_token(Token::RecursiveZeroOrMore) } - _ => Err(Error::InvalidRecursive), + _ => Err(self.error(ErrorKind::InvalidRecursive)), } } fn parse_class(&mut self) -> Result<(), Error> { fn add_to_last_range( + glob: &str, r: &mut (char, char), add: char, ) -> Result<(), Error> { r.1 = add; if r.1 < r.0 { - Err(Error::InvalidRange(r.0, r.1)) + Err(Error { + glob: Some(glob.to_string()), + kind: ErrorKind::InvalidRange(r.0, r.1), + }) } else { Ok(()) } @@ -837,7 +853,7 @@ impl<'a> Parser<'a> { Some(c) => c, // The only way to successfully break this loop is to observe // a ']'. - None => return Err(Error::UnclosedClass), + None => return Err(self.error(ErrorKind::UnclosedClass)), }; match c { ']' => { @@ -854,7 +870,7 @@ impl<'a> Parser<'a> { // invariant: in_range is only set when there is // already at least one character seen. let r = ranges.last_mut().unwrap(); - try!(add_to_last_range(r, '-')); + try!(add_to_last_range(&self.glob, r, '-')); in_range = false; } else { assert!(!ranges.is_empty()); @@ -865,7 +881,8 @@ impl<'a> Parser<'a> { if in_range { // invariant: in_range is only set when there is // already at least one character seen. - try!(add_to_last_range(ranges.last_mut().unwrap(), c)); + try!(add_to_last_range( + &self.glob, ranges.last_mut().unwrap(), c)); } else { ranges.push((c, c)); } @@ -909,7 +926,7 @@ fn ends_with(needle: &[u8], haystack: &[u8]) -> bool { mod tests { use std::ffi::{OsStr, OsString}; - use {GlobSetBuilder, Error}; + use {GlobSetBuilder, ErrorKind}; use super::{Glob, GlobBuilder, Token}; use super::Token::*; @@ -934,7 +951,7 @@ mod tests { #[test] fn $name() { let err = Glob::new($pat).unwrap_err(); - assert_eq!($err, err); + assert_eq!(&$err, err.kind()); } } } @@ -1057,19 +1074,19 @@ mod tests { syntax!(cls18, "[!0-9a-z]", vec![rclassn(&[('0', '9'), ('a', 'z')])]); syntax!(cls19, "[!a-z0-9]", vec![rclassn(&[('a', 'z'), ('0', '9')])]); - syntaxerr!(err_rseq1, "a**", Error::InvalidRecursive); - syntaxerr!(err_rseq2, "**a", Error::InvalidRecursive); - syntaxerr!(err_rseq3, "a**b", Error::InvalidRecursive); - syntaxerr!(err_rseq4, "***", Error::InvalidRecursive); - syntaxerr!(err_rseq5, "/a**", Error::InvalidRecursive); - syntaxerr!(err_rseq6, "/**a", Error::InvalidRecursive); - syntaxerr!(err_rseq7, "/a**b", Error::InvalidRecursive); - syntaxerr!(err_unclosed1, "[", Error::UnclosedClass); - syntaxerr!(err_unclosed2, "[]", Error::UnclosedClass); - syntaxerr!(err_unclosed3, "[!", Error::UnclosedClass); - syntaxerr!(err_unclosed4, "[!]", Error::UnclosedClass); - syntaxerr!(err_range1, "[z-a]", Error::InvalidRange('z', 'a')); - syntaxerr!(err_range2, "[z--]", Error::InvalidRange('z', '-')); + syntaxerr!(err_rseq1, "a**", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq2, "**a", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq3, "a**b", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq4, "***", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq5, "/a**", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq6, "/**a", ErrorKind::InvalidRecursive); + syntaxerr!(err_rseq7, "/a**b", ErrorKind::InvalidRecursive); + syntaxerr!(err_unclosed1, "[", ErrorKind::UnclosedClass); + syntaxerr!(err_unclosed2, "[]", ErrorKind::UnclosedClass); + syntaxerr!(err_unclosed3, "[!", ErrorKind::UnclosedClass); + syntaxerr!(err_unclosed4, "[!]", ErrorKind::UnclosedClass); + syntaxerr!(err_range1, "[z-a]", ErrorKind::InvalidRange('z', 'a')); + syntaxerr!(err_range2, "[z--]", ErrorKind::InvalidRange('z', '-')); const CASEI: Options = Options { casei: true, diff --git a/globset/src/lib.rs b/globset/src/lib.rs index ea118c73d..980807057 100644 --- a/globset/src/lib.rs +++ b/globset/src/lib.rs @@ -128,7 +128,16 @@ mod pathutil; /// Represents an error that can occur when parsing a glob pattern. #[derive(Clone, Debug, Eq, PartialEq)] -pub enum Error { +pub struct Error { + /// The original glob provided by the caller. + glob: Option, + /// The kind of error. + kind: ErrorKind, +} + +/// The kind of error that can occur when parsing a glob pattern. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ErrorKind { /// Occurs when a use of `**` is invalid. Namely, `**` can only appear /// adjacent to a path separator, or the beginning/end of a glob. InvalidRecursive, @@ -150,45 +159,74 @@ pub enum Error { } impl StdError for Error { + fn description(&self) -> &str { + self.kind.description() + } +} + +impl Error { + /// Return the glob that caused this error, if one exists. + pub fn glob(&self) -> Option<&str> { + self.glob.as_ref().map(|s| &**s) + } + + /// Return the kind of this error. + pub fn kind(&self) -> &ErrorKind { + &self.kind + } +} + +impl ErrorKind { fn description(&self) -> &str { match *self { - Error::InvalidRecursive => { + ErrorKind::InvalidRecursive => { "invalid use of **; must be one path component" } - Error::UnclosedClass => { + ErrorKind::UnclosedClass => { "unclosed character class; missing ']'" } - Error::InvalidRange(_, _) => { + ErrorKind::InvalidRange(_, _) => { "invalid character range" } - Error::UnopenedAlternates => { + ErrorKind::UnopenedAlternates => { "unopened alternate group; missing '{' \ (maybe escape '}' with '[}]'?)" } - Error::UnclosedAlternates => { + ErrorKind::UnclosedAlternates => { "unclosed alternate group; missing '}' \ (maybe escape '{' with '[{]'?)" } - Error::NestedAlternates => { + ErrorKind::NestedAlternates => { "nested alternate groups are not allowed" } - Error::Regex(ref err) => err, + ErrorKind::Regex(ref err) => err, } } } impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.glob { + None => self.kind.fmt(f), + Some(ref glob) => { + write!(f, "error parsing glob '{}': {}", glob, self.kind) + } + } + } +} + +impl fmt::Display for ErrorKind { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { - Error::InvalidRecursive - | Error::UnclosedClass - | Error::UnopenedAlternates - | Error::UnclosedAlternates - | Error::NestedAlternates - | Error::Regex(_) => { + ErrorKind::InvalidRecursive + | ErrorKind::UnclosedClass + | ErrorKind::UnopenedAlternates + | ErrorKind::UnclosedAlternates + | ErrorKind::NestedAlternates + | ErrorKind::Regex(_) => { write!(f, "{}", self.description()) } - Error::InvalidRange(s, e) => { + ErrorKind::InvalidRange(s, e) => { write!(f, "invalid range; '{}' > '{}'", s, e) } } @@ -201,12 +239,22 @@ fn new_regex(pat: &str) -> Result { .size_limit(10 * (1 << 20)) .dfa_size_limit(10 * (1 << 20)) .build() - .map_err(|err| Error::Regex(err.to_string())) + .map_err(|err| { + Error { + glob: Some(pat.to_string()), + kind: ErrorKind::Regex(err.to_string()), + } + }) } fn new_regex_set(pats: I) -> Result where S: AsRef, I: IntoIterator { - RegexSet::new(pats).map_err(|err| Error::Regex(err.to_string())) + RegexSet::new(pats).map_err(|err| { + Error { + glob: None, + kind: ErrorKind::Regex(err.to_string()), + } + }) } type Fnv = hash::BuildHasherDefault; diff --git a/ignore/Cargo.toml b/ignore/Cargo.toml index d73e0d003..ea03ca735 100644 --- a/ignore/Cargo.toml +++ b/ignore/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ignore" -version = "0.1.8" #:version +version = "0.1.9" #:version authors = ["Andrew Gallant "] description = """ A fast library for efficiently matching ignore files such as `.gitignore` diff --git a/ignore/src/gitignore.rs b/ignore/src/gitignore.rs index 4972dcd53..68655261c 100644 --- a/ignore/src/gitignore.rs +++ b/ignore/src/gitignore.rs @@ -279,7 +279,12 @@ impl GitignoreBuilder { let nignore = self.globs.iter().filter(|g| !g.is_whitelist()).count(); let nwhite = self.globs.iter().filter(|g| g.is_whitelist()).count(); let set = try!( - self.builder.build().map_err(|err| Error::Glob(err.to_string()))); + self.builder.build().map_err(|err| { + Error::Glob { + glob: None, + err: err.to_string(), + } + })); Ok(Gitignore { set: set, root: self.root.clone(), @@ -420,7 +425,12 @@ impl GitignoreBuilder { GlobBuilder::new(&glob.actual) .literal_separator(literal_separator) .build() - .map_err(|err| Error::Glob(err.to_string()))); + .map_err(|err| { + Error::Glob { + glob: Some(glob.original.clone()), + err: err.kind().to_string(), + } + })); self.builder.add(parsed); self.globs.push(glob); Ok(self) diff --git a/ignore/src/lib.rs b/ignore/src/lib.rs index c93a4de73..d053014cc 100644 --- a/ignore/src/lib.rs +++ b/ignore/src/lib.rs @@ -112,7 +112,17 @@ pub enum Error { /// An error that occurs when doing I/O, such as reading an ignore file. Io(io::Error), /// An error that occurs when trying to parse a glob. - Glob(String), + Glob { + /// The original glob that caused this error. This glob, when + /// available, always corresponds to the glob provided by an end user. + /// e.g., It is the glob as writtein in a `.gitignore` file. + /// + /// (This glob may be distinct from the glob that is actually + /// compiled, after accounting for `gitignore` semantics.) + glob: Option, + /// The underlying glob error as a string. + err: String, + }, /// A type selection for a file type that is not defined. UnrecognizedFileType(String), /// A user specified file type definition could not be parsed. @@ -144,7 +154,7 @@ impl Error { Error::WithDepth { ref err, .. } => err.is_io(), Error::Loop { .. } => false, Error::Io(_) => true, - Error::Glob(_) => false, + Error::Glob { .. } => false, Error::UnrecognizedFileType(_) => false, Error::InvalidDefinition => false, } @@ -199,7 +209,7 @@ impl error::Error for Error { Error::WithDepth { ref err, .. } => err.description(), Error::Loop { .. } => "file system loop found", Error::Io(ref err) => err.description(), - Error::Glob(ref msg) => msg, + Error::Glob { ref err, .. } => err, Error::UnrecognizedFileType(_) => "unrecognized file type", Error::InvalidDefinition => "invalid definition", } @@ -227,7 +237,10 @@ impl fmt::Display for Error { child.display(), ancestor.display()) } Error::Io(ref err) => err.fmt(f), - Error::Glob(ref msg) => write!(f, "{}", msg), + Error::Glob { glob: None, ref err } => write!(f, "{}", err), + Error::Glob { glob: Some(ref glob), ref err } => { + write!(f, "error parsing glob '{}': {}", glob, err) + } Error::UnrecognizedFileType(ref ty) => { write!(f, "unrecognized file type: {}", ty) } diff --git a/ignore/src/types.rs b/ignore/src/types.rs index f35156000..2b8c1ddb2 100644 --- a/ignore/src/types.rs +++ b/ignore/src/types.rs @@ -155,6 +155,7 @@ const DEFAULT_TYPES: &'static [(&'static str, &'static [&'static str])] = &[ ("mk", &["mkfile"]), ("ml", &["*.ml"]), ("nim", &["*.nim"]), + ("nix", &["*.nix"]), ("objc", &["*.h", "*.m"]), ("objcpp", &["*.h", "*.mm"]), ("ocaml", &["*.ml", "*.mli", "*.mll", "*.mly"]), @@ -191,6 +192,7 @@ const DEFAULT_TYPES: &'static [(&'static str, &'static [&'static str])] = &[ ("twig", &["*.twig"]), ("vala", &["*.vala"]), ("vb", &["*.vb"]), + ("vim", &["*.vim"]), ("vimscript", &["*.vim"]), ("wiki", &["*.mediawiki", "*.wiki"]), ("xml", &["*.xml"]), @@ -446,13 +448,18 @@ impl TypesBuilder { GlobBuilder::new(glob) .literal_separator(true) .build() - .map_err(|err| Error::Glob(err.to_string())))); + .map_err(|err| { + Error::Glob { + glob: Some(glob.to_string()), + err: err.kind().to_string(), + } + }))); glob_to_selection.push((isel, iglob)); } selections.push(selection.clone().map(move |_| def)); } let set = try!(build_set.build().map_err(|err| { - Error::Glob(err.to_string()) + Error::Glob { glob: None, err: err.to_string() } })); Ok(Types { defs: defs, diff --git a/pkg/brew/ripgrep-bin.rb b/pkg/brew/ripgrep-bin.rb index 014e69063..6f0d760d9 100644 --- a/pkg/brew/ripgrep-bin.rb +++ b/pkg/brew/ripgrep-bin.rb @@ -1,16 +1,16 @@ class RipgrepBin < Formula - version '0.4.0' + version '0.5.1' desc "Search tool like grep and The Silver Searcher." homepage "https://github.com/BurntSushi/ripgrep" url "https://github.com/BurntSushi/ripgrep/releases/download/#{version}/ripgrep-#{version}-x86_64-apple-darwin.tar.gz" - sha256 "6ac71251909227f8ef7eda27d3080c954843f3665b81e455362c90b2a9c4734a" + sha256 "517e40823f151ceb85840c8f147206360ee093aecbe96de20be4c2d5517d51ca" conflicts_with "ripgrep" def install bin.install "rg" man1.install "rg.1" - + bash_completion.install "complete/rg.bash-completion" fish_completion.install "complete/rg.fish" zsh_completion.install "complete/_rg" diff --git a/src/app.rs b/src/app.rs index d327e27ca..44e49517e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,16 +32,6 @@ ARGS: OPTIONS: {unified}"; -/// Build a clap application with short help strings. -pub fn app_short() -> App<'static, 'static> { - app(false, |k| USAGES[k].short) -} - -/// Build a clap application with long help strings. -pub fn app_long() -> App<'static, 'static> { - app(true, |k| USAGES[k].long) -} - /// Build a clap application parameterized by usage strings. /// /// The function given should take a clap argument name and return a help @@ -49,10 +39,11 @@ pub fn app_long() -> App<'static, 'static> { /// /// This is an intentionally stand-alone module so that it can be used easily /// in a `build.rs` script to build shell completion files. -fn app(next_line_help: bool, doc: F) -> App<'static, 'static> - where F: Fn(&'static str) -> &'static str { +pub fn app() -> App<'static, 'static> { let arg = |name| { - Arg::with_name(name).help(doc(name)).next_line_help(next_line_help) + Arg::with_name(name) + .help(USAGES[name].short) + .long_help(USAGES[name].long) }; let flag = |name| arg(name).long(name); @@ -64,11 +55,7 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .setting(AppSettings::UnifiedHelpMessage) .usage(USAGE) .template(TEMPLATE) - // Handle help/version manually to make their output formatting - // consistent with short/long views. - .arg(arg("help-short").short("h")) - .arg(flag("help")) - .arg(arg("ripgrep-version").long("version").short("V")) + .help_message("Prints help information. Use --help for more details.") // First, set up primary positional/flag arguments. .arg(arg("pattern") .required_unless_one(&[ @@ -129,6 +116,8 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .arg(flag("column")) .arg(flag("context-separator") .value_name("SEPARATOR").takes_value(true)) + .arg(flag("dfa-size-limit") + .value_name("NUM+SUFFIX?").takes_value(true)) .arg(flag("debug")) .arg(flag("file").short("f") .value_name("FILE").takes_value(true) @@ -158,10 +147,13 @@ fn app(next_line_help: bool, doc: F) -> App<'static, 'static> .arg(flag("no-ignore")) .arg(flag("no-ignore-parent")) .arg(flag("no-ignore-vcs")) - .arg(flag("null")) + .arg(flag("null").short("0")) + .arg(flag("only-matching").short("o").conflicts_with("replace")) .arg(flag("path-separator").value_name("SEPARATOR").takes_value(true)) .arg(flag("pretty").short("p")) .arg(flag("replace").short("r").value_name("ARG").takes_value(true)) + .arg(flag("regex-size-limit") + .value_name("NUM+SUFFIX?").takes_value(true)) .arg(flag("case-sensitive").short("s")) .arg(flag("smart-case").short("S")) .arg(flag("sort-files")) @@ -249,12 +241,12 @@ lazy_static! { red, blue, green, cyan, magenta, yellow, white and black. \ Styles are limited to nobold, bold, nointense or intense.\n\n\ The format of the flag is {type}:{attribute}:{value}. {type} \ - should be one of path, line or match. {attribute} can be fg, bg \ - or style. {value} is either a color (for fg and bg) or a text \ - style. A special format, {type}:none, will clear all color \ - settings for {type}.\n\nFor example, the following command will \ - change the match color to magenta and the background color for \ - line numbers to yellow:\n\n\ + should be one of path, line, column or match. {attribute} can \ + be fg, bg or style. {value} is either a color (for fg and bg) \ + or a text style. A special format, {type}:none, will clear all \ + color settings for {type}.\n\nFor example, the following \ + command will change the match color to magenta and the \ + background color for line numbers to yellow:\n\n\ rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo."); doc!(h, "encoding", "Specify the text encoding of files to search.", @@ -340,6 +332,13 @@ lazy_static! { doc!(h, "debug", "Show debug messages.", "Show debug messages. Please use this when filing a bug report."); + doc!(h, "dfa-size-limit", + "The upper size limit of the generated dfa.", + "The upper size limit of the generated dfa. The default limit is \ + 10M. This should only be changed on very large regex inputs \ + where the (slower) fallback regex engine may otherwise be used. \ + \n\nThe argument accepts the same size suffixes as allowed in \ + the 'max-filesize' argument."); doc!(h, "file", "Search for patterns from the given file.", "Search for patterns from the given file, with one pattern per \ @@ -388,10 +387,11 @@ lazy_static! { "Limit the number of matching lines per file searched to NUM."); doc!(h, "max-filesize", "Ignore files larger than NUM in size.", - "Ignore files larger than NUM in size. Does not ignore directories. \ + "Ignore files larger than NUM in size. Does not ignore \ + directories. \ \n\nThe input format accepts suffixes of K, M or G which \ - correspond to kilobytes, megabytes and gigabytes. If no suffix is \ - provided the input is treated as bytes. \ + correspond to kilobytes, megabytes and gigabytes. If no suffix \ + is provided the input is treated as bytes. \ \n\nExample: --max-filesize 50K or --max-filesize 80M"); doc!(h, "maxdepth", "Descend at most NUM directories.", @@ -435,6 +435,10 @@ lazy_static! { printing a list of matching files such as with --count, \ --files-with-matches and --files. This option is useful for use \ with xargs."); + doc!(h, "only-matching", + "Print only matched parts of a line.", + "Print only the matched (non-empty) parts of a matching line, \ + with each such part on a separate output line."); doc!(h, "path-separator", "Path separator to use when printing file paths.", "The path separator to use when printing file paths. This \ @@ -453,6 +457,11 @@ lazy_static! { Note that the replacement by default replaces each match, and \ NOT the entire line. To replace the entire line, you should \ match the entire line."); + doc!(h, "regex-size-limit", + "The upper size limit of the compiled regex.", + "The upper size limit of the compiled regex. The default limit \ + is 10M. \n\nThe argument accepts the same size suffixes as \ + allowed in the 'max-filesize' argument."); doc!(h, "case-sensitive", "Search case sensitively.", "Search case sensitively. This overrides -i/--ignore-case and \ @@ -496,8 +505,8 @@ lazy_static! { permits specifying one or more other type names (separated by a \ comma) that have been defined and its rules will automatically \ be imported into the type specified. For example, to create a \ - type called src that matches C++, Python and Markdown files, one \ - can use:\n\n\ + type called src that matches C++, Python and Markdown files, \ + one can use:\n\n\ --type-add 'src:include:cpp,py,md'\n\n\ Additional glob rules can still be added to the src type by \ using the --type-add flag again:\n\n\ diff --git a/src/args.rs b/src/args.rs index 0b1aaa9a0..9e941a7ba 100644 --- a/src/args.rs +++ b/src/args.rs @@ -5,7 +5,6 @@ use std::fs; use std::io::{self, BufRead}; use std::ops; use std::path::{Path, PathBuf}; -use std::process; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -66,6 +65,7 @@ pub struct Args { no_ignore_vcs: bool, no_messages: bool, null: bool, + only_matching: bool, path_separator: Option, quiet: bool, quiet_matched: QuietMatched, @@ -88,26 +88,7 @@ impl Args { /// /// Also, initialize a global logger. pub fn parse() -> Result { - use clap::ErrorKind::*; - - let matches = match app::app_short().get_matches_safe() { - Ok(matches) => matches, - Err(clap::Error { kind: HelpDisplayed, .. }) => { - let _ = ::app::app_long().print_help(); - println!(""); - process::exit(0); - } - Err(err) => err.exit(), - }; - if matches.is_present("help-short") { - let _ = ::app::app_short().print_help(); - println!(""); - process::exit(0); - } - if matches.is_present("ripgrep-version") { - println!("ripgrep {}", crate_version!()); - process::exit(0); - } + let matches = app::app().get_matches(); let mut logb = env_logger::LogBuilder::new(); if matches.is_present("debug") { @@ -161,6 +142,7 @@ impl Args { .heading(self.heading) .line_per_match(self.line_per_match) .null(self.null) + .only_matching(self.only_matching) .path_separator(self.path_separator) .with_filename(self.with_filename) .max_columns(self.max_columns); @@ -365,6 +347,7 @@ impl<'a> ArgMatches<'a> { no_ignore_vcs: self.no_ignore_vcs(), no_messages: self.is_present("no-messages"), null: self.is_present("null"), + only_matching: self.is_present("only-matching"), path_separator: try!(self.path_separator()), quiet: quiet, quiet_matched: QuietMatched::new(quiet), @@ -754,7 +737,7 @@ impl<'a> ArgMatches<'a> { if label == "auto" { return Ok(None); } - match Encoding::for_label(label.as_bytes()) { + match Encoding::for_label_no_replacement(label.as_bytes()) { Some(enc) => Ok(Some(enc)), None => Err(From::from( format!("unsupported encoding: {}", label))), @@ -788,12 +771,18 @@ impl<'a> ArgMatches<'a> { let casei = self.is_present("ignore-case") && !self.is_present("case-sensitive"); - GrepBuilder::new(&try!(self.pattern())) + let mut gb = GrepBuilder::new(&try!(self.pattern())) .case_smart(smart) .case_insensitive(casei) - .line_terminator(b'\n') - .build() - .map_err(From::from) + .line_terminator(b'\n'); + + if let Some(limit) = try!(self.dfa_size_limit()) { + gb = gb.dfa_size_limit(limit); + } + if let Some(limit) = try!(self.regex_size_limit()) { + gb = gb.size_limit(limit); + } + gb.build().map_err(From::from) } /// Builds the set of glob overrides from the command line flags. @@ -824,31 +813,64 @@ impl<'a> ArgMatches<'a> { btypes.build().map_err(From::from) } - /// Parses the max-filesize argument option into a byte count. - fn max_filesize(&self) -> Result> { - use regex::Regex; - - let max_filesize = match self.value_of_lossy("max-filesize") { + /// Parses an argument of the form `[0-9]+(KMG)?`. + /// + /// This always returns the result as a type `u64`. This must be converted + /// to the appropriate type by the caller. + fn parse_human_readable_size_arg( + &self, + arg_name: &str, + ) -> Result> { + let arg_value = match self.value_of_lossy(arg_name) { Some(x) => x, None => return Ok(None) }; + let re = regex::Regex::new("^([0-9]+)([KMG])?$").unwrap(); + let caps = try!( + re.captures(&arg_value).ok_or_else(|| { + format!("invalid format for {}", arg_name) + })); - let re = Regex::new("^([0-9]+)([KMG])?$").unwrap(); - let caps = try!(re.captures(&max_filesize) - .ok_or("invalid format for max-filesize argument")); - - let value = try!(caps[1].parse::().map_err(|err| err.to_string())); + let value = try!(caps[1].parse::()); let suffix = caps.get(2).map(|x| x.as_str()); + let v_10 = value.checked_mul(1024); + let v_20 = v_10.and_then(|x| x.checked_mul(1024)); + let v_30 = v_20.and_then(|x| x.checked_mul(1024)); + + let try_suffix = |x: Option| { + if x.is_some() { + Ok(x) + } else { + Err(From::from(format!("number too large for {}", arg_name))) + } + }; match suffix { None => Ok(Some(value)), - Some("K") => Ok(Some(value * 1024)), - Some("M") => Ok(Some(value * 1024 * 1024)), - Some("G") => Ok(Some(value * 1024 * 1024 * 1024)), - _ => Err(From::from("invalid suffix for max-filesize argument")) + Some("K") => try_suffix(v_10), + Some("M") => try_suffix(v_20), + Some("G") => try_suffix(v_30), + _ => Err(From::from(format!("invalid suffix for {}", arg_name))) } } + /// Parse the dfa-size-limit argument option into a byte count. + fn dfa_size_limit(&self) -> Result> { + let r = try!(self.parse_human_readable_size_arg("dfa-size-limit")); + human_readable_to_usize("dfa-size-limit", r) + } + + /// Parse the regex-size-limit argument option into a byte count. + fn regex_size_limit(&self) -> Result> { + let r = try!(self.parse_human_readable_size_arg("regex-size-limit")); + human_readable_to_usize("regex-size-limit", r) + } + + /// Parses the max-filesize argument option into a byte count. + fn max_filesize(&self) -> Result> { + self.parse_human_readable_size_arg("max-filesize") + } + /// Returns true if ignore files should be ignored. fn no_ignore(&self) -> bool { self.is_present("no-ignore") @@ -943,6 +965,27 @@ impl QuietMatched { } } +/// Convert the result of a `parse_human_readable_size_arg` call into +/// a `usize`, failing if the type does not fit. +fn human_readable_to_usize( + arg_name: &str, + value: Option, +) -> Result> { + use std::usize; + + match value { + None => Ok(None), + Some(v) => { + if v <= usize::MAX as u64 { + Ok(Some(v as usize)) + } else { + let msg = format!("number too large for {}", arg_name); + Err(From::from(msg)) + } + } + } +} + /// Returns true if and only if stdin is deemed searchable. #[cfg(unix)] fn stdin_is_readable() -> bool { diff --git a/src/decoder.rs b/src/decoder.rs index 345389a5a..d718f55c2 100644 --- a/src/decoder.rs +++ b/src/decoder.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::cmp; use std::io::{self, Read}; @@ -290,10 +288,6 @@ mod tests { use super::{Bom, BomPeeker, DecodeReader}; - fn utf8(bytes: &[u8]) -> &str { - ::std::str::from_utf8(bytes).unwrap() - } - fn read_to_string(mut rdr: R) -> String { let mut s = String::new(); rdr.read_to_string(&mut s).unwrap(); @@ -453,7 +447,8 @@ mod tests { test_trans_simple!(trans_simple_utf16be, "utf-16be", b"\x04\x16", "Ж"); test_trans_simple!(trans_simple_chinese, "chinese", b"\xA7\xA8", "Ж"); test_trans_simple!(trans_simple_korean, "korean", b"\xAC\xA8", "Ж"); - test_trans_simple!(trans_simple_big5_hkscs, "big5-hkscs", b"\xC7\xFA", "Ж"); + test_trans_simple!( + trans_simple_big5_hkscs, "big5-hkscs", b"\xC7\xFA", "Ж"); test_trans_simple!(trans_simple_gbk, "gbk", b"\xA7\xA8", "Ж"); test_trans_simple!(trans_simple_sjis, "sjis", b"\x84\x47", "Ж"); test_trans_simple!(trans_simple_eucjp, "euc-jp", b"\xA7\xA8", "Ж"); diff --git a/src/printer.rs b/src/printer.rs index 809e0d75e..5aaf1475f 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -58,6 +58,8 @@ pub struct Printer { /// Whether to print NUL bytes after a file path instead of new lines /// or `:`. null: bool, + /// Print only the matched (non-empty) parts of a matching line + only_matching: 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. @@ -83,6 +85,7 @@ impl Printer { heading: false, line_per_match: false, null: false, + only_matching: false, replace: None, with_filename: false, colors: ColorSpecs::default(), @@ -144,6 +147,12 @@ impl Printer { self } + /// Print only the matched (non-empty) parts of a matching line + pub fn only_matching(mut self, yes: bool) -> Printer { + self.only_matching = yes; + self + } + /// A separator to use when printing file paths. When empty, use the /// default separator for the current platform. (/ on Unix, \ on Windows.) pub fn path_separator(mut self, sep: Option) -> Printer { @@ -153,9 +162,6 @@ impl Printer { /// 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 @@ -204,22 +210,14 @@ impl Printer { pub fn path>(&mut self, path: P) { let path = strip_prefix("./", path.as_ref()).unwrap_or(path.as_ref()); self.write_path(path); - if self.null { - self.write(b"\x00"); - } else { - self.write_eol(); - } + self.write_path_eol(); } /// Prints the given path and a count of the number of matches found. pub fn path_count>(&mut self, path: P, count: u64) { if self.with_filename { self.write_path(path); - if self.null { - self.write(b"\x00"); - } else { - self.write(b":"); - } + self.write_path_sep(b':'); } self.write(count.to_string().as_bytes()); self.write_eol(); @@ -227,13 +225,11 @@ impl Printer { /// Prints the context separator. pub fn context_separate(&mut self) { - // N.B. We can't use `write` here because of borrowing restrictions. if self.context_separator.is_empty() { return; } - self.has_printed = true; let _ = self.wtr.write_all(&self.context_separator); - let _ = self.wtr.write_all(&[self.eol]); + self.write_eol(); } pub fn matched>( @@ -245,24 +241,14 @@ impl Printer { end: usize, line_number: Option, ) { - if !self.line_per_match { - let column = - if self.column { - Some(re.find(&buf[start..end]) - .map(|m| m.start()).unwrap_or(0) as u64) - } else { - None - }; + if !self.line_per_match && !self.only_matching { + let column = re.find(&buf[start..end]) + .map(|m| m.start()).unwrap_or(0); return self.write_match( re, path, buf, start, end, line_number, column); } for m in re.find_iter(&buf[start..end]) { - let column = - if self.column { - Some(m.start() as u64) - } else { - None - }; + let column = m.start(); self.write_match( re, path.as_ref(), buf, start, end, line_number, column); } @@ -276,20 +262,21 @@ impl Printer { start: usize, end: usize, line_number: Option, - column: Option, + column: usize, ) { if self.heading && self.with_filename && !self.has_printed { self.write_file_sep(); - self.write_heading(path.as_ref()); + self.write_path(path); + self.write_path_eol(); } else if !self.heading && self.with_filename { - self.write_non_heading_path(path.as_ref()); + self.write_path(path); + self.write_path_sep(b':'); } if let Some(line_number) = line_number { self.line_number(line_number, b':'); } - if let Some(c) = column { - self.write((c + 1).to_string().as_bytes()); - self.write(b":"); + if self.column { + self.column_number(column as u64 + 1, b':'); } if self.replace.is_some() { let mut count = 0; @@ -299,11 +286,9 @@ impl Printer { re.replace_all(&buf[start..end], replacer) }; if self.max_columns.map_or(false, |m| line.len() > m) { - let _ = self.wtr.set_color(self.colors.matched()); let msg = format!( "[Omitted long line with {} replacements]", count); - self.write(msg.as_bytes()); - let _ = self.wtr.reset(); + self.write_colored(msg.as_bytes(), |colors| colors.matched()); self.write_eol(); return; } @@ -312,7 +297,14 @@ impl Printer { self.write_eol(); } } else { - self.write_matched_line(re, &buf[start..end]); + let line_buf = if self.only_matching { + let start_offset = start + column; + let m = re.find(&buf[start_offset..end]).unwrap(); + &buf[start_offset + m.start()..start_offset + m.end()] + } else { + &buf[start..end] + }; + self.write_matched_line(re, line_buf); // write_matched_line guarantees to write a newline. } } @@ -320,10 +312,8 @@ impl Printer { fn write_matched_line(&mut self, re: &Regex, buf: &[u8]) { if self.max_columns.map_or(false, |m| buf.len() > m) { let count = re.find_iter(buf).count(); - let _ = self.wtr.set_color(self.colors.matched()); let msg = format!("[Omitted long line with {} matches]", count); - self.write(msg.as_bytes()); - let _ = self.wtr.reset(); + self.write_colored(msg.as_bytes(), |colors| colors.matched()); self.write_eol(); return; } @@ -333,9 +323,8 @@ impl Printer { let mut last_written = 0; for m in re.find_iter(buf) { self.write(&buf[last_written..m.start()]); - let _ = self.wtr.set_color(self.colors.matched()); - self.write(&buf[m.start()..m.end()]); - let _ = self.wtr.reset(); + self.write_colored( + &buf[m.start()..m.end()], |colors| colors.matched()); last_written = m.end(); } self.write(&buf[last_written..]); @@ -355,14 +344,11 @@ impl Printer { ) { if self.heading && self.with_filename && !self.has_printed { self.write_file_sep(); - self.write_heading(path.as_ref()); + self.write_path(path); + self.write_path_eol(); } else if !self.heading && self.with_filename { - self.write_path(path.as_ref()); - if self.null { - self.write(b"\x00"); - } else { - self.write(b"-"); - } + self.write_path(path); + self.write_path_sep(b'-'); } if let Some(line_number) = line_number { self.line_number(line_number, b'-'); @@ -378,63 +364,63 @@ impl Printer { } } - fn write_heading>(&mut self, path: P) { - let _ = self.wtr.set_color(self.colors.path()); - self.write_path(path.as_ref()); - let _ = self.wtr.reset(); + fn separator(&mut self, sep: &[u8]) { + self.write(&sep); + } + + fn write_path_sep(&mut self, sep: u8) { if self.null { self.write(b"\x00"); } else { - self.write_eol(); + self.separator(&[sep]); } } - fn write_non_heading_path>(&mut self, path: P) { - let _ = self.wtr.set_color(self.colors.path()); - self.write_path(path.as_ref()); - let _ = self.wtr.reset(); + fn write_path_eol(&mut self) { if self.null { self.write(b"\x00"); } else { - self.write(b":"); + self.write_eol(); } } - fn line_number(&mut self, n: u64, sep: u8) { - let _ = self.wtr.set_color(self.colors.line()); - self.write(n.to_string().as_bytes()); - let _ = self.wtr.reset(); - self.write(&[sep]); - } - #[cfg(unix)] fn write_path>(&mut self, path: P) { use std::os::unix::ffi::OsStrExt; - let path = path.as_ref().as_os_str().as_bytes(); - match self.path_separator { - None => self.write(path), - Some(sep) => self.write_path_with_sep(path, sep), - } + self.write_path_replace_separator(path); } #[cfg(not(unix))] fn write_path>(&mut self, path: P) { let path = path.as_ref().to_string_lossy(); - match self.path_separator { - None => self.write(path.as_bytes()), - Some(sep) => self.write_path_with_sep(path.as_bytes(), sep), - } + self.write_path_replace_separator(path.as_bytes()); } - fn write_path_with_sep(&mut self, path: &[u8], sep: u8) { - let mut path = path.to_vec(); - for b in &mut path { - if *b == b'/' || (cfg!(windows) && *b == b'\\') { - *b = sep; + fn write_path_replace_separator(&mut self, path: &[u8]) { + match self.path_separator { + None => self.write_colored(path, |colors| colors.path()), + Some(sep) => { + let transformed_path: Vec<_> = path.iter().map(|&b| { + if b == b'/' || (cfg!(windows) && b == b'\\') { + sep + } else { + b + } + }).collect(); + self.write_colored(&transformed_path, |colors| colors.path()); } } - self.write(&path); + } + + fn line_number(&mut self, n: u64, sep: u8) { + self.write_colored(n.to_string().as_bytes(), |colors| colors.line()); + self.separator(&[sep]); + } + + fn column_number(&mut self, n: u64, sep: u8) { + self.write_colored(n.to_string().as_bytes(), |colors| colors.column()); + self.separator(&[sep]); } fn write(&mut self, buf: &[u8]) { @@ -447,6 +433,14 @@ impl Printer { self.write(&[eol]); } + fn write_colored(&mut self, buf: &[u8], get_color: F) + where F: Fn(&ColorSpecs) -> &ColorSpec + { + let _ = self.wtr.set_color( get_color(&self.colors) ); + self.write(buf); + let _ = self.wtr.reset(); + } + fn write_file_sep(&mut self) { if let Some(ref sep) = self.file_separator { self.has_printed = true; @@ -492,7 +486,7 @@ impl fmt::Display for Error { match *self { Error::UnrecognizedOutType(ref name) => { write!(f, "Unrecognized output type '{}'. Choose from: \ - path, line, match.", name) + path, line, column, match.", name) } Error::UnrecognizedSpecType(ref name) => { write!(f, "Unrecognized spec type '{}'. Choose from: \ @@ -503,12 +497,14 @@ impl fmt::Display for Error { } Error::UnrecognizedStyle(ref name) => { write!(f, "Unrecognized style attribute '{}'. Choose from: \ - nobold, bold.", name) + nobold, bold, nointense, intense.", name) } Error::InvalidFormat(ref original) => { - write!(f, "Invalid color speci format: '{}'. Valid format \ - is '(path|line|match):(fg|bg|style):(value)'.", - original) + write!( + f, + "Invalid color speci format: '{}'. Valid format \ + is '(path|line|column|match):(fg|bg|style):(value)'.", + original) } } } @@ -525,6 +521,7 @@ impl From for Error { pub struct ColorSpecs { path: ColorSpec, line: ColorSpec, + column: ColorSpec, matched: ColorSpec, } @@ -554,7 +551,7 @@ pub struct ColorSpecs { /// The format of a `Spec` is a triple: `{type}:{attribute}:{value}`. Each /// component is defined as follows: /// -/// * `{type}` can be one of `path`, `line` or `match`. +/// * `{type}` can be one of `path`, `line`, `column` or `match`. /// * `{attribute}` can be one of `fg`, `bg` or `style`. `{attribute}` may also /// be the special value `none`, in which case, `{value}` can be omitted. /// * `{value}` is either a color name (for `fg`/`bg`) or a style instruction. @@ -593,6 +590,7 @@ enum SpecValue { enum OutType { Path, Line, + Column, Match, } @@ -623,6 +621,7 @@ impl ColorSpecs { match user_spec.ty { OutType::Path => user_spec.merge_into(&mut specs.path), OutType::Line => user_spec.merge_into(&mut specs.line), + OutType::Column => user_spec.merge_into(&mut specs.column), OutType::Match => user_spec.merge_into(&mut specs.matched), } } @@ -639,6 +638,11 @@ impl ColorSpecs { &self.line } + /// Return the color specification for coloring column numbers. + fn column(&self) -> &ColorSpec { + &self.column + } + /// Return the color specification for coloring matched text. fn matched(&self) -> &ColorSpec { &self.matched @@ -714,6 +718,7 @@ impl FromStr for OutType { match &*s.to_lowercase() { "path" => Ok(OutType::Path), "line" => Ok(OutType::Line), + "column" => Ok(OutType::Column), "match" => Ok(OutType::Match), _ => Err(Error::UnrecognizedOutType(s.to_string())), } @@ -765,6 +770,7 @@ mod tests { assert_eq!(ColorSpecs::new(user_specs), ColorSpecs { path: ColorSpec::default(), line: ColorSpec::default(), + column: ColorSpec::default(), matched: expect_matched, }); } @@ -800,6 +806,12 @@ mod tests { ty: OutType::Line, value: SpecValue::None, }); + + let spec: Spec = "column:bg:green".parse().unwrap(); + assert_eq!(spec, Spec { + ty: OutType::Column, + value: SpecValue::Bg(Color::Green), + }); } #[test] diff --git a/src/search_buffer.rs b/src/search_buffer.rs index 4745c2f87..d2e3692a2 100644 --- a/src/search_buffer.rs +++ b/src/search_buffer.rs @@ -3,8 +3,8 @@ The `search_buffer` module is responsible for searching a single file all in a single buffer. Typically, the source of the buffer is a memory map. This can be useful for when memory maps are faster than streaming search. -Note that this module doesn't quite support everything that `search_stream` does. -Notably, showing contexts. +Note that this module doesn't quite support everything that `search_stream` +does. Notably, showing contexts. */ use std::cmp; use std::path::Path; diff --git a/termcolor/COPYING b/termcolor/COPYING new file mode 100644 index 000000000..bb9c20a09 --- /dev/null +++ b/termcolor/COPYING @@ -0,0 +1,3 @@ +This project is dual-licensed under the Unlicense and MIT licenses. + +You may use this code under the terms of either license. diff --git a/termcolor/Cargo.toml b/termcolor/Cargo.toml index 2d253a0a8..9d37199fc 100644 --- a/termcolor/Cargo.toml +++ b/termcolor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "termcolor" -version = "0.3.1" #:version +version = "0.3.2" #:version authors = ["Andrew Gallant "] description = """ A simple cross platform library for writing colored text to a terminal. diff --git a/termcolor/LICENSE-MIT b/termcolor/LICENSE-MIT new file mode 100644 index 000000000..3b0a5dc09 --- /dev/null +++ b/termcolor/LICENSE-MIT @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Andrew Gallant + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/termcolor/UNLICENSE b/termcolor/UNLICENSE new file mode 100644 index 000000000..68a49daad --- /dev/null +++ b/termcolor/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/tests/tests.rs b/tests/tests.rs index 9477f32eb..95c6e6a58 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -455,7 +455,6 @@ sherlock!(max_filesize_parse_no_suffix, "Sherlock", ".", let expected = "\ foo "; - assert_eq!(lines, expected); }); @@ -470,7 +469,6 @@ sherlock!(max_filesize_parse_k_suffix, "Sherlock", ".", let expected = "\ foo "; - assert_eq!(lines, expected); }); @@ -485,10 +483,19 @@ sherlock!(max_filesize_parse_m_suffix, "Sherlock", ".", let expected = "\ foo "; - assert_eq!(lines, expected); }); +sherlock!(max_filesize_suffix_overflow, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create_size("foo", 1000000); + + // 2^35 * 2^30 would otherwise overflow + cmd.arg("--max-filesize").arg("34359738368G").arg("--files"); + wd.assert_err(&mut cmd); +}); + sherlock!(ignore_hidden, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { wd.remove("sherlock"); wd.create(".sherlock", hay::SHERLOCK); @@ -1057,6 +1064,33 @@ clean!(regression_405, "test", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, format!("{}:test\n", path("bar/foo/file2.txt"))); }); +// See: https://github.com/BurntSushi/ripgrep/issues/428 +clean!(regression_428_color_context_path, "foo", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("sherlock", "foo\nbar"); + cmd.arg("-A1").arg("-H").arg("--no-heading").arg("-N") + .arg("--colors=match:none").arg("--color=always"); + + let lines: String = wd.stdout(&mut cmd); + let expected = format!("\ +{colored_path}:foo +{colored_path}-bar +", colored_path=format!("\x1b\x5b\x6d\x1b\x5b\x33\x35\x6d{path}\x1b\x5b\x6d", path=path("sherlock"))); + assert_eq!(lines, expected); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/428 +clean!(regression_428_unrecognized_style, "Sherlok", ".", |wd: WorkDir, mut cmd: Command| { + cmd.arg("--colors=match:style:"); + wd.assert_err(&mut cmd); + + let output = cmd.output().unwrap(); + let err = String::from_utf8_lossy(&output.stderr); + let expected = "\ +Unrecognized style attribute ''. Choose from: nobold, bold, nointense, intense. +"; + assert_eq!(err, expected); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/1 clean!(feature_1_sjis, "Шерлок Холмс", ".", |wd: WorkDir, mut cmd: Command| { let sherlock = @@ -1103,6 +1137,21 @@ clean!(feature_1_eucjp, "Шерлок Холмс", ".", assert_eq!(lines, "foo:Шерлок Холмс\n"); }); +// See: https://github.com/BurntSushi/ripgrep/issues/1 +sherlock!(feature_1_unknown_encoding, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-Efoobar"); + wd.assert_non_empty_stderr(&mut cmd); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/1 +// Specific: https://github.com/BurntSushi/ripgrep/pull/398/files#r111109265 +sherlock!(feature_1_replacement_encoding, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-Ecsiso2022kr"); + wd.assert_non_empty_stderr(&mut cmd); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/7 sherlock!(feature_7, "-fpat", "sherlock", |wd: WorkDir, mut cmd: Command| { wd.create("pat", "Sherlock\nHolmes"); @@ -1139,6 +1188,32 @@ be, to a very large extent, the result of luck. Sherlock Holmes assert_eq!(lines, expected); }); +// See: https://github.com/BurntSushi/ripgrep/issues/34 +sherlock!(feature_34_only_matching, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("--only-matching"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:Sherlock +sherlock:Sherlock +"; + assert_eq!(lines, expected); +}); + +// See: https://github.com/BurntSushi/ripgrep/issues/34 +sherlock!(feature_34_only_matching_line_column, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("--only-matching").arg("--column").arg("--line-number"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:1:57:Sherlock +sherlock:3:49:Sherlock +"; + assert_eq!(lines, expected); +}); + // See: https://github.com/BurntSushi/ripgrep/issues/45 sherlock!(feature_45_relative_cwd, "test", ".", |wd: WorkDir, mut cmd: Command| { @@ -1390,6 +1465,46 @@ clean!(feature_275_pathsep, "test", ".", |wd: WorkDir, mut cmd: Command| { assert_eq!(lines, "fooZbar:test\n"); }); +// See: https://github.com/BurntSushi/ripgrep/issues/362 +sherlock!(feature_362_dfa_size_limit, r"For\s", +|wd: WorkDir, mut cmd: Command| { + // This should fall back to the nfa engine but should still produce the + // expected result. + cmd.arg("--dfa-size-limit").arg("10"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +"; + assert_eq!(lines, expected); +}); + +sherlock!(feature_362_exceeds_regex_size_limit, r"[0-9]\w+", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("--regex-size-limit").arg("10K"); + wd.assert_err(&mut cmd); +}); + +#[cfg(target_pointer_width = "32")] +sherlock!(feature_362_u64_to_narrow_usize_suffix_overflow, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create_size("foo", 1000000); + + // 2^35 * 2^20 is ok for u64, but not for usize + cmd.arg("--dfa-size-limit").arg("34359738368M").arg("--files"); + wd.assert_err(&mut cmd); +}); + + +// See: https://github.com/BurntSushi/ripgrep/issues/419 +sherlock!(feature_419_zero_as_shortcut_for_null, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-0").arg("--count"); + + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "sherlock\x002\n"); +}); + #[test] fn binary_nosearch() { let wd = WorkDir::new("binary_nosearch"); @@ -1492,6 +1607,51 @@ fn regression_391() { assert_eq!(lines, "bar.py\n"); } +// See: https://github.com/BurntSushi/ripgrep/issues/451 +#[test] +fn regression_451_only_matching_as_in_issue() { + let wd = WorkDir::new("regression_451_only_matching"); + let path = "digits.txt"; + wd.create(path, "1 2 3\n"); + + let mut cmd = wd.command(); + cmd.arg("[0-9]+").arg(path).arg("--only-matching"); + let lines: String = wd.stdout(&mut cmd); + + let expected = "\ +1 +2 +3 +"; + + assert_eq!(lines, expected); +} + +// See: https://github.com/BurntSushi/ripgrep/issues/451 +#[test] +fn regression_451_only_matching() { + let wd = WorkDir::new("regression_451_only_matching"); + let path = "digits.txt"; + wd.create(path, "1 2 3\n123\n"); + + let mut cmd = wd.command(); + cmd.arg("[0-9]").arg(path) + .arg("--only-matching") + .arg("--column"); + let lines: String = wd.stdout(&mut cmd); + + let expected = "\ +1:1:1 +1:3:2 +1:5:3 +2:1:1 +2:2:2 +2:3:3 +"; + + assert_eq!(lines, expected); +} + #[test] fn type_list() { let wd = WorkDir::new("type_list"); diff --git a/tests/workdir.rs b/tests/workdir.rs index a5fe9e54d..06621532a 100644 --- a/tests/workdir.rs +++ b/tests/workdir.rs @@ -256,6 +256,23 @@ impl WorkDir { String::from_utf8_lossy(&o.stderr)); } } + + /// Runs the given command and asserts that something was printed to + /// stderr. + pub fn assert_non_empty_stderr(&self, cmd: &mut process::Command) { + let o = cmd.output().unwrap(); + if o.status.success() || o.stderr.is_empty() { + panic!("\n\n===== {:?} =====\n\ + command succeeded but expected failure!\ + \n\ncwd: {}\ + \n\nstatus: {}\ + \n\nstdout: {}\n\nstderr: {}\ + \n\n=====\n", + cmd, self.dir.display(), o.status, + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr)); + } + } } fn nice_err, T, E: error::Error>(