mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-08-02 13:11:58 -07:00
Compare commits
145 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4981991a6e | ||
|
51440f59cd | ||
|
7b8a8d77d0 | ||
|
4737326ed3 | ||
|
a3537aa32a | ||
|
d3e118a786 | ||
|
4e52059ad6 | ||
|
60c016c243 | ||
|
4665128f25 | ||
|
dde5bd5a80 | ||
|
762ad44f71 | ||
|
705386934d | ||
|
97bbc6ef11 | ||
|
27a980c1bc | ||
|
e8645dc8ae | ||
|
e96d93034a | ||
|
bc5accc035 | ||
|
c9d0ca8257 | ||
|
45fe4aab96 | ||
|
97f981fbcb | ||
|
59329dcc61 | ||
|
604da8eb86 | ||
|
1c964372ad | ||
|
50a961960e | ||
|
7481c5fe29 | ||
|
3ae37b0937 | ||
|
4ee6dbe422 | ||
|
cd4bdcf810 | ||
|
175406df01 | ||
|
89811d43d4 | ||
|
f0053682c0 | ||
|
35045d6105 | ||
|
95f552fc06 | ||
|
48353bea17 | ||
|
703d5b558e | ||
|
47efea234f | ||
|
ca0d8998a2 | ||
|
fdf24317ac | ||
|
b9d5f22a4d | ||
|
67bb4f040f | ||
|
cee2f09a6d | ||
|
ced777e91f | ||
|
e9d9083898 | ||
|
46dff8f4be | ||
|
7aa6e87952 | ||
|
925d0db9f0 | ||
|
316ffd87b3 | ||
|
5943b1effe | ||
|
c42f97b4da | ||
|
0d9bba7816 | ||
|
3550f2e29a | ||
|
babe80d498 | ||
|
3e892a7a80 | ||
|
1df3f0b793 | ||
|
b3935935cb | ||
|
67abbf6f22 | ||
|
7b9f7d7dc6 | ||
|
7ab29a91d0 | ||
|
9fa38c6232 | ||
|
de79be2db2 | ||
|
416b69bae5 | ||
|
3e78fce3a3 | ||
|
7a3fd1f23f | ||
|
d306403440 | ||
|
ebabe1df6a | ||
|
f27aa3ff6f | ||
|
20ccd441f2 | ||
|
104d740f76 | ||
|
2da0eab2b8 | ||
|
b8c7864a02 | ||
|
ec26995655 | ||
|
a41235a3b5 | ||
|
1a91b900e7 | ||
|
2b15832655 | ||
|
b1c52b52d6 | ||
|
109bc3f78e | ||
|
b62195b33f | ||
|
baebfd7add | ||
|
19e405e5c5 | ||
|
f85822266f | ||
|
b034b77798 | ||
|
278e1168bf | ||
|
6a8051b258 | ||
|
a13ac3e3d4 | ||
|
a72467996b | ||
|
9395076468 | ||
|
a12c63957b | ||
|
982265af70 | ||
|
ed94aedf27 | ||
|
fd5ae2f795 | ||
|
3d6a39be06 | ||
|
e7839f2200 | ||
|
9dc5464c84 | ||
|
95edcd4d3a | ||
|
d97f404970 | ||
|
b2bbd46178 | ||
|
82542df5cb | ||
|
e4329037aa | ||
|
ab0d1c1c79 | ||
|
2015c56e8d | ||
|
23ad8b989d | ||
|
a8f3d9e87e | ||
|
9f1aae64f8 | ||
|
1595f0faf5 | ||
|
8eeb0c0b60 | ||
|
423f2a1927 | ||
|
4b5e789a2a | ||
|
37b731a048 | ||
|
a44735aa87 | ||
|
6b2efd4d88 | ||
|
c8227e0cf3 | ||
|
b941c10b90 | ||
|
872a107658 | ||
|
71ad9bf393 | ||
|
f733e9ebe4 | ||
|
ce85df1d2e | ||
|
a6e3cab65a | ||
|
7b860affbe | ||
|
af4dc78537 | ||
|
9ce0484670 | ||
|
346bad7dfc | ||
|
56fe93d343 | ||
|
155676b474 | ||
|
a3fc4cdded | ||
|
3bec8f3f0a | ||
|
3b37f12ec0 | ||
|
a2ed677e03 | ||
|
2fb9c3c42c | ||
|
447e1ba0e2 | ||
|
3b45059212 | ||
|
f74078af5b | ||
|
5ff9b2f2a2 | ||
|
cc90511ab2 | ||
|
f5d60a80a8 | ||
|
6fa158f6d3 | ||
|
ef6dea40ff | ||
|
9035c6b7b3 | ||
|
f5eb36baac | ||
|
6367dd61ba | ||
|
98892de1c1 | ||
|
273c14a45a | ||
|
b33e9cba69 | ||
|
d5c045469b | ||
|
0ce82403d4 | ||
|
d2f95f6e59 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@
|
|||||||
tags
|
tags
|
||||||
target
|
target
|
||||||
/grep/Cargo.lock
|
/grep/Cargo.lock
|
||||||
|
/globset/Cargo.lock
|
||||||
|
@@ -15,9 +15,6 @@ matrix:
|
|||||||
- os: linux
|
- os: linux
|
||||||
rust: nightly
|
rust: nightly
|
||||||
env: TARGET=x86_64-unknown-linux-musl
|
env: TARGET=x86_64-unknown-linux-musl
|
||||||
- os: osx
|
|
||||||
rust: nightly
|
|
||||||
env: TARGET=i686-apple-darwin
|
|
||||||
- os: osx
|
- os: osx
|
||||||
rust: nightly
|
rust: nightly
|
||||||
env: TARGET=x86_64-apple-darwin
|
env: TARGET=x86_64-apple-darwin
|
||||||
|
146
CHANGELOG.md
Normal file
146
CHANGELOG.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
0.2.2
|
||||||
|
=====
|
||||||
|
Packaging updates:
|
||||||
|
|
||||||
|
* `ripgrep` is now in homebrew-core. `brew install ripgrep` will do the trick
|
||||||
|
on a Mac.
|
||||||
|
* `ripgrep` is now in the Archlinux community repository.
|
||||||
|
`pacman -S ripgrep` will do the trick on Archlinux.
|
||||||
|
* Support has been discontinued for i686-darwin.
|
||||||
|
* Glob matching has been moved out into its own crate:
|
||||||
|
[`globset`](https://crates.io/crates/globset).
|
||||||
|
|
||||||
|
Feature enhancements:
|
||||||
|
|
||||||
|
* Added or improved file type filtering for CMake, config, Jinja, Markdown,
|
||||||
|
Spark,
|
||||||
|
* [FEATURE #109](https://github.com/BurntSushi/ripgrep/issues/109):
|
||||||
|
Add a --max-depth flag for directory traversal.
|
||||||
|
* [FEATURE #124](https://github.com/BurntSushi/ripgrep/issues/124):
|
||||||
|
Add -s/--case-sensitive flag. Overrides --smart-case.
|
||||||
|
* [FEATURE #139](https://github.com/BurntSushi/ripgrep/pull/139):
|
||||||
|
The `ripgrep` repo is now a Homebrew tap. This is useful for installing
|
||||||
|
SIMD accelerated binaries, which aren't available in homebrew-core.
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
* [BUG #87](https://github.com/BurntSushi/ripgrep/issues/87),
|
||||||
|
[BUG #127](https://github.com/BurntSushi/ripgrep/issues/127),
|
||||||
|
[BUG #131](https://github.com/BurntSushi/ripgrep/issues/131):
|
||||||
|
Various issues related to glob matching.
|
||||||
|
* [BUG #116](https://github.com/BurntSushi/ripgrep/issues/116):
|
||||||
|
--quiet should stop search after first match.
|
||||||
|
* [BUG #121](https://github.com/BurntSushi/ripgrep/pull/121):
|
||||||
|
--color always should show colors, even when --vimgrep is used.
|
||||||
|
* [BUG #122](https://github.com/BurntSushi/ripgrep/pull/122):
|
||||||
|
Colorize file path at beginning of line.
|
||||||
|
* [BUG #134](https://github.com/BurntSushi/ripgrep/issues/134):
|
||||||
|
Processing a large ignore file (thousands of globs) was very slow.
|
||||||
|
* [BUG #137](https://github.com/BurntSushi/ripgrep/issues/137):
|
||||||
|
Always follow symlinks when given as an explicit argument.
|
||||||
|
* [BUG #147](https://github.com/BurntSushi/ripgrep/issues/147):
|
||||||
|
Clarify documentation for --replace.
|
||||||
|
|
||||||
|
|
||||||
|
0.2.1
|
||||||
|
=====
|
||||||
|
Feature enhancements:
|
||||||
|
|
||||||
|
* Added or improved file type filtering for Clojure and SystemVerilog.
|
||||||
|
* [FEATURE #89](https://github.com/BurntSushi/ripgrep/issues/89):
|
||||||
|
Add a --null flag that outputs a NUL byte after every file path.
|
||||||
|
|
||||||
|
Bug fixes:
|
||||||
|
|
||||||
|
* [BUG #98](https://github.com/BurntSushi/ripgrep/issues/98):
|
||||||
|
Fix a bug in single threaded mode when if opening a file failed, ripgrep
|
||||||
|
quit instead of continuing the search.
|
||||||
|
* [BUG #99](https://github.com/BurntSushi/ripgrep/issues/99):
|
||||||
|
Fix another bug in single threaded mode where empty lines were being printed
|
||||||
|
by mistake.
|
||||||
|
* [BUG #105](https://github.com/BurntSushi/ripgrep/issues/105):
|
||||||
|
Fix an off-by-one error with --column.
|
||||||
|
* [BUG #106](https://github.com/BurntSushi/ripgrep/issues/106):
|
||||||
|
Fix a bug where a whitespace only line in a gitignore file caused ripgrep
|
||||||
|
to panic (i.e., crash).
|
||||||
|
|
||||||
|
|
||||||
|
0.2.0
|
||||||
|
=====
|
||||||
|
Feature enhancements:
|
||||||
|
|
||||||
|
* Added or improved file type filtering for VB, R, F#, Swift, Nim, Javascript,
|
||||||
|
TypeScript
|
||||||
|
* [FEATURE #20](https://github.com/BurntSushi/ripgrep/issues/20):
|
||||||
|
Adds a --no-filename flag.
|
||||||
|
* [FEATURE #26](https://github.com/BurntSushi/ripgrep/issues/26):
|
||||||
|
Adds --files-with-matches flag. Like --count, but only prints file paths
|
||||||
|
and doesn't need to count every match.
|
||||||
|
* [FEATURE #40](https://github.com/BurntSushi/ripgrep/issues/40):
|
||||||
|
Switch from using `.rgignore` to `.ignore`. Note that `.rgignore` is
|
||||||
|
still supported, but deprecated.
|
||||||
|
* [FEATURE #68](https://github.com/BurntSushi/ripgrep/issues/68):
|
||||||
|
Add --no-ignore-vcs flag that ignores .gitignore but not .ignore.
|
||||||
|
* [FEATURE #70](https://github.com/BurntSushi/ripgrep/issues/70):
|
||||||
|
Add -S/--smart-case flag (but is disabled by default).
|
||||||
|
* [FEATURE #80](https://github.com/BurntSushi/ripgrep/issues/80):
|
||||||
|
Add support for `{foo,bar}` globs.
|
||||||
|
|
||||||
|
Many many bug fixes. Thanks every for reporting these and helping make
|
||||||
|
`ripgrep` better! (Note that I haven't captured every tracking issue here,
|
||||||
|
some were closed as duplicates.)
|
||||||
|
|
||||||
|
* [BUG #8](https://github.com/BurntSushi/ripgrep/issues/8):
|
||||||
|
Don't use an intermediate buffer when --threads=1. (Permits constant memory
|
||||||
|
usage.)
|
||||||
|
* [BUG #15](https://github.com/BurntSushi/ripgrep/issues/15):
|
||||||
|
Improves the documentation for --type-add.
|
||||||
|
* [BUG #16](https://github.com/BurntSushi/ripgrep/issues/16),
|
||||||
|
[BUG #49](https://github.com/BurntSushi/ripgrep/issues/49),
|
||||||
|
[BUG #50](https://github.com/BurntSushi/ripgrep/issues/50),
|
||||||
|
[BUG #65](https://github.com/BurntSushi/ripgrep/issues/65):
|
||||||
|
Some gitignore globs were being treated as anchored when they weren't.
|
||||||
|
* [BUG #18](https://github.com/BurntSushi/ripgrep/issues/18):
|
||||||
|
--vimgrep reported incorrect column number.
|
||||||
|
* [BUG #19](https://github.com/BurntSushi/ripgrep/issues/19):
|
||||||
|
ripgrep was hanging waiting on stdin in some Windows terminals. Note that
|
||||||
|
this introduced a new bug:
|
||||||
|
[#94](https://github.com/BurntSushi/ripgrep/issues/94).
|
||||||
|
* [BUG #21](https://github.com/BurntSushi/ripgrep/issues/21):
|
||||||
|
Removes leading `./` when printing file paths.
|
||||||
|
* [BUG #22](https://github.com/BurntSushi/ripgrep/issues/22):
|
||||||
|
Running `rg --help | echo` caused `rg` to panic.
|
||||||
|
* [BUG #24](https://github.com/BurntSushi/ripgrep/issues/22):
|
||||||
|
Clarify the central purpose of rg in its usage message.
|
||||||
|
* [BUG #25](https://github.com/BurntSushi/ripgrep/issues/25):
|
||||||
|
Anchored gitignore globs weren't applied in subdirectories correctly.
|
||||||
|
* [BUG #30](https://github.com/BurntSushi/ripgrep/issues/30):
|
||||||
|
Globs like `foo/**` should match contents of `foo`, but not `foo` itself.
|
||||||
|
* [BUG #35](https://github.com/BurntSushi/ripgrep/issues/35),
|
||||||
|
[BUG #81](https://github.com/BurntSushi/ripgrep/issues/81):
|
||||||
|
When automatically detecting stdin, only read if it's a file or a fifo.
|
||||||
|
i.e., ignore stdin in `rg foo < /dev/null`.
|
||||||
|
* [BUG #36](https://github.com/BurntSushi/ripgrep/issues/36):
|
||||||
|
Don't automatically pick memory maps on MacOS. Ever.
|
||||||
|
* [BUG #38](https://github.com/BurntSushi/ripgrep/issues/38):
|
||||||
|
Trailing whitespace in gitignore wasn't being ignored.
|
||||||
|
* [BUG #43](https://github.com/BurntSushi/ripgrep/issues/43):
|
||||||
|
--glob didn't work with directories.
|
||||||
|
* [BUG #46](https://github.com/BurntSushi/ripgrep/issues/46):
|
||||||
|
Use one fewer worker thread than what is provided on CLI.
|
||||||
|
* [BUG #47](https://github.com/BurntSushi/ripgrep/issues/47):
|
||||||
|
--help/--version now work even if other options are set.
|
||||||
|
* [BUG #55](https://github.com/BurntSushi/ripgrep/issues/55):
|
||||||
|
ripgrep was refusing to search /proc/cpuinfo. Fixed by disabling memory
|
||||||
|
maps for files with zero size.
|
||||||
|
* [BUG #64](https://github.com/BurntSushi/ripgrep/issues/64):
|
||||||
|
The first path given with --files set was ignored.
|
||||||
|
* [BUG #67](https://github.com/BurntSushi/ripgrep/issues/67):
|
||||||
|
Sometimes whitelist globs like `!/dir` weren't interpreted as anchored.
|
||||||
|
* [BUG #77](https://github.com/BurntSushi/ripgrep/issues/77):
|
||||||
|
When -q/--quiet flag was passed, ripgrep kept searching even after a match
|
||||||
|
was found.
|
||||||
|
* [BUG #90](https://github.com/BurntSushi/ripgrep/issues/90):
|
||||||
|
Permit whitelisting hidden files.
|
||||||
|
* [BUG #93](https://github.com/BurntSushi/ripgrep/issues/93):
|
||||||
|
ripgrep was extracting an erroneous inner literal from a repeated pattern.
|
37
Cargo.lock
generated
37
Cargo.lock
generated
@@ -1,13 +1,12 @@
|
|||||||
[root]
|
[root]
|
||||||
name = "ripgrep"
|
name = "ripgrep"
|
||||||
version = "0.1.16"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"docopt 0.6.83 (registry+https://github.com/rust-lang/crates.io-index)",
|
"docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"globset 0.1.0",
|
||||||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"grep 0.1.3",
|
||||||
"grep 0.1.2",
|
|
||||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
"libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -40,7 +39,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "docopt"
|
name = "docopt"
|
||||||
version = "0.6.83"
|
version = "0.6.86"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -74,19 +73,26 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glob"
|
name = "globset"
|
||||||
version = "0.2.11"
|
version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
dependencies = [
|
||||||
|
"aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "grep"
|
name = "grep"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
"log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"memmap 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"memmap 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex-syntax 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -155,7 +161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
"memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
"regex-syntax 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
"thread_local 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
@@ -163,7 +169,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex-syntax"
|
name = "regex-syntax"
|
||||||
version = "0.3.5"
|
version = "0.3.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -234,11 +240,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
[metadata]
|
[metadata]
|
||||||
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
"checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66"
|
||||||
"checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf"
|
"checksum deque 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1614659040e711785ed8ea24219140654da1729f3ec8a47a9719d041112fe7bf"
|
||||||
"checksum docopt 0.6.83 (registry+https://github.com/rust-lang/crates.io-index)" = "fc42c6077823a361410c37d47c2535b73a190cbe10838dc4f400fe87c10c8c3b"
|
"checksum docopt 0.6.86 (registry+https://github.com/rust-lang/crates.io-index)" = "4a7ef30445607f6fc8720f0a0a2c7442284b629cf0d049286860fae23e71c4d9"
|
||||||
"checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
|
"checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f"
|
||||||
"checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
|
"checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
|
||||||
"checksum fs2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bcd414e5a1a979b931bb92f41b7a54106d3f6d2e6c253e9ce943b7cd468251ef"
|
"checksum fs2 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "bcd414e5a1a979b931bb92f41b7a54106d3f6d2e6c253e9ce943b7cd468251ef"
|
||||||
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
|
|
||||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||||
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
|
"checksum lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "49247ec2a285bb3dcb23cbd9c35193c025e7251bfce77c1d5da97e6362dffe7f"
|
||||||
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
|
"checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d"
|
||||||
@@ -248,7 +253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
"checksum num_cpus 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8890e6084723d57d0df8d2720b0d60c6ee67d6c93e7169630e4371e88765dcad"
|
"checksum num_cpus 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8890e6084723d57d0df8d2720b0d60c6ee67d6c93e7169630e4371e88765dcad"
|
||||||
"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
|
"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
|
||||||
"checksum regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)" = "64b03446c466d35b42f2a8b203c8e03ed8b91c0f17b56e1f84f7210a257aa665"
|
"checksum regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)" = "64b03446c466d35b42f2a8b203c8e03ed8b91c0f17b56e1f84f7210a257aa665"
|
||||||
"checksum regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279401017ae31cf4e15344aa3f085d0e2e5c1e70067289ef906906fdbe92c8fd"
|
"checksum regex-syntax 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "48f0573bcee95a48da786f8823465b5f2a1fae288a55407aca991e5b3e0eae11"
|
||||||
"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b"
|
"checksum rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6159e4e6e559c81bd706afe9c8fd68f547d3e851ce12e76b1de7914bab61691b"
|
||||||
"checksum simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "63b5847c2d766ca7ce7227672850955802fabd779ba616aeabead4c2c3877023"
|
"checksum simd 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "63b5847c2d766ca7ce7227672850955802fabd779ba616aeabead4c2c3877023"
|
||||||
"checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e"
|
"checksum strsim 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "50c069df92e4b01425a8bf3576d5d417943a6a7272fbabaf5bd80b1aaa76442e"
|
||||||
|
11
Cargo.toml
11
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ripgrep"
|
name = "ripgrep"
|
||||||
version = "0.1.16" #:version
|
version = "0.2.2" #:version
|
||||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||||
description = """
|
description = """
|
||||||
Line oriented search tool using Rust's regex library. Combines the raw
|
Line oriented search tool using Rust's regex library. Combines the raw
|
||||||
@@ -12,6 +12,7 @@ repository = "https://github.com/BurntSushi/ripgrep"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["regex", "grep", "egrep", "search", "pattern"]
|
keywords = ["regex", "grep", "egrep", "search", "pattern"]
|
||||||
license = "Unlicense/MIT"
|
license = "Unlicense/MIT"
|
||||||
|
exclude = ["HomebrewFormula"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
bench = false
|
bench = false
|
||||||
@@ -26,8 +27,8 @@ path = "tests/tests.rs"
|
|||||||
deque = "0.3"
|
deque = "0.3"
|
||||||
docopt = "0.6"
|
docopt = "0.6"
|
||||||
env_logger = "0.3"
|
env_logger = "0.3"
|
||||||
fnv = "1.0"
|
globset = { version = "0.1.0", path = "globset" }
|
||||||
grep = { version = "0.1.2", path = "grep" }
|
grep = { version = "0.1.3", path = "grep" }
|
||||||
lazy_static = "0.2"
|
lazy_static = "0.2"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
@@ -46,9 +47,5 @@ winapi = "0.2"
|
|||||||
[features]
|
[features]
|
||||||
simd-accel = ["regex/simd-accel"]
|
simd-accel = ["regex/simd-accel"]
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
glob = "0.2"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
panic = "abort"
|
|
||||||
|
1
HomebrewFormula
Symbolic link
1
HomebrewFormula
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
pkg/brew
|
263
README-NEW.md
263
README-NEW.md
@@ -1,263 +0,0 @@
|
|||||||
ripgrep (rg)
|
|
||||||
------------
|
|
||||||
`ripgrep` is a command line search tool that combines the usability of The
|
|
||||||
Silver Searcher (an `ack` clone) with the raw speed of GNU grep. `ripgrep` has
|
|
||||||
first class support on Windows, Mac and Linux, with binary downloads available
|
|
||||||
for [every release](https://github.com/BurntSushi/ripgrep/releases).
|
|
||||||
|
|
||||||
[](https://travis-ci.org/BurntSushi/ripgrep)
|
|
||||||
[](https://ci.appveyor.com/project/BurntSushi/ripgrep)
|
|
||||||
[](https://crates.io/crates/ripgrep)
|
|
||||||
|
|
||||||
Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).
|
|
||||||
|
|
||||||
### Screenshot of search results
|
|
||||||
|
|
||||||
[](http://burntsushi.net/stuff/ripgrep1.png)
|
|
||||||
|
|
||||||
### Quick example comparing tools
|
|
||||||
|
|
||||||
This example searches the entire Linux kernel source tree (after running
|
|
||||||
`make defconfig && make -j8`) for `[A-Z]+_SUSPEND`, where all matches must be
|
|
||||||
words. Timings were collected on a system with an Intel i7-6900K 3.2 GHz.
|
|
||||||
|
|
||||||
Please remember that a single benchmark is never enough! See my
|
|
||||||
[blog post on `ripgrep`](http://blog.burntsushi.net/ripgrep/)
|
|
||||||
for a very detailed comparison with more benchmarks and analysis.
|
|
||||||
|
|
||||||
| Tool | Command | Line count | Time |
|
|
||||||
| ---- | ------- | ---------- | ---- |
|
|
||||||
| ripgrep | `rg -n -w '[A-Z]+_SUSPEND'` | 450 | **0.245s** |
|
|
||||||
| [The Silver Searcher](https://github.com/ggreer/the_silver_searcher) | `ag -w '[A-Z]+_SUSPEND'` | 450 | 0.753s |
|
|
||||||
| [git grep](https://www.kernel.org/pub/software/scm/git/docs/git-grep.html) | `LC_ALL=C git grep -E -n -w '[A-Z]+_SUSPEND'` | 450 | 0.823s |
|
|
||||||
| [git grep](https://www.kernel.org/pub/software/scm/git/docs/git-grep.html) | `LC_ALL=en_US.UTF-8 git grep -E -n -w '[A-Z]+_SUSPEND'` | 450 | 2.880s |
|
|
||||||
| [sift](https://github.com/svent/sift) | `sift --git -n -w '[A-Z]+_SUSPEND'` | 450 | 3.656s |
|
|
||||||
| [The Platinum Searcher](https://github.com/monochromegane/the_platinum_searcher) | `pt -w -e '[A-Z]+_SUSPEND'` | 450 | 12.369s |
|
|
||||||
| [ack](http://beyondgrep.com/) | `ack -w '[A-Z]+_SUSPEND'` | 1878 | 16.952s |
|
|
||||||
|
|
||||||
(Yes, `ack` [has](https://github.com/petdance/ack2/issues/445) a
|
|
||||||
[bug](https://github.com/petdance/ack2/issues/14).)
|
|
||||||
|
|
||||||
### Why should I use `ripgrep`?
|
|
||||||
|
|
||||||
* It can replace both The Silver Searcher and GNU grep because it is faster
|
|
||||||
than both. (N.B. It is not, strictly speaking, a "drop-in" replacement for
|
|
||||||
both, but the feature sets are far more similar than different.)
|
|
||||||
* Like The Silver Searcher, `ripgrep` defaults to recursive directory search
|
|
||||||
and won't search files ignored by your `.gitignore` files. It also ignores
|
|
||||||
hidden and binary files by default. `ripgrep` also implements full support
|
|
||||||
for `.gitignore`, where as there are many bugs related to that functionality
|
|
||||||
in The Silver Searcher.
|
|
||||||
* `ripgrep` can search specific types of files. For example, `rg -tpy foo`
|
|
||||||
limits your search to Python files and `rg -Tjs foo` excludes Javascript
|
|
||||||
files from your search. `ripgrep` can be taught about new file types with
|
|
||||||
custom matching rules.
|
|
||||||
* `ripgrep` supports many features found in `grep`, such as showing the context
|
|
||||||
of search results, searching multiple patterns, highlighting matches with
|
|
||||||
color and full Unicode support. Unlike GNU grep, `ripgrep` stays fast while
|
|
||||||
supporting Unicode (which is always on).
|
|
||||||
|
|
||||||
In other words, use `ripgrep` if you like speed, sane defaults, fewer bugs and
|
|
||||||
Unicode.
|
|
||||||
|
|
||||||
### Is it really faster than everything else?
|
|
||||||
|
|
||||||
Yes. A large number of benchmarks with detailed analysis for each is
|
|
||||||
[available on my blog](http://blog.burntsushi.net/ripgrep/).
|
|
||||||
|
|
||||||
Summarizing, `ripgrep` is fast because:
|
|
||||||
|
|
||||||
* It is built on top of
|
|
||||||
[Rust's regex engine](https://github.com/rust-lang-nursery/regex).
|
|
||||||
Rust's regex engine uses finite automata, SIMD and aggressive literal
|
|
||||||
optimizations to make searching very fast.
|
|
||||||
* Rust's regex library maintains performance with full Unicode support by
|
|
||||||
building UTF-8 decoding directly into its deterministic finite automaton
|
|
||||||
engine.
|
|
||||||
* It supports searching with either memory maps or by searching incrementally
|
|
||||||
with an intermediate buffer. The former is better for single files and the
|
|
||||||
latter is better for large directories. `ripgrep` chooses the best searching
|
|
||||||
strategy for you automatically.
|
|
||||||
* Applies your ignore patterns in `.gitignore` files using a
|
|
||||||
[`RegexSet`](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html).
|
|
||||||
That means a single file path can be matched against multiple glob patterns
|
|
||||||
simultaneously.
|
|
||||||
* Uses a Chase-Lev work-stealing queue for quickly distributing work to
|
|
||||||
multiple threads.
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
The binary name for `ripgrep` is `rg`.
|
|
||||||
|
|
||||||
[Binaries for `ripgrep` are available for Windows, Mac and
|
|
||||||
Linux.](https://github.com/BurntSushi/ripgrep/releases) Linux binaries are
|
|
||||||
static executables. Windows binaries are available either as built with MinGW
|
|
||||||
(GNU) or with Microsoft Visual C++ (MSVC). When possible, prefer MSVC over GNU,
|
|
||||||
but you'll need to have the
|
|
||||||
[Microsoft Visual C++ Build
|
|
||||||
Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools)
|
|
||||||
installed.
|
|
||||||
|
|
||||||
If you're a **Homebrew** user, then you can install it with a custom formula
|
|
||||||
(N.B. `ripgrep` isn't actually in Homebrew yet. This just installs the binary
|
|
||||||
directly):
|
|
||||||
|
|
||||||
```
|
|
||||||
$ brew install https://raw.githubusercontent.com/BurntSushi/ripgrep/master/pkg/brew/ripgrep.rb
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're an **Archlinux** user, then you can install `ripgrep` from the
|
|
||||||
[`ripgrep` AUR package](https://aur.archlinux.org/packages/ripgrep/), e.g.,
|
|
||||||
|
|
||||||
```
|
|
||||||
$ yaourt -S ripgrep
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're a **Rust programmer**, `ripgrep` can be installed with `cargo`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cargo install ripgrep
|
|
||||||
```
|
|
||||||
|
|
||||||
`ripgrep` isn't currently in any other package repositories.
|
|
||||||
[I'd like to change that](https://github.com/BurntSushi/ripgrep/issues/10).
|
|
||||||
|
|
||||||
### Whirlwind tour
|
|
||||||
|
|
||||||
The command line usage of `ripgrep` doesn't differ much from other tools that
|
|
||||||
perform a similar function, so you probably already know how to use `ripgrep`.
|
|
||||||
The full details can be found in `rg --help`, but let's go on a whirlwind tour.
|
|
||||||
|
|
||||||
`ripgrep` detects when its printing to a terminal, and will automatically
|
|
||||||
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)
|
|
||||||
|
|
||||||
To recursively search the current directory, while respecting all `.gitignore`
|
|
||||||
files, ignore hidden files and directories and skip binary files:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg foobar
|
|
||||||
```
|
|
||||||
|
|
||||||
The above command also respects all `.rgignore` files, including in parent
|
|
||||||
directories. `.rgignore` files can be used when `.gitignore` files are
|
|
||||||
insufficient. In all cases, `.rgignore` patterns take precedence over
|
|
||||||
`.gitignore`.
|
|
||||||
|
|
||||||
To ignore all ignore files, use `-u`. To additionally search hidden files
|
|
||||||
and directories, use `-uu`. To additionally search binary files, use `-uuu`.
|
|
||||||
(In other words, "search everything, dammit!") In particular, `rg -uuu` is
|
|
||||||
similar to `grep -a -r`.
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg -uu foobar # similar to `grep -r`
|
|
||||||
$ rg -uuu foobar # similar to `grep -a -r`
|
|
||||||
```
|
|
||||||
|
|
||||||
(Tip: If your ignore files aren't being adhered to like you expect, run your
|
|
||||||
search with the `--debug` flag.)
|
|
||||||
|
|
||||||
Make the search case insensitive with `-i`, invert the search with `-v` or
|
|
||||||
show the 2 lines before and after every search result with `-C2`.
|
|
||||||
|
|
||||||
Force all matches to be surrounded by word boundaries with `-w`.
|
|
||||||
|
|
||||||
Search and replace (find first and last names and swap them):
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg '([A-Z][a-z]+)\s+([A-Z][a-z]+)' --replace '$2, $1'
|
|
||||||
```
|
|
||||||
|
|
||||||
Named groups are supported:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg '(?P<first>[A-Z][a-z]+)\s+(?P<last>[A-Z][a-z]+)' --replace '$last, $first'
|
|
||||||
```
|
|
||||||
|
|
||||||
Up the ante with full Unicode support, by matching any uppercase Unicode letter
|
|
||||||
followed by any sequence of lowercase Unicode letters (good luck doing this
|
|
||||||
with other search tools!):
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg '(\p{Lu}\p{Ll}+)\s+(\p{Lu}\p{Ll}+)' --replace '$2, $1'
|
|
||||||
```
|
|
||||||
|
|
||||||
Search only files matching a particular glob:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg foo -g 'README.*'
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--*-->
|
|
||||||
|
|
||||||
Or exclude files matching a particular glob:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg foo -g '!*.min.js'
|
|
||||||
```
|
|
||||||
|
|
||||||
Search only HTML and CSS files:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg -thtml -tcss foobar
|
|
||||||
```
|
|
||||||
|
|
||||||
Search everything except for Javascript files:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg -Tjs foobar
|
|
||||||
```
|
|
||||||
|
|
||||||
To see a list of types supported, run `rg --type-list`. To add a new type, use
|
|
||||||
`--type-add`:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ rg --type-add 'foo:*.foo,*.foobar'
|
|
||||||
```
|
|
||||||
|
|
||||||
The type `foo` will now match any file ending with the `.foo` or `.foobar`
|
|
||||||
extensions.
|
|
||||||
|
|
||||||
### Regex syntax
|
|
||||||
|
|
||||||
The syntax supported is
|
|
||||||
[documented as part of Rust's regex library](https://doc.rust-lang.org/regex/regex/index.html#syntax).
|
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
`ripgrep` is written in Rust, so you'll need to grab a
|
|
||||||
[Rust installation](https://www.rust-lang.org/) in order to compile it.
|
|
||||||
`ripgrep` compiles with Rust 1.9 (stable) or newer. Building is easy:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git clone git://github.com/BurntSushi/ripgrep
|
|
||||||
$ cd ripgrep
|
|
||||||
$ cargo build --release
|
|
||||||
$ ./target/release/rg --version
|
|
||||||
0.1.3
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have a Rust nightly compiler, then you can enable optional SIMD
|
|
||||||
acceleration like so:
|
|
||||||
|
|
||||||
```
|
|
||||||
RUSTFLAGS="-C target-cpu=native" cargo build --release --features simd-accel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running tests
|
|
||||||
|
|
||||||
`ripgrep` is relatively well tested, including both unit tests and integration
|
|
||||||
tests. To run the full test suite, use:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ cargo test
|
|
||||||
```
|
|
||||||
|
|
||||||
from the repository root.
|
|
271
README.md
271
README.md
@@ -1,6 +1,269 @@
|
|||||||
**UNDER DEVELOPMENT.**
|
|
||||||
|
|
||||||
ripgrep (rg)
|
ripgrep (rg)
|
||||||
------------
|
------------
|
||||||
ripgrep combines the usability of the silver searcher with the raw speed of
|
`ripgrep` is a command line search tool that combines the usability of The
|
||||||
grep.
|
Silver Searcher (an `ack` clone) with the raw speed of GNU grep. `ripgrep` has
|
||||||
|
first class support on Windows, Mac and Linux, with binary downloads available
|
||||||
|
for [every release](https://github.com/BurntSushi/ripgrep/releases).
|
||||||
|
|
||||||
|
[](https://travis-ci.org/BurntSushi/ripgrep)
|
||||||
|
[](https://ci.appveyor.com/project/BurntSushi/ripgrep)
|
||||||
|
[](https://crates.io/crates/ripgrep)
|
||||||
|
|
||||||
|
Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).
|
||||||
|
|
||||||
|
### Screenshot of search results
|
||||||
|
|
||||||
|
[](http://burntsushi.net/stuff/ripgrep1.png)
|
||||||
|
|
||||||
|
### Quick example comparing tools
|
||||||
|
|
||||||
|
This example searches the entire Linux kernel source tree (after running
|
||||||
|
`make defconfig && make -j8`) for `[A-Z]+_SUSPEND`, where all matches must be
|
||||||
|
words. Timings were collected on a system with an Intel i7-6900K 3.2 GHz.
|
||||||
|
|
||||||
|
Please remember that a single benchmark is never enough! See my
|
||||||
|
[blog post on `ripgrep`](http://blog.burntsushi.net/ripgrep/)
|
||||||
|
for a very detailed comparison with more benchmarks and analysis.
|
||||||
|
|
||||||
|
| Tool | Command | Line count | Time |
|
||||||
|
| ---- | ------- | ---------- | ---- |
|
||||||
|
| ripgrep | `rg -n -w '[A-Z]+_SUSPEND'` | 450 | **0.245s** |
|
||||||
|
| [The Silver Searcher](https://github.com/ggreer/the_silver_searcher) | `ag -w '[A-Z]+_SUSPEND'` | 450 | 0.753s |
|
||||||
|
| [git grep](https://www.kernel.org/pub/software/scm/git/docs/git-grep.html) | `LC_ALL=C git grep -E -n -w '[A-Z]+_SUSPEND'` | 450 | 0.823s |
|
||||||
|
| [git grep (Unicode)](https://www.kernel.org/pub/software/scm/git/docs/git-grep.html) | `LC_ALL=en_US.UTF-8 git grep -E -n -w '[A-Z]+_SUSPEND'` | 450 | 2.880s |
|
||||||
|
| [sift](https://github.com/svent/sift) | `sift --git -n -w '[A-Z]+_SUSPEND'` | 450 | 3.656s |
|
||||||
|
| [The Platinum Searcher](https://github.com/monochromegane/the_platinum_searcher) | `pt -w -e '[A-Z]+_SUSPEND'` | 450 | 12.369s |
|
||||||
|
| [ack](http://beyondgrep.com/) | `ack -w '[A-Z]+_SUSPEND'` | 1878 | 16.952s |
|
||||||
|
|
||||||
|
(Yes, `ack` [has](https://github.com/petdance/ack2/issues/445) a
|
||||||
|
[bug](https://github.com/petdance/ack2/issues/14).)
|
||||||
|
|
||||||
|
### Why should I use `ripgrep`?
|
||||||
|
|
||||||
|
* It can replace both The Silver Searcher and GNU grep because it is faster
|
||||||
|
than both. (N.B. It is not, strictly speaking, a "drop-in" replacement for
|
||||||
|
both, but the feature sets are far more similar than different.)
|
||||||
|
* Like The Silver Searcher, `ripgrep` defaults to recursive directory search
|
||||||
|
and won't search files ignored by your `.gitignore` files. It also ignores
|
||||||
|
hidden and binary files by default. `ripgrep` also implements full support
|
||||||
|
for `.gitignore`, where as there are many bugs related to that functionality
|
||||||
|
in The Silver Searcher.
|
||||||
|
* `ripgrep` can search specific types of files. For example, `rg -tpy foo`
|
||||||
|
limits your search to Python files and `rg -Tjs foo` excludes Javascript
|
||||||
|
files from your search. `ripgrep` can be taught about new file types with
|
||||||
|
custom matching rules.
|
||||||
|
* `ripgrep` supports many features found in `grep`, such as showing the context
|
||||||
|
of search results, searching multiple patterns, highlighting matches with
|
||||||
|
color and full Unicode support. Unlike GNU grep, `ripgrep` stays fast while
|
||||||
|
supporting Unicode (which is always on).
|
||||||
|
|
||||||
|
In other words, use `ripgrep` if you like speed, sane defaults, fewer bugs and
|
||||||
|
Unicode.
|
||||||
|
|
||||||
|
### Is it really faster than everything else?
|
||||||
|
|
||||||
|
Yes. A large number of benchmarks with detailed analysis for each is
|
||||||
|
[available on my blog](http://blog.burntsushi.net/ripgrep/).
|
||||||
|
|
||||||
|
Summarizing, `ripgrep` is fast because:
|
||||||
|
|
||||||
|
* It is built on top of
|
||||||
|
[Rust's regex engine](https://github.com/rust-lang-nursery/regex).
|
||||||
|
Rust's regex engine uses finite automata, SIMD and aggressive literal
|
||||||
|
optimizations to make searching very fast.
|
||||||
|
* Rust's regex library maintains performance with full Unicode support by
|
||||||
|
building UTF-8 decoding directly into its deterministic finite automaton
|
||||||
|
engine.
|
||||||
|
* It supports searching with either memory maps or by searching incrementally
|
||||||
|
with an intermediate buffer. The former is better for single files and the
|
||||||
|
latter is better for large directories. `ripgrep` chooses the best searching
|
||||||
|
strategy for you automatically.
|
||||||
|
* Applies your ignore patterns in `.gitignore` files using a
|
||||||
|
[`RegexSet`](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html).
|
||||||
|
That means a single file path can be matched against multiple glob patterns
|
||||||
|
simultaneously.
|
||||||
|
* Uses a Chase-Lev work-stealing queue for quickly distributing work to
|
||||||
|
multiple threads.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
The binary name for `ripgrep` is `rg`.
|
||||||
|
|
||||||
|
[Binaries for `ripgrep` are available for Windows, Mac and
|
||||||
|
Linux.](https://github.com/BurntSushi/ripgrep/releases) Linux binaries are
|
||||||
|
static executables. Windows binaries are available either as built with MinGW
|
||||||
|
(GNU) or with Microsoft Visual C++ (MSVC). When possible, prefer MSVC over GNU,
|
||||||
|
but you'll need to have the
|
||||||
|
[Microsoft VC++ 2015 redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=48145)
|
||||||
|
installed.
|
||||||
|
|
||||||
|
If you're a **Mac OS X Homebrew** user, then you can install ripgrep either
|
||||||
|
from homebrew-core, (compiled with rust stable, no SIMD):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ brew install ripgrep
|
||||||
|
```
|
||||||
|
|
||||||
|
or you can install a binary compiled with rust nightly (including SIMD and all
|
||||||
|
optimizations) by utilizing a custom tap:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ brew tap burntsushi/ripgrep https://github.com/BurntSushi/ripgrep.git
|
||||||
|
$ brew install burntsushi/ripgrep/ripgrep-bin
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're an **Arch Linux** user, then you can install `ripgrep` from the official repos:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pacman -S ripgrep
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're a **Rust programmer**, `ripgrep` can be installed with `cargo`:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cargo install ripgrep
|
||||||
|
```
|
||||||
|
|
||||||
|
`ripgrep` isn't currently in any other package repositories.
|
||||||
|
[I'd like to change that](https://github.com/BurntSushi/ripgrep/issues/10).
|
||||||
|
|
||||||
|
### Whirlwind tour
|
||||||
|
|
||||||
|
The command line usage of `ripgrep` doesn't differ much from other tools that
|
||||||
|
perform a similar function, so you probably already know how to use `ripgrep`.
|
||||||
|
The full details can be found in `rg --help`, but let's go on a whirlwind tour.
|
||||||
|
|
||||||
|
`ripgrep` detects when its printing to a terminal, and will automatically
|
||||||
|
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)
|
||||||
|
|
||||||
|
To recursively search the current directory, while respecting all `.gitignore`
|
||||||
|
files, ignore hidden files and directories and skip binary files:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
The above command also respects all `.ignore` files, including in parent
|
||||||
|
directories. `.ignore` files can be used when `.gitignore` files are
|
||||||
|
insufficient. In all cases, `.ignore` patterns take precedence over
|
||||||
|
`.gitignore`.
|
||||||
|
|
||||||
|
To ignore all ignore files, use `-u`. To additionally search hidden files
|
||||||
|
and directories, use `-uu`. To additionally search binary files, use `-uuu`.
|
||||||
|
(In other words, "search everything, dammit!") In particular, `rg -uuu` is
|
||||||
|
similar to `grep -a -r`.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg -uu foobar # similar to `grep -r`
|
||||||
|
$ rg -uuu foobar # similar to `grep -a -r`
|
||||||
|
```
|
||||||
|
|
||||||
|
(Tip: If your ignore files aren't being adhered to like you expect, run your
|
||||||
|
search with the `--debug` flag.)
|
||||||
|
|
||||||
|
Make the search case insensitive with `-i`, invert the search with `-v` or
|
||||||
|
show the 2 lines before and after every search result with `-C2`.
|
||||||
|
|
||||||
|
Force all matches to be surrounded by word boundaries with `-w`.
|
||||||
|
|
||||||
|
Search and replace (find first and last names and swap them):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg '([A-Z][a-z]+)\s+([A-Z][a-z]+)' --replace '$2, $1'
|
||||||
|
```
|
||||||
|
|
||||||
|
Named groups are supported:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg '(?P<first>[A-Z][a-z]+)\s+(?P<last>[A-Z][a-z]+)' --replace '$last, $first'
|
||||||
|
```
|
||||||
|
|
||||||
|
Up the ante with full Unicode support, by matching any uppercase Unicode letter
|
||||||
|
followed by any sequence of lowercase Unicode letters (good luck doing this
|
||||||
|
with other search tools!):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg '(\p{Lu}\p{Ll}+)\s+(\p{Lu}\p{Ll}+)' --replace '$2, $1'
|
||||||
|
```
|
||||||
|
|
||||||
|
Search only files matching a particular glob:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg foo -g 'README.*'
|
||||||
|
```
|
||||||
|
|
||||||
|
<!--*-->
|
||||||
|
|
||||||
|
Or exclude files matching a particular glob:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg foo -g '!*.min.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
Search only HTML and CSS files:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg -thtml -tcss foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
Search everything except for Javascript files:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg -Tjs foobar
|
||||||
|
```
|
||||||
|
|
||||||
|
To see a list of types supported, run `rg --type-list`. To add a new type, use
|
||||||
|
`--type-add`, which must be accompanied by a pattern for searching (`rg` won't
|
||||||
|
persist your type settings):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ rg --type-add 'foo:*.{foo,foobar}' -tfoo bar
|
||||||
|
```
|
||||||
|
|
||||||
|
The type `foo` will now match any file ending with the `.foo` or `.foobar`
|
||||||
|
extensions.
|
||||||
|
|
||||||
|
### Regex syntax
|
||||||
|
|
||||||
|
The syntax supported is
|
||||||
|
[documented as part of Rust's regex library](https://doc.rust-lang.org/regex/regex/index.html#syntax).
|
||||||
|
|
||||||
|
### Building
|
||||||
|
|
||||||
|
`ripgrep` is written in Rust, so you'll need to grab a
|
||||||
|
[Rust installation](https://www.rust-lang.org/) in order to compile it.
|
||||||
|
`ripgrep` compiles with Rust 1.9 (stable) or newer. Building is easy:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ git clone https://github.com/BurntSushi/ripgrep
|
||||||
|
$ cd ripgrep
|
||||||
|
$ cargo build --release
|
||||||
|
$ ./target/release/rg --version
|
||||||
|
0.1.3
|
||||||
|
```
|
||||||
|
|
||||||
|
If you have a Rust nightly compiler, then you can enable optional SIMD
|
||||||
|
acceleration like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
RUSTFLAGS="-C target-cpu=native" cargo build --release --features simd-accel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
`ripgrep` is relatively well tested, including both unit tests and integration
|
||||||
|
tests. To run the full test suite, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
from the repository root.
|
||||||
|
@@ -28,6 +28,8 @@ build: false
|
|||||||
# TODO modify this phase as you see fit
|
# TODO modify this phase as you see fit
|
||||||
test_script:
|
test_script:
|
||||||
- cargo test --verbose
|
- cargo test --verbose
|
||||||
|
- cargo test --verbose --manifest-path grep/Cargo.toml
|
||||||
|
- cargo test --verbose --manifest-path globset/Cargo.toml
|
||||||
|
|
||||||
before_deploy:
|
before_deploy:
|
||||||
# Generate artifacts for release
|
# Generate artifacts for release
|
||||||
@@ -41,7 +43,7 @@ before_deploy:
|
|||||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
description: 'Windows release'
|
description: 'Automatically deployed release'
|
||||||
# All the zipped artifacts will be deployed
|
# All the zipped artifacts will be deployed
|
||||||
artifact: /.*\.zip/
|
artifact: /.*\.zip/
|
||||||
auth_token:
|
auth_token:
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
These are internal microbenchmarks for tracking the peformance of individual
|
|
||||||
components inside of ripgrep. At the moment, they aren't heavily used.
|
|
||||||
|
|
||||||
For performance benchmarks of ripgrep proper, see the sibling `benchsuite`
|
|
||||||
directory.
|
|
@@ -16,8 +16,13 @@ disable_cross_doctests() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
run_test_suite() {
|
run_test_suite() {
|
||||||
|
cargo clean --target $TARGET --verbose
|
||||||
cargo build --target $TARGET --verbose
|
cargo build --target $TARGET --verbose
|
||||||
cargo test --target $TARGET --verbose
|
cargo test --target $TARGET --verbose
|
||||||
|
cargo build --target $TARGET --verbose --manifest-path grep/Cargo.toml
|
||||||
|
cargo test --target $TARGET --verbose --manifest-path grep/Cargo.toml
|
||||||
|
cargo build --target $TARGET --verbose --manifest-path globset/Cargo.toml
|
||||||
|
cargo test --target $TARGET --verbose --manifest-path globset/Cargo.toml
|
||||||
|
|
||||||
# sanity check the file type
|
# sanity check the file type
|
||||||
file target/$TARGET/debug/rg
|
file target/$TARGET/debug/rg
|
||||||
|
84
doc/rg.1
84
doc/rg.1
@@ -16,9 +16,9 @@ rg [\f[I]options\f[]] \-\-files [\f[I]<\f[]path\f[I]> ...\f[]]
|
|||||||
.PP
|
.PP
|
||||||
rg [\f[I]options\f[]] \-\-type\-list
|
rg [\f[I]options\f[]] \-\-type\-list
|
||||||
.PP
|
.PP
|
||||||
rg \-\-help
|
rg [\f[I]options\f[]] \-\-help
|
||||||
.PP
|
.PP
|
||||||
rg \-\-version
|
rg [\f[I]options\f[]] \-\-version
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
.PP
|
.PP
|
||||||
rg (ripgrep) combines the usability of The Silver Searcher (an ack
|
rg (ripgrep) combines the usability of The Silver Searcher (an ack
|
||||||
@@ -70,6 +70,7 @@ Show this usage message.
|
|||||||
.TP
|
.TP
|
||||||
.B \-i, \-\-ignore\-case
|
.B \-i, \-\-ignore\-case
|
||||||
Case insensitive search.
|
Case insensitive search.
|
||||||
|
Overridden by \-\-case\-sensitive.
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
@@ -86,12 +87,7 @@ Suppress line numbers.
|
|||||||
.TP
|
.TP
|
||||||
.B \-q, \-\-quiet
|
.B \-q, \-\-quiet
|
||||||
Do not print anything to stdout.
|
Do not print anything to stdout.
|
||||||
.RS
|
If a match is found in a file, stop searching that file.
|
||||||
.RE
|
|
||||||
.TP
|
|
||||||
.B \-r, \-\-replace \f[I]ARG\f[]
|
|
||||||
Replace every match with the string given.
|
|
||||||
Capture group indices (e.g., $5) and names (e.g., $foo) are supported.
|
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
@@ -169,12 +165,23 @@ Print each file that would be searched (but don\[aq]t search).
|
|||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-l, \-\-files\-with\-matches
|
||||||
|
Only show path of each file with matches.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-H, \-\-with\-filename
|
.B \-H, \-\-with\-filename
|
||||||
Prefix each match with the file name that contains it.
|
Prefix each match with the file name that contains it.
|
||||||
This is the default when more than one file is searched.
|
This is the default when more than one file is searched.
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-\-no\-filename
|
||||||
|
Never show the filename for a match.
|
||||||
|
This is the default when one file is searched.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-\-heading
|
.B \-\-heading
|
||||||
Show the file name above clusters of matches from each file.
|
Show the file name above clusters of matches from each file.
|
||||||
This is the default mode at a tty.
|
This is the default mode at a tty.
|
||||||
@@ -197,6 +204,12 @@ Follow symlinks.
|
|||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-\-maxdepth \f[I]NUM\f[]
|
||||||
|
Descend at most NUM directories below the command line arguments.
|
||||||
|
A value of zero searches only the starting\-points themselves.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-\-mmap
|
.B \-\-mmap
|
||||||
Search using memory maps when possible.
|
Search using memory maps when possible.
|
||||||
This is enabled by default when ripgrep thinks it will be faster.
|
This is enabled by default when ripgrep thinks it will be faster.
|
||||||
@@ -211,8 +224,8 @@ Never use memory maps, even when they might be faster.
|
|||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.B \-\-no\-ignore
|
.B \-\-no\-ignore
|
||||||
Don\[aq]t respect ignore files (.gitignore, .rgignore, etc.) This
|
Don\[aq]t respect ignore files (.gitignore, .ignore, etc.) This implies
|
||||||
implies \-\-no\-ignore\-parent.
|
\-\-no\-ignore\-parent.
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
@@ -221,11 +234,47 @@ Don\[aq]t respect ignore files in parent directories.
|
|||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-\-no\-ignore\-vcs
|
||||||
|
Don\[aq]t respect version control ignore files (e.g., .gitignore).
|
||||||
|
Note that .ignore files will continue to be respected.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B \-\-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.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-p, \-\-pretty
|
.B \-p, \-\-pretty
|
||||||
Alias for \-\-color=always \-\-heading \-n.
|
Alias for \-\-color=always \-\-heading \-n.
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
|
.B \-r, \-\-replace \f[I]ARG\f[]
|
||||||
|
Replace every match with the string given when printing search results.
|
||||||
|
Neither this flag nor any other flag will modify your files.
|
||||||
|
.RS
|
||||||
|
.PP
|
||||||
|
Capture group indices (e.g., $5) and names (e.g., $foo) are supported in
|
||||||
|
the replacement string.
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B \-s, \-\-case\-sensitive
|
||||||
|
Search case sensitively.
|
||||||
|
This overrides \-\-ignore\-case and \-\-smart\-case.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
|
.B \-S, \-\-smart\-case
|
||||||
|
Search case insensitively if the pattern is all lowercase.
|
||||||
|
Search case sensitively otherwise.
|
||||||
|
This is overridden by either \-\-case\-sensitive or \-\-ignore\-case.
|
||||||
|
.RS
|
||||||
|
.RE
|
||||||
|
.TP
|
||||||
.B \-j, \-\-threads \f[I]ARG\f[]
|
.B \-j, \-\-threads \f[I]ARG\f[]
|
||||||
The number of threads to use.
|
The number of threads to use.
|
||||||
Defaults to the number of logical CPUs (capped at 6).
|
Defaults to the number of logical CPUs (capped at 6).
|
||||||
@@ -254,11 +303,22 @@ Show all supported file types and their associated globs.
|
|||||||
.TP
|
.TP
|
||||||
.B \-\-type\-add \f[I]ARG\f[] ...
|
.B \-\-type\-add \f[I]ARG\f[] ...
|
||||||
Add a new glob for a particular file type.
|
Add a new glob for a particular file type.
|
||||||
Example: \-\-type\-add html:\f[I]\&.html,\f[].htm
|
Only one glob can be added at a time.
|
||||||
|
Multiple \-\-type\-add flags can be provided.
|
||||||
|
Unless \-\-type\-clear is used, globs are added to any existing globs
|
||||||
|
inside of ripgrep.
|
||||||
|
Note that this must be passed to every invocation of rg.
|
||||||
|
Type settings are NOT persisted.
|
||||||
.RS
|
.RS
|
||||||
|
.PP
|
||||||
|
Example:
|
||||||
|
\f[C]rg\ \-\-type\-add\ \[aq]foo:*.foo\[aq]\ \-tfoo\ PATTERN\f[]
|
||||||
.RE
|
.RE
|
||||||
.TP
|
.TP
|
||||||
.B \-\-type\-clear \f[I]TYPE\f[] ...
|
.B \-\-type\-clear \f[I]TYPE\f[] ...
|
||||||
Clear the file type globs for TYPE.
|
Clear the file type globs previously defined for TYPE.
|
||||||
|
This only clears the default type definitions that are found inside of
|
||||||
|
ripgrep.
|
||||||
|
Note that this must be passed to every invocation of rg.
|
||||||
.RS
|
.RS
|
||||||
.RE
|
.RE
|
||||||
|
64
doc/rg.1.md
64
doc/rg.1.md
@@ -12,9 +12,9 @@ rg [*options*] --files [*<*path*> ...*]
|
|||||||
|
|
||||||
rg [*options*] --type-list
|
rg [*options*] --type-list
|
||||||
|
|
||||||
rg --help
|
rg [*options*] --help
|
||||||
|
|
||||||
rg --version
|
rg [*options*] --version
|
||||||
|
|
||||||
# DESCRIPTION
|
# DESCRIPTION
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ the raw speed of grep.
|
|||||||
: Show this usage message.
|
: Show this usage message.
|
||||||
|
|
||||||
-i, --ignore-case
|
-i, --ignore-case
|
||||||
: Case insensitive search.
|
: Case insensitive search. Overridden by --case-sensitive.
|
||||||
|
|
||||||
-n, --line-number
|
-n, --line-number
|
||||||
: Show line numbers (1-based). This is enabled by default at a tty.
|
: Show line numbers (1-based). This is enabled by default at a tty.
|
||||||
@@ -58,11 +58,8 @@ the raw speed of grep.
|
|||||||
: Suppress line numbers.
|
: Suppress line numbers.
|
||||||
|
|
||||||
-q, --quiet
|
-q, --quiet
|
||||||
: Do not print anything to stdout.
|
: Do not print anything to stdout. If a match is found in a file, stop
|
||||||
|
searching that file.
|
||||||
-r, --replace *ARG*
|
|
||||||
: Replace every match with the string given. Capture group indices (e.g., $5)
|
|
||||||
and names (e.g., $foo) are supported.
|
|
||||||
|
|
||||||
-t, --type *TYPE* ...
|
-t, --type *TYPE* ...
|
||||||
: Only search files matching TYPE. Multiple type flags may be provided. Use the
|
: Only search files matching TYPE. Multiple type flags may be provided. Use the
|
||||||
@@ -110,10 +107,17 @@ the raw speed of grep.
|
|||||||
--files
|
--files
|
||||||
: Print each file that would be searched (but don't search).
|
: Print each file that would be searched (but don't search).
|
||||||
|
|
||||||
|
-l, --files-with-matches
|
||||||
|
: Only show path of each file with matches.
|
||||||
|
|
||||||
-H, --with-filename
|
-H, --with-filename
|
||||||
: Prefix each match with the file name that contains it. This is the
|
: Prefix each match with the file name that contains it. This is the
|
||||||
default when more than one file is searched.
|
default when more than one file is searched.
|
||||||
|
|
||||||
|
--no-filename
|
||||||
|
: Never show the filename for a match. This is the default when
|
||||||
|
one file is searched.
|
||||||
|
|
||||||
--heading
|
--heading
|
||||||
: Show the file name above clusters of matches from each file.
|
: Show the file name above clusters of matches from each file.
|
||||||
This is the default mode at a tty.
|
This is the default mode at a tty.
|
||||||
@@ -128,6 +132,10 @@ the raw speed of grep.
|
|||||||
-L, --follow
|
-L, --follow
|
||||||
: Follow symlinks.
|
: Follow symlinks.
|
||||||
|
|
||||||
|
--maxdepth *NUM*
|
||||||
|
: Descend at most NUM directories below the command line arguments.
|
||||||
|
A value of zero searches only the starting-points themselves.
|
||||||
|
|
||||||
--mmap
|
--mmap
|
||||||
: Search using memory maps when possible. This is enabled by default
|
: Search using memory maps when possible. This is enabled by default
|
||||||
when ripgrep thinks it will be faster. (Note that mmap searching
|
when ripgrep thinks it will be faster. (Note that mmap searching
|
||||||
@@ -137,15 +145,40 @@ the raw speed of grep.
|
|||||||
: Never use memory maps, even when they might be faster.
|
: Never use memory maps, even when they might be faster.
|
||||||
|
|
||||||
--no-ignore
|
--no-ignore
|
||||||
: Don't respect ignore files (.gitignore, .rgignore, etc.)
|
: Don't respect ignore files (.gitignore, .ignore, etc.)
|
||||||
This implies --no-ignore-parent.
|
This implies --no-ignore-parent.
|
||||||
|
|
||||||
--no-ignore-parent
|
--no-ignore-parent
|
||||||
: Don't respect ignore files in parent directories.
|
: Don't respect ignore files in parent directories.
|
||||||
|
|
||||||
|
--no-ignore-vcs
|
||||||
|
: Don't respect version control ignore files (e.g., .gitignore).
|
||||||
|
Note that .ignore files will continue to be respected.
|
||||||
|
|
||||||
|
--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.
|
||||||
|
|
||||||
-p, --pretty
|
-p, --pretty
|
||||||
: Alias for --color=always --heading -n.
|
: Alias for --color=always --heading -n.
|
||||||
|
|
||||||
|
-r, --replace *ARG*
|
||||||
|
: Replace every match with the string given when printing search results.
|
||||||
|
Neither this flag nor any other flag will modify your files.
|
||||||
|
|
||||||
|
Capture group indices (e.g., $5) and names (e.g., $foo) are supported
|
||||||
|
in the replacement string.
|
||||||
|
|
||||||
|
-s, --case-sensitive
|
||||||
|
: Search case sensitively. This overrides --ignore-case and --smart-case.
|
||||||
|
|
||||||
|
-S, --smart-case
|
||||||
|
: Search case insensitively if the pattern is all lowercase.
|
||||||
|
Search case sensitively otherwise. This is overridden by either
|
||||||
|
--case-sensitive or --ignore-case.
|
||||||
|
|
||||||
-j, --threads *ARG*
|
-j, --threads *ARG*
|
||||||
: The number of threads to use. Defaults to the number of logical CPUs
|
: The number of threads to use. Defaults to the number of logical CPUs
|
||||||
(capped at 6). [default: 0]
|
(capped at 6). [default: 0]
|
||||||
@@ -164,8 +197,15 @@ the raw speed of grep.
|
|||||||
: Show all supported file types and their associated globs.
|
: Show all supported file types and their associated globs.
|
||||||
|
|
||||||
--type-add *ARG* ...
|
--type-add *ARG* ...
|
||||||
: Add a new glob for a particular file type.
|
: Add a new glob for a particular file type. Only one glob can be added
|
||||||
Example: --type-add html:*.html,*.htm
|
at a time. Multiple --type-add flags can be provided. Unless --type-clear
|
||||||
|
is used, globs are added to any existing globs inside of ripgrep. Note that
|
||||||
|
this must be passed to every invocation of rg. Type settings are NOT
|
||||||
|
persisted.
|
||||||
|
|
||||||
|
Example: `rg --type-add 'foo:*.foo' -tfoo PATTERN`
|
||||||
|
|
||||||
--type-clear *TYPE* ...
|
--type-clear *TYPE* ...
|
||||||
: Clear the file type globs for TYPE.
|
: Clear the file type globs previously defined for TYPE. This only clears
|
||||||
|
the default type definitions that are found inside of ripgrep. Note
|
||||||
|
that this must be passed to every invocation of rg.
|
||||||
|
30
globset/Cargo.toml
Normal file
30
globset/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.1.0" #:version
|
||||||
|
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||||
|
description = """
|
||||||
|
Cross platform single glob and glob set matching. Glob set matching is the
|
||||||
|
process of matching one or more glob patterns against a single candidate path
|
||||||
|
simultaneously, and returning all of the globs that matched.
|
||||||
|
"""
|
||||||
|
documentation = "https://docs.rs/globset"
|
||||||
|
homepage = "https://github.com/BurntSushi/ripgrep/tree/master/globset"
|
||||||
|
repository = "https://github.com/BurntSushi/ripgrep/tree/master/globset"
|
||||||
|
readme = "README.md"
|
||||||
|
keywords = ["regex", "glob", "multiple", "set", "pattern"]
|
||||||
|
license = "Unlicense/MIT"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "globset"
|
||||||
|
bench = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
aho-corasick = "0.5.3"
|
||||||
|
fnv = "1.0"
|
||||||
|
lazy_static = "0.2"
|
||||||
|
log = "0.3"
|
||||||
|
memchr = "0.1"
|
||||||
|
regex = "0.1.77"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
glob = "0.2"
|
122
globset/README.md
Normal file
122
globset/README.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
globset
|
||||||
|
=======
|
||||||
|
Cross platform single glob and glob set matching. Glob set matching is the
|
||||||
|
process of matching one or more glob patterns against a single candidate path
|
||||||
|
simultaneously, and returning all of the globs that matched.
|
||||||
|
|
||||||
|
[](https://travis-ci.org/BurntSushi/ripgrep)
|
||||||
|
[](https://ci.appveyor.com/project/BurntSushi/ripgrep)
|
||||||
|
[](https://crates.io/crates/globset)
|
||||||
|
|
||||||
|
Dual-licensed under MIT or the [UNLICENSE](http://unlicense.org).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
[https://docs.rs/globset](https://docs.rs/globset)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Add this to your `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
globset = "0.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
and this to your crate root:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
extern crate globset;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: one glob
|
||||||
|
|
||||||
|
This example shows how to match a single glob against a single file path.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use globset::Glob;
|
||||||
|
|
||||||
|
let glob = try!(Glob::new("*.rs")).compile_matcher();
|
||||||
|
|
||||||
|
assert!(glob.is_match("foo.rs"));
|
||||||
|
assert!(glob.is_match("foo/bar.rs"));
|
||||||
|
assert!(!glob.is_match("Cargo.toml"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: configuring a glob matcher
|
||||||
|
|
||||||
|
This example shows how to use a `GlobBuilder` to configure aspects of match
|
||||||
|
semantics. In this example, we prevent wildcards from matching path separators.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use globset::GlobBuilder;
|
||||||
|
|
||||||
|
let glob = try!(GlobBuilder::new("*.rs")
|
||||||
|
.literal_separator(true).build()).compile_matcher();
|
||||||
|
|
||||||
|
assert!(glob.is_match("foo.rs"));
|
||||||
|
assert!(!glob.is_match("foo/bar.rs")); // no longer matches
|
||||||
|
assert!(!glob.is_match("Cargo.toml"));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: match multiple globs at once
|
||||||
|
|
||||||
|
This example shows how to match multiple glob patterns at once.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use globset::{Glob, GlobSetBuilder};
|
||||||
|
|
||||||
|
let mut builder = GlobSetBuilder::new();
|
||||||
|
// A GlobBuilder can be used to configure each glob's match semantics
|
||||||
|
// independently.
|
||||||
|
builder.add(try!(Glob::new("*.rs")));
|
||||||
|
builder.add(try!(Glob::new("src/lib.rs")));
|
||||||
|
builder.add(try!(Glob::new("src/**/foo.rs")));
|
||||||
|
let set = try!(builder.build());
|
||||||
|
|
||||||
|
assert_eq!(set.matches("src/bar/baz/foo.rs"), vec![0, 2]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
This crate implements globs by converting them to regular expressions, and
|
||||||
|
executing them with the
|
||||||
|
[`regex`](https://github.com/rust-lang-nursery/regex)
|
||||||
|
crate.
|
||||||
|
|
||||||
|
For single glob matching, performance of this crate should be roughly on par
|
||||||
|
with the performance of the
|
||||||
|
[`glob`](https://github.com/rust-lang-nursery/glob)
|
||||||
|
crate. (`*_regex` correspond to benchmarks for this library while `*_glob`
|
||||||
|
correspond to benchmarks for the `glob` library.)
|
||||||
|
Optimizations in the `regex` crate may propel this library past `glob`,
|
||||||
|
particularly when matching longer paths.
|
||||||
|
|
||||||
|
```
|
||||||
|
test ext_glob ... bench: 425 ns/iter (+/- 21)
|
||||||
|
test ext_regex ... bench: 175 ns/iter (+/- 10)
|
||||||
|
test long_glob ... bench: 182 ns/iter (+/- 11)
|
||||||
|
test long_regex ... bench: 173 ns/iter (+/- 10)
|
||||||
|
test short_glob ... bench: 69 ns/iter (+/- 4)
|
||||||
|
test short_regex ... bench: 83 ns/iter (+/- 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
The primary performance advantage of this crate is when matching multiple
|
||||||
|
globs against a single path. With the `glob` crate, one must match each glob
|
||||||
|
synchronously, one after the other. In this crate, many can be matched
|
||||||
|
simultaneously. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
test many_short_glob ... bench: 1,063 ns/iter (+/- 47)
|
||||||
|
test many_short_regex_set ... bench: 186 ns/iter (+/- 11)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Comparison with the [`glob`](https://github.com/rust-lang-nursery/glob) crate
|
||||||
|
|
||||||
|
* Supports alternate "or" globs, e.g., `*.{foo,bar}`.
|
||||||
|
* Can match non-UTF-8 file paths correctly.
|
||||||
|
* Supports matching multiple globs at once.
|
||||||
|
* Doesn't provide a recursive directory iterator of matching file paths,
|
||||||
|
although I believe this crate should grow one eventually.
|
||||||
|
* Supports case insensitive and require-literal-separator match options, but
|
||||||
|
**doesn't** support the require-literal-leading-dot option.
|
@@ -5,37 +5,50 @@ tool itself, see the benchsuite directory.
|
|||||||
#![feature(test)]
|
#![feature(test)]
|
||||||
|
|
||||||
extern crate glob;
|
extern crate glob;
|
||||||
|
extern crate globset;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate lazy_static;
|
extern crate lazy_static;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
extern crate test;
|
extern crate test;
|
||||||
|
|
||||||
|
use globset::{Candidate, Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||||
|
|
||||||
|
const EXT: &'static str = "some/a/bigger/path/to/the/crazy/needle.txt";
|
||||||
|
const EXT_PAT: &'static str = "*.txt";
|
||||||
|
|
||||||
const SHORT: &'static str = "some/needle.txt";
|
const SHORT: &'static str = "some/needle.txt";
|
||||||
const SHORT_PAT: &'static str = "some/**/needle.txt";
|
const SHORT_PAT: &'static str = "some/**/needle.txt";
|
||||||
|
|
||||||
const LONG: &'static str = "some/a/bigger/path/to/the/crazy/needle.txt";
|
const LONG: &'static str = "some/a/bigger/path/to/the/crazy/needle.txt";
|
||||||
const LONG_PAT: &'static str = "some/**/needle.txt";
|
const LONG_PAT: &'static str = "some/**/needle.txt";
|
||||||
|
|
||||||
#[allow(dead_code, unused_variables)]
|
|
||||||
#[path = "../src/glob.rs"]
|
|
||||||
mod reglob;
|
|
||||||
|
|
||||||
fn new_glob(pat: &str) -> glob::Pattern {
|
fn new_glob(pat: &str) -> glob::Pattern {
|
||||||
glob::Pattern::new(pat).unwrap()
|
glob::Pattern::new(pat).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_reglob(pat: &str) -> reglob::Set {
|
fn new_reglob(pat: &str) -> GlobMatcher {
|
||||||
let mut builder = reglob::SetBuilder::new();
|
Glob::new(pat).unwrap().compile_matcher()
|
||||||
builder.add(pat).unwrap();
|
}
|
||||||
|
|
||||||
|
fn new_reglob_many(pats: &[&str]) -> GlobSet {
|
||||||
|
let mut builder = GlobSetBuilder::new();
|
||||||
|
for pat in pats {
|
||||||
|
builder.add(Glob::new(pat).unwrap());
|
||||||
|
}
|
||||||
builder.build().unwrap()
|
builder.build().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_reglob_many(pats: &[&str]) -> reglob::Set {
|
#[bench]
|
||||||
let mut builder = reglob::SetBuilder::new();
|
fn ext_glob(b: &mut test::Bencher) {
|
||||||
for pat in pats {
|
let pat = new_glob(EXT_PAT);
|
||||||
builder.add(pat).unwrap();
|
b.iter(|| assert!(pat.matches(EXT)));
|
||||||
}
|
}
|
||||||
builder.build().unwrap()
|
|
||||||
|
#[bench]
|
||||||
|
fn ext_regex(b: &mut test::Bencher) {
|
||||||
|
let set = new_reglob(EXT_PAT);
|
||||||
|
let cand = Candidate::new(EXT);
|
||||||
|
b.iter(|| assert!(set.is_match_candidate(&cand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
@@ -47,7 +60,8 @@ fn short_glob(b: &mut test::Bencher) {
|
|||||||
#[bench]
|
#[bench]
|
||||||
fn short_regex(b: &mut test::Bencher) {
|
fn short_regex(b: &mut test::Bencher) {
|
||||||
let set = new_reglob(SHORT_PAT);
|
let set = new_reglob(SHORT_PAT);
|
||||||
b.iter(|| assert!(set.is_match(SHORT)));
|
let cand = Candidate::new(SHORT);
|
||||||
|
b.iter(|| assert!(set.is_match_candidate(&cand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[bench]
|
#[bench]
|
||||||
@@ -59,7 +73,8 @@ fn long_glob(b: &mut test::Bencher) {
|
|||||||
#[bench]
|
#[bench]
|
||||||
fn long_regex(b: &mut test::Bencher) {
|
fn long_regex(b: &mut test::Bencher) {
|
||||||
let set = new_reglob(LONG_PAT);
|
let set = new_reglob(LONG_PAT);
|
||||||
b.iter(|| assert!(set.is_match(LONG)));
|
let cand = Candidate::new(LONG);
|
||||||
|
b.iter(|| assert!(set.is_match_candidate(&cand)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const MANY_SHORT_GLOBS: &'static [&'static str] = &[
|
const MANY_SHORT_GLOBS: &'static [&'static str] = &[
|
||||||
@@ -101,26 +116,3 @@ fn many_short_regex_set(b: &mut test::Bencher) {
|
|||||||
let set = new_reglob_many(MANY_SHORT_GLOBS);
|
let set = new_reglob_many(MANY_SHORT_GLOBS);
|
||||||
b.iter(|| assert_eq!(2, set.matches(MANY_SHORT_SEARCH).iter().count()));
|
b.iter(|| assert_eq!(2, set.matches(MANY_SHORT_SEARCH).iter().count()));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is the fastest on my system (beating many_glob by about 2x). This
|
|
||||||
// suggests that a RegexSet needs quite a few regexes (or a larger haystack)
|
|
||||||
// in order for it to scale.
|
|
||||||
//
|
|
||||||
// TODO(burntsushi): come up with a benchmark that uses more complex patterns
|
|
||||||
// or a longer haystack.
|
|
||||||
#[bench]
|
|
||||||
fn many_short_regex_pattern(b: &mut test::Bencher) {
|
|
||||||
let pats: Vec<_> = MANY_SHORT_GLOBS.iter().map(|&s| {
|
|
||||||
let pat = reglob::Pattern::new(s).unwrap();
|
|
||||||
regex::Regex::new(&pat.to_regex()).unwrap()
|
|
||||||
}).collect();
|
|
||||||
b.iter(|| {
|
|
||||||
let mut count = 0;
|
|
||||||
for pat in &pats {
|
|
||||||
if pat.is_match(MANY_SHORT_SEARCH) {
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert_eq!(2, count);
|
|
||||||
})
|
|
||||||
}
|
|
1300
globset/src/glob.rs
Normal file
1300
globset/src/glob.rs
Normal file
File diff suppressed because it is too large
Load Diff
753
globset/src/lib.rs
Normal file
753
globset/src/lib.rs
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
/*!
|
||||||
|
The globset crate provides cross platform single glob and glob set matching.
|
||||||
|
|
||||||
|
Glob set matching is the process of matching one or more glob patterns against
|
||||||
|
a single candidate path simultaneously, and returning all of the globs that
|
||||||
|
matched. For example, given this set of globs:
|
||||||
|
|
||||||
|
```ignore
|
||||||
|
*.rs
|
||||||
|
src/lib.rs
|
||||||
|
src/**/foo.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
and a path `src/bar/baz/foo.rs`, then the set would report the first and third
|
||||||
|
globs as matching.
|
||||||
|
|
||||||
|
Single glob matching is also provided and is done by converting globs to
|
||||||
|
|
||||||
|
# Example: one glob
|
||||||
|
|
||||||
|
This example shows how to match a single glob against a single file path.
|
||||||
|
|
||||||
|
```
|
||||||
|
# fn example() -> Result<(), globset::Error> {
|
||||||
|
use globset::Glob;
|
||||||
|
|
||||||
|
let glob = try!(Glob::new("*.rs")).compile_matcher();
|
||||||
|
|
||||||
|
assert!(glob.is_match("foo.rs"));
|
||||||
|
assert!(glob.is_match("foo/bar.rs"));
|
||||||
|
assert!(!glob.is_match("Cargo.toml"));
|
||||||
|
# Ok(()) } example().unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
# Example: configuring a glob matcher
|
||||||
|
|
||||||
|
This example shows how to use a `GlobBuilder` to configure aspects of match
|
||||||
|
semantics. In this example, we prevent wildcards from matching path separators.
|
||||||
|
|
||||||
|
```
|
||||||
|
# fn example() -> Result<(), globset::Error> {
|
||||||
|
use globset::GlobBuilder;
|
||||||
|
|
||||||
|
let glob = try!(GlobBuilder::new("*.rs")
|
||||||
|
.literal_separator(true).build()).compile_matcher();
|
||||||
|
|
||||||
|
assert!(glob.is_match("foo.rs"));
|
||||||
|
assert!(!glob.is_match("foo/bar.rs")); // no longer matches
|
||||||
|
assert!(!glob.is_match("Cargo.toml"));
|
||||||
|
# Ok(()) } example().unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
# Example: match multiple globs at once
|
||||||
|
|
||||||
|
This example shows how to match multiple glob patterns at once.
|
||||||
|
|
||||||
|
```
|
||||||
|
# fn example() -> Result<(), globset::Error> {
|
||||||
|
use globset::{Glob, GlobSetBuilder};
|
||||||
|
|
||||||
|
let mut builder = GlobSetBuilder::new();
|
||||||
|
// A GlobBuilder can be used to configure each glob's match semantics
|
||||||
|
// independently.
|
||||||
|
builder.add(try!(Glob::new("*.rs")));
|
||||||
|
builder.add(try!(Glob::new("src/lib.rs")));
|
||||||
|
builder.add(try!(Glob::new("src/**/foo.rs")));
|
||||||
|
let set = try!(builder.build());
|
||||||
|
|
||||||
|
assert_eq!(set.matches("src/bar/baz/foo.rs"), vec![0, 2]);
|
||||||
|
# Ok(()) } example().unwrap();
|
||||||
|
```
|
||||||
|
|
||||||
|
# Syntax
|
||||||
|
|
||||||
|
Standard Unix-style glob syntax is supported:
|
||||||
|
|
||||||
|
* `?` matches any single character. (If the `literal_separator` option is
|
||||||
|
enabled, then `?` can never match a path separator.)
|
||||||
|
* `*` matches zero or more characters. (If the `literal_separator` option is
|
||||||
|
enabled, then `*` can never match a path separator.)
|
||||||
|
* `**` recursively matches directories but are only legal in three situations.
|
||||||
|
First, if the glob starts with <code>\*\*/</code>, then it matches
|
||||||
|
all directories. For example, <code>\*\*/foo</code> matches `foo`
|
||||||
|
and `bar/foo` but not `foo/bar`. Secondly, if the glob ends with
|
||||||
|
<code>/\*\*</code>, then it matches all sub-entries. For example,
|
||||||
|
<code>foo/\*\*</code> matches `foo/a` and `foo/a/b`, but not `foo`.
|
||||||
|
Thirdly, if the glob contains <code>/\*\*/</code> anywhere within
|
||||||
|
the pattern, then it matches zero or more directories. Using `**` anywhere
|
||||||
|
else is illegal (N.B. the glob `**` is allowed and means "match everything").
|
||||||
|
* `{a,b}` matches `a` or `b` where `a` and `b` are arbitrary glob patterns.
|
||||||
|
(N.B. Nesting `{...}` is not currently allowed.)
|
||||||
|
* `[ab]` matches `a` or `b` where `a` and `b` are characters. Use
|
||||||
|
`[!ab]` to match any character except for `a` and `b`.
|
||||||
|
* Metacharacters such as `*` and `?` can be escaped with character class
|
||||||
|
notation. e.g., `[*]` matches `*`.
|
||||||
|
|
||||||
|
A `GlobBuilder` can be used to prevent wildcards from matching path separators,
|
||||||
|
or to enable case insensitive matching.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
extern crate aho_corasick;
|
||||||
|
extern crate fnv;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
extern crate memchr;
|
||||||
|
extern crate regex;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::error::Error as StdError;
|
||||||
|
use std::ffi::{OsStr, OsString};
|
||||||
|
use std::fmt;
|
||||||
|
use std::hash;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::str;
|
||||||
|
|
||||||
|
use aho_corasick::{Automaton, AcAutomaton, FullAcAutomaton};
|
||||||
|
use regex::bytes::{Regex, RegexBuilder, RegexSet};
|
||||||
|
|
||||||
|
use pathutil::{
|
||||||
|
file_name, file_name_ext, normalize_path, os_str_bytes, path_bytes,
|
||||||
|
};
|
||||||
|
use glob::MatchStrategy;
|
||||||
|
pub use glob::{Glob, GlobBuilder, GlobMatcher};
|
||||||
|
|
||||||
|
mod glob;
|
||||||
|
mod pathutil;
|
||||||
|
|
||||||
|
macro_rules! eprintln {
|
||||||
|
($($tt:tt)*) => {{
|
||||||
|
use std::io::Write;
|
||||||
|
let _ = writeln!(&mut ::std::io::stderr(), $($tt)*);
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an error that can occur when parsing a glob pattern.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Occurs when a use of `**` is invalid. Namely, `**` can only appear
|
||||||
|
/// adjacent to a path separator, or the beginning/end of a glob.
|
||||||
|
InvalidRecursive,
|
||||||
|
/// Occurs when a character class (e.g., `[abc]`) is not closed.
|
||||||
|
UnclosedClass,
|
||||||
|
/// Occurs when a range in a character (e.g., `[a-z]`) is invalid. For
|
||||||
|
/// example, if the range starts with a lexicographically larger character
|
||||||
|
/// than it ends with.
|
||||||
|
InvalidRange(char, char),
|
||||||
|
/// Occurs when a `}` is found without a matching `{`.
|
||||||
|
UnopenedAlternates,
|
||||||
|
/// Occurs when a `{` is found without a matching `}`.
|
||||||
|
UnclosedAlternates,
|
||||||
|
/// Occurs when an alternating group is nested inside another alternating
|
||||||
|
/// group, e.g., `{{a,b},{c,d}}`.
|
||||||
|
NestedAlternates,
|
||||||
|
/// An error associated with parsing or compiling a regex.
|
||||||
|
Regex(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StdError for Error {
|
||||||
|
fn description(&self) -> &str {
|
||||||
|
match *self {
|
||||||
|
Error::InvalidRecursive => {
|
||||||
|
"invalid use of **; must be one path component"
|
||||||
|
}
|
||||||
|
Error::UnclosedClass => {
|
||||||
|
"unclosed character class; missing ']'"
|
||||||
|
}
|
||||||
|
Error::InvalidRange(_, _) => {
|
||||||
|
"invalid character range"
|
||||||
|
}
|
||||||
|
Error::UnopenedAlternates => {
|
||||||
|
"unopened alternate group; missing '{' \
|
||||||
|
(maybe escape '}' with '[}]'?)"
|
||||||
|
}
|
||||||
|
Error::UnclosedAlternates => {
|
||||||
|
"unclosed alternate group; missing '}' \
|
||||||
|
(maybe escape '{' with '[{]'?)"
|
||||||
|
}
|
||||||
|
Error::NestedAlternates => {
|
||||||
|
"nested alternate groups are not allowed"
|
||||||
|
}
|
||||||
|
Error::Regex(ref err) => err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Error::InvalidRecursive
|
||||||
|
| Error::UnclosedClass
|
||||||
|
| Error::UnopenedAlternates
|
||||||
|
| Error::UnclosedAlternates
|
||||||
|
| Error::NestedAlternates
|
||||||
|
| Error::Regex(_) => {
|
||||||
|
write!(f, "{}", self.description())
|
||||||
|
}
|
||||||
|
Error::InvalidRange(s, e) => {
|
||||||
|
write!(f, "invalid range; '{}' > '{}'", s, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_regex(pat: &str) -> Result<Regex, Error> {
|
||||||
|
RegexBuilder::new(pat)
|
||||||
|
.dot_matches_new_line(true)
|
||||||
|
.size_limit(10 * (1 << 20))
|
||||||
|
.dfa_size_limit(10 * (1 << 20))
|
||||||
|
.compile()
|
||||||
|
.map_err(|err| Error::Regex(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_regex_set<I, S>(pats: I) -> Result<RegexSet, Error>
|
||||||
|
where S: AsRef<str>, I: IntoIterator<Item=S> {
|
||||||
|
RegexSet::new(pats).map_err(|err| Error::Regex(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Fnv = hash::BuildHasherDefault<fnv::FnvHasher>;
|
||||||
|
|
||||||
|
/// GlobSet represents a group of globs that can be matched together in a
|
||||||
|
/// single pass.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GlobSet {
|
||||||
|
strats: Vec<GlobSetMatchStrategy>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobSet {
|
||||||
|
/// Returns true if any glob in this set matches the path given.
|
||||||
|
pub fn is_match<P: AsRef<Path>>(&self, path: P) -> bool {
|
||||||
|
self.is_match_candidate(&Candidate::new(path.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if any glob in this set matches the path given.
|
||||||
|
///
|
||||||
|
/// This takes a Candidate as input, which can be used to amortize the
|
||||||
|
/// cost of preparing a path for matching.
|
||||||
|
pub fn is_match_candidate(&self, path: &Candidate) -> bool {
|
||||||
|
for strat in &self.strats {
|
||||||
|
if strat.is_match(path) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sequence number of every glob pattern that matches the
|
||||||
|
/// given path.
|
||||||
|
///
|
||||||
|
/// This takes a Candidate as input, which can be used to amortize the
|
||||||
|
/// cost of preparing a path for matching.
|
||||||
|
pub fn matches<P: AsRef<Path>>(&self, path: P) -> Vec<usize> {
|
||||||
|
self.matches_candidate(&Candidate::new(path.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sequence number of every glob pattern that matches the
|
||||||
|
/// given path.
|
||||||
|
///
|
||||||
|
/// This takes a Candidate as input, which can be used to amortize the
|
||||||
|
/// cost of preparing a path for matching.
|
||||||
|
pub fn matches_candidate(&self, path: &Candidate) -> Vec<usize> {
|
||||||
|
let mut into = vec![];
|
||||||
|
self.matches_candidate_into(path, &mut into);
|
||||||
|
into
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the sequence number of every glob pattern that matches the given
|
||||||
|
/// path to the vec given.
|
||||||
|
///
|
||||||
|
/// `into` is is cleared before matching begins, and contains the set of
|
||||||
|
/// sequence numbers (in ascending order) after matching ends. If no globs
|
||||||
|
/// were matched, then `into` will be empty.
|
||||||
|
pub fn matches_candidate_into(
|
||||||
|
&self,
|
||||||
|
path: &Candidate,
|
||||||
|
into: &mut Vec<usize>,
|
||||||
|
) {
|
||||||
|
into.clear();
|
||||||
|
for strat in &self.strats {
|
||||||
|
strat.matches_into(path, into);
|
||||||
|
}
|
||||||
|
into.sort();
|
||||||
|
into.dedup();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(pats: &[Glob]) -> Result<GlobSet, Error> {
|
||||||
|
let mut lits = LiteralStrategy::new();
|
||||||
|
let mut base_lits = BasenameLiteralStrategy::new();
|
||||||
|
let mut exts = ExtensionStrategy::new();
|
||||||
|
let mut prefixes = MultiStrategyBuilder::new();
|
||||||
|
let mut suffixes = MultiStrategyBuilder::new();
|
||||||
|
let mut required_exts = RequiredExtensionStrategyBuilder::new();
|
||||||
|
let mut regexes = MultiStrategyBuilder::new();
|
||||||
|
for (i, p) in pats.iter().enumerate() {
|
||||||
|
match MatchStrategy::new(p) {
|
||||||
|
MatchStrategy::Literal(lit) => {
|
||||||
|
lits.add(i, lit);
|
||||||
|
}
|
||||||
|
MatchStrategy::BasenameLiteral(lit) => {
|
||||||
|
base_lits.add(i, lit);
|
||||||
|
}
|
||||||
|
MatchStrategy::Extension(ext) => {
|
||||||
|
exts.add(i, ext);
|
||||||
|
}
|
||||||
|
MatchStrategy::Prefix(prefix) => {
|
||||||
|
prefixes.add(i, prefix);
|
||||||
|
}
|
||||||
|
MatchStrategy::Suffix { suffix, component } => {
|
||||||
|
if component {
|
||||||
|
lits.add(i, suffix[1..].to_string());
|
||||||
|
}
|
||||||
|
suffixes.add(i, suffix);
|
||||||
|
}
|
||||||
|
MatchStrategy::RequiredExtension(ext) => {
|
||||||
|
required_exts.add(i, ext, p.regex().to_owned());
|
||||||
|
}
|
||||||
|
MatchStrategy::Regex => {
|
||||||
|
debug!("glob converted to regex: {:?}", p);
|
||||||
|
regexes.add(i, p.regex().to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!("built glob set; {} literals, {} basenames, {} extensions, \
|
||||||
|
{} prefixes, {} suffixes, {} required extensions, {} regexes",
|
||||||
|
lits.0.len(), base_lits.0.len(), exts.0.len(),
|
||||||
|
prefixes.literals.len(), suffixes.literals.len(),
|
||||||
|
required_exts.0.len(), regexes.literals.len());
|
||||||
|
Ok(GlobSet {
|
||||||
|
strats: vec![
|
||||||
|
GlobSetMatchStrategy::Extension(exts),
|
||||||
|
GlobSetMatchStrategy::BasenameLiteral(base_lits),
|
||||||
|
GlobSetMatchStrategy::Literal(lits),
|
||||||
|
GlobSetMatchStrategy::Suffix(suffixes.suffix()),
|
||||||
|
GlobSetMatchStrategy::Prefix(prefixes.prefix()),
|
||||||
|
GlobSetMatchStrategy::RequiredExtension(
|
||||||
|
try!(required_exts.build())),
|
||||||
|
GlobSetMatchStrategy::Regex(try!(regexes.regex_set())),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GlobSetBuilder builds a group of patterns that can be used to
|
||||||
|
/// simultaneously match a file path.
|
||||||
|
pub struct GlobSetBuilder {
|
||||||
|
pats: Vec<Glob>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobSetBuilder {
|
||||||
|
/// Create a new GlobSetBuilder. A GlobSetBuilder can be used to add new
|
||||||
|
/// patterns. Once all patterns have been added, `build` should be called
|
||||||
|
/// to produce a `GlobSet`, which can then be used for matching.
|
||||||
|
pub fn new() -> GlobSetBuilder {
|
||||||
|
GlobSetBuilder { pats: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a new matcher from all of the glob patterns added so far.
|
||||||
|
///
|
||||||
|
/// Once a matcher is built, no new patterns can be added to it.
|
||||||
|
pub fn build(&self) -> Result<GlobSet, Error> {
|
||||||
|
GlobSet::new(&self.pats)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new pattern to this set.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn add(&mut self, pat: Glob) -> &mut GlobSetBuilder {
|
||||||
|
self.pats.push(pat);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A candidate path for matching.
|
||||||
|
///
|
||||||
|
/// All glob matching in this crate operates on `Candidate` values.
|
||||||
|
/// Constructing candidates has a very small cost associated with it, so
|
||||||
|
/// callers may find it beneficial to amortize that cost when matching a single
|
||||||
|
/// path against multiple globs or sets of globs.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Candidate<'a> {
|
||||||
|
path: Cow<'a, [u8]>,
|
||||||
|
basename: Cow<'a, [u8]>,
|
||||||
|
ext: &'a OsStr,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Candidate<'a> {
|
||||||
|
/// Create a new candidate for matching from the given path.
|
||||||
|
pub fn new<P: AsRef<Path> + ?Sized>(path: &'a P) -> Candidate<'a> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
let basename = file_name(path).unwrap_or(OsStr::new(""));
|
||||||
|
Candidate {
|
||||||
|
path: normalize_path(path_bytes(path)),
|
||||||
|
basename: os_str_bytes(basename),
|
||||||
|
ext: file_name_ext(basename).unwrap_or(OsStr::new("")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_prefix(&self, max: usize) -> &[u8] {
|
||||||
|
if self.path.len() <= max {
|
||||||
|
&*self.path
|
||||||
|
} else {
|
||||||
|
&self.path[..max]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path_suffix(&self, max: usize) -> &[u8] {
|
||||||
|
if self.path.len() <= max {
|
||||||
|
&*self.path
|
||||||
|
} else {
|
||||||
|
&self.path[self.path.len() - max..]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum GlobSetMatchStrategy {
|
||||||
|
Literal(LiteralStrategy),
|
||||||
|
BasenameLiteral(BasenameLiteralStrategy),
|
||||||
|
Extension(ExtensionStrategy),
|
||||||
|
Prefix(PrefixStrategy),
|
||||||
|
Suffix(SuffixStrategy),
|
||||||
|
RequiredExtension(RequiredExtensionStrategy),
|
||||||
|
Regex(RegexSetStrategy),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobSetMatchStrategy {
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
use self::GlobSetMatchStrategy::*;
|
||||||
|
match *self {
|
||||||
|
Literal(ref s) => s.is_match(candidate),
|
||||||
|
BasenameLiteral(ref s) => s.is_match(candidate),
|
||||||
|
Extension(ref s) => s.is_match(candidate),
|
||||||
|
Prefix(ref s) => s.is_match(candidate),
|
||||||
|
Suffix(ref s) => s.is_match(candidate),
|
||||||
|
RequiredExtension(ref s) => s.is_match(candidate),
|
||||||
|
Regex(ref s) => s.is_match(candidate),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
use self::GlobSetMatchStrategy::*;
|
||||||
|
match *self {
|
||||||
|
Literal(ref s) => s.matches_into(candidate, matches),
|
||||||
|
BasenameLiteral(ref s) => s.matches_into(candidate, matches),
|
||||||
|
Extension(ref s) => s.matches_into(candidate, matches),
|
||||||
|
Prefix(ref s) => s.matches_into(candidate, matches),
|
||||||
|
Suffix(ref s) => s.matches_into(candidate, matches),
|
||||||
|
RequiredExtension(ref s) => s.matches_into(candidate, matches),
|
||||||
|
Regex(ref s) => s.matches_into(candidate, matches),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct LiteralStrategy(BTreeMap<Vec<u8>, Vec<usize>>);
|
||||||
|
|
||||||
|
impl LiteralStrategy {
|
||||||
|
fn new() -> LiteralStrategy {
|
||||||
|
LiteralStrategy(BTreeMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, global_index: usize, lit: String) {
|
||||||
|
self.0.entry(lit.into_bytes()).or_insert(vec![]).push(global_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
self.0.contains_key(&*candidate.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
if let Some(hits) = self.0.get(&*candidate.path) {
|
||||||
|
matches.extend(hits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct BasenameLiteralStrategy(BTreeMap<Vec<u8>, Vec<usize>>);
|
||||||
|
|
||||||
|
impl BasenameLiteralStrategy {
|
||||||
|
fn new() -> BasenameLiteralStrategy {
|
||||||
|
BasenameLiteralStrategy(BTreeMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, global_index: usize, lit: String) {
|
||||||
|
self.0.entry(lit.into_bytes()).or_insert(vec![]).push(global_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
if candidate.basename.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.0.contains_key(&*candidate.basename)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
if candidate.basename.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(hits) = self.0.get(&*candidate.basename) {
|
||||||
|
matches.extend(hits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ExtensionStrategy(HashMap<OsString, Vec<usize>, Fnv>);
|
||||||
|
|
||||||
|
impl ExtensionStrategy {
|
||||||
|
fn new() -> ExtensionStrategy {
|
||||||
|
ExtensionStrategy(HashMap::with_hasher(Fnv::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, global_index: usize, ext: OsString) {
|
||||||
|
self.0.entry(ext).or_insert(vec![]).push(global_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
if candidate.ext.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.0.contains_key(candidate.ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
if candidate.ext.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(hits) = self.0.get(candidate.ext) {
|
||||||
|
matches.extend(hits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct PrefixStrategy {
|
||||||
|
matcher: FullAcAutomaton<Vec<u8>>,
|
||||||
|
map: Vec<usize>,
|
||||||
|
longest: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrefixStrategy {
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
let path = candidate.path_prefix(self.longest);
|
||||||
|
for m in self.matcher.find_overlapping(path) {
|
||||||
|
if m.start == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
let path = candidate.path_prefix(self.longest);
|
||||||
|
for m in self.matcher.find_overlapping(path) {
|
||||||
|
if m.start == 0 {
|
||||||
|
matches.push(self.map[m.pati]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct SuffixStrategy {
|
||||||
|
matcher: FullAcAutomaton<Vec<u8>>,
|
||||||
|
map: Vec<usize>,
|
||||||
|
longest: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuffixStrategy {
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
let path = candidate.path_suffix(self.longest);
|
||||||
|
for m in self.matcher.find_overlapping(path) {
|
||||||
|
if m.end == path.len() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
let path = candidate.path_suffix(self.longest);
|
||||||
|
for m in self.matcher.find_overlapping(path) {
|
||||||
|
if m.end == path.len() {
|
||||||
|
matches.push(self.map[m.pati]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RequiredExtensionStrategy(HashMap<OsString, Vec<(usize, Regex)>, Fnv>);
|
||||||
|
|
||||||
|
impl RequiredExtensionStrategy {
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
if candidate.ext.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match self.0.get(candidate.ext) {
|
||||||
|
None => false,
|
||||||
|
Some(regexes) => {
|
||||||
|
for &(_, ref re) in regexes {
|
||||||
|
if re.is_match(&*candidate.path) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(never)]
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
if candidate.ext.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(regexes) = self.0.get(candidate.ext) {
|
||||||
|
for &(global_index, ref re) in regexes {
|
||||||
|
if re.is_match(&*candidate.path) {
|
||||||
|
matches.push(global_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RegexSetStrategy {
|
||||||
|
matcher: RegexSet,
|
||||||
|
map: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RegexSetStrategy {
|
||||||
|
fn is_match(&self, candidate: &Candidate) -> bool {
|
||||||
|
self.matcher.is_match(&*candidate.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_into(&self, candidate: &Candidate, matches: &mut Vec<usize>) {
|
||||||
|
for i in self.matcher.matches(&*candidate.path) {
|
||||||
|
matches.push(self.map[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct MultiStrategyBuilder {
|
||||||
|
literals: Vec<String>,
|
||||||
|
map: Vec<usize>,
|
||||||
|
longest: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MultiStrategyBuilder {
|
||||||
|
fn new() -> MultiStrategyBuilder {
|
||||||
|
MultiStrategyBuilder {
|
||||||
|
literals: vec![],
|
||||||
|
map: vec![],
|
||||||
|
longest: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, global_index: usize, literal: String) {
|
||||||
|
if literal.len() > self.longest {
|
||||||
|
self.longest = literal.len();
|
||||||
|
}
|
||||||
|
self.map.push(global_index);
|
||||||
|
self.literals.push(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix(self) -> PrefixStrategy {
|
||||||
|
let it = self.literals.into_iter().map(|s| s.into_bytes());
|
||||||
|
PrefixStrategy {
|
||||||
|
matcher: AcAutomaton::new(it).into_full(),
|
||||||
|
map: self.map,
|
||||||
|
longest: self.longest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn suffix(self) -> SuffixStrategy {
|
||||||
|
let it = self.literals.into_iter().map(|s| s.into_bytes());
|
||||||
|
SuffixStrategy {
|
||||||
|
matcher: AcAutomaton::new(it).into_full(),
|
||||||
|
map: self.map,
|
||||||
|
longest: self.longest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn regex_set(self) -> Result<RegexSetStrategy, Error> {
|
||||||
|
Ok(RegexSetStrategy {
|
||||||
|
matcher: try!(new_regex_set(self.literals)),
|
||||||
|
map: self.map,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RequiredExtensionStrategyBuilder(
|
||||||
|
HashMap<OsString, Vec<(usize, String)>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl RequiredExtensionStrategyBuilder {
|
||||||
|
fn new() -> RequiredExtensionStrategyBuilder {
|
||||||
|
RequiredExtensionStrategyBuilder(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add(&mut self, global_index: usize, ext: OsString, regex: String) {
|
||||||
|
self.0.entry(ext).or_insert(vec![]).push((global_index, regex));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self) -> Result<RequiredExtensionStrategy, Error> {
|
||||||
|
let mut exts = HashMap::with_hasher(Fnv::default());
|
||||||
|
for (ext, regexes) in self.0.into_iter() {
|
||||||
|
exts.insert(ext.clone(), vec![]);
|
||||||
|
for (global_index, regex) in regexes {
|
||||||
|
let compiled = try!(new_regex(®ex));
|
||||||
|
exts.get_mut(&ext).unwrap().push((global_index, compiled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(RequiredExtensionStrategy(exts))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::GlobSetBuilder;
|
||||||
|
use glob::Glob;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_works() {
|
||||||
|
let mut builder = GlobSetBuilder::new();
|
||||||
|
builder.add(Glob::new("src/**/*.rs").unwrap());
|
||||||
|
builder.add(Glob::new("*.c").unwrap());
|
||||||
|
builder.add(Glob::new("src/lib.rs").unwrap());
|
||||||
|
let set = builder.build().unwrap();
|
||||||
|
|
||||||
|
assert!(set.is_match("foo.c"));
|
||||||
|
assert!(set.is_match("src/foo.c"));
|
||||||
|
assert!(!set.is_match("foo.rs"));
|
||||||
|
assert!(!set.is_match("tests/foo.rs"));
|
||||||
|
assert!(set.is_match("src/foo.rs"));
|
||||||
|
assert!(set.is_match("src/grep/src/main.rs"));
|
||||||
|
|
||||||
|
let matches = set.matches("src/lib.rs");
|
||||||
|
assert_eq!(2, matches.len());
|
||||||
|
assert_eq!(0, matches[0]);
|
||||||
|
assert_eq!(2, matches[1]);
|
||||||
|
}
|
||||||
|
}
|
180
globset/src/pathutil.rs
Normal file
180
globset/src/pathutil.rs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// The final component of the path, if it is a normal file.
|
||||||
|
///
|
||||||
|
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||||
|
/// file_name will return None.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||||
|
path: &'a P,
|
||||||
|
) -> Option<&'a OsStr> {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use memchr::memrchr;
|
||||||
|
|
||||||
|
let path = path.as_ref().as_os_str().as_bytes();
|
||||||
|
if path.is_empty() {
|
||||||
|
return None;
|
||||||
|
} else if path.len() == 1 && path[0] == b'.' {
|
||||||
|
return None;
|
||||||
|
} else if path.last() == Some(&b'.') {
|
||||||
|
return None;
|
||||||
|
} else if path.len() >= 2 && &path[path.len() - 2..] == &b".."[..] {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let last_slash = memrchr(b'/', path).map(|i| i + 1).unwrap_or(0);
|
||||||
|
Some(OsStr::from_bytes(&path[last_slash..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The final component of the path, if it is a normal file.
|
||||||
|
///
|
||||||
|
/// If the path terminates in ., .., or consists solely of a root of prefix,
|
||||||
|
/// file_name will return None.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn file_name<'a, P: AsRef<Path> + ?Sized>(
|
||||||
|
path: &'a P,
|
||||||
|
) -> Option<&'a OsStr> {
|
||||||
|
path.as_ref().file_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a file extension given a path's file name.
|
||||||
|
///
|
||||||
|
/// Note that this does NOT match the semantics of std::path::Path::extension.
|
||||||
|
/// Namely, the extension includes the `.` and matching is otherwise more
|
||||||
|
/// liberal. Specifically, the extenion is:
|
||||||
|
///
|
||||||
|
/// * None, if the file name given is empty;
|
||||||
|
/// * None, if there is no embedded `.`;
|
||||||
|
/// * Otherwise, the portion of the file name starting with the final `.`.
|
||||||
|
///
|
||||||
|
/// e.g., A file name of `.rs` has an extension `.rs`.
|
||||||
|
///
|
||||||
|
/// N.B. This is done to make certain glob match optimizations easier. Namely,
|
||||||
|
/// a pattern like `*.rs` is obviously trying to match files with a `rs`
|
||||||
|
/// extension, but it also matches files like `.rs`, which doesn't have an
|
||||||
|
/// extension according to std::path::Path::extension.
|
||||||
|
pub fn file_name_ext(name: &OsStr) -> Option<&OsStr> {
|
||||||
|
// Yes, these functions are awful, and yes, we are completely violating
|
||||||
|
// the abstraction barrier of std::ffi. The barrier we're violating is
|
||||||
|
// that an OsStr's encoding is *ASCII compatible*. While this is obviously
|
||||||
|
// true on Unix systems, it's also true on Windows because an OsStr uses
|
||||||
|
// WTF-8 internally: https://simonsapin.github.io/wtf-8/
|
||||||
|
//
|
||||||
|
// We should consider doing the same for the other path utility functions.
|
||||||
|
// Right now, we don't break any barriers, but Windows users are paying
|
||||||
|
// for it.
|
||||||
|
//
|
||||||
|
// Got any better ideas that don't cost anything? Hit me up. ---AG
|
||||||
|
unsafe fn os_str_as_u8_slice(s: &OsStr) -> &[u8] {
|
||||||
|
::std::mem::transmute(s)
|
||||||
|
}
|
||||||
|
unsafe fn u8_slice_as_os_str(s: &[u8]) -> &OsStr {
|
||||||
|
::std::mem::transmute(s)
|
||||||
|
}
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let name = unsafe { os_str_as_u8_slice(name) };
|
||||||
|
for (i, &b) in name.iter().enumerate().rev() {
|
||||||
|
if b == b'.' {
|
||||||
|
return Some(unsafe { u8_slice_as_os_str(&name[i..]) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return raw bytes of a path, transcoded to UTF-8 if necessary.
|
||||||
|
pub fn path_bytes(path: &Path) -> Cow<[u8]> {
|
||||||
|
os_str_bytes(path.as_os_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the raw bytes of the given OS string, transcoded to UTF-8 if
|
||||||
|
/// necessary.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn os_str_bytes(s: &OsStr) -> Cow<[u8]> {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
Cow::Borrowed(s.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the raw bytes of the given OS string, transcoded to UTF-8 if
|
||||||
|
/// necessary.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn os_str_bytes(s: &OsStr) -> Cow<[u8]> {
|
||||||
|
// TODO(burntsushi): On Windows, OS strings are WTF-8, which is a superset
|
||||||
|
// of UTF-8, so even if we could get at the raw bytes, they wouldn't
|
||||||
|
// be useful. We *must* convert to UTF-8 before doing path matching.
|
||||||
|
// Unfortunate, but necessary.
|
||||||
|
match s.to_string_lossy() {
|
||||||
|
Cow::Owned(s) => Cow::Owned(s.into_bytes()),
|
||||||
|
Cow::Borrowed(s) => Cow::Borrowed(s.as_bytes()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalizes a path to use `/` as a separator everywhere, even on platforms
|
||||||
|
/// that recognize other characters as separators.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn normalize_path(path: Cow<[u8]>) -> Cow<[u8]> {
|
||||||
|
// UNIX only uses /, so we're good.
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Normalizes a path to use `/` as a separator everywhere, even on platforms
|
||||||
|
/// that recognize other characters as separators.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn normalize_path(mut path: Cow<[u8]>) -> Cow<[u8]> {
|
||||||
|
use std::path::is_separator;
|
||||||
|
|
||||||
|
for i in 0..path.len() {
|
||||||
|
if path[i] == b'/' || !is_separator(path[i] as char) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
path.to_mut()[i] = b'/';
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
|
||||||
|
use super::{file_name_ext, normalize_path};
|
||||||
|
|
||||||
|
macro_rules! ext {
|
||||||
|
($name:ident, $file_name:expr, $ext:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let got = file_name_ext(OsStr::new($file_name));
|
||||||
|
assert_eq!($ext.map(OsStr::new), got);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ext!(ext1, "foo.rs", Some(".rs"));
|
||||||
|
ext!(ext2, ".rs", Some(".rs"));
|
||||||
|
ext!(ext3, "..rs", Some(".rs"));
|
||||||
|
ext!(ext4, "", None::<&str>);
|
||||||
|
ext!(ext5, "foo", None::<&str>);
|
||||||
|
|
||||||
|
macro_rules! normalize {
|
||||||
|
($name:ident, $path:expr, $expected:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let got = normalize_path(Cow::Owned($path.to_vec()));
|
||||||
|
assert_eq!($expected.to_vec(), got.into_owned());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize!(normal1, b"foo", b"foo");
|
||||||
|
normalize!(normal2, b"foo/bar", b"foo/bar");
|
||||||
|
#[cfg(unix)]
|
||||||
|
normalize!(normal3, b"foo\\bar", b"foo\\bar");
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
normalize!(normal3, b"foo\\bar", b"foo/bar");
|
||||||
|
#[cfg(unix)]
|
||||||
|
normalize!(normal4, b"foo\\bar/baz", b"foo\\bar/baz");
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
normalize!(normal4, b"foo\\bar/baz", b"foo/bar/baz");
|
||||||
|
}
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "grep"
|
name = "grep"
|
||||||
version = "0.1.2" #:version
|
version = "0.1.3" #:version
|
||||||
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
authors = ["Andrew Gallant <jamslam@gmail.com>"]
|
||||||
description = """
|
description = """
|
||||||
Fast line oriented regex searching as a library.
|
Fast line oriented regex searching as a library.
|
||||||
|
@@ -8,7 +8,6 @@ Note that this implementation is incredibly suspicious. We need something more
|
|||||||
principled.
|
principled.
|
||||||
*/
|
*/
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::iter;
|
|
||||||
|
|
||||||
use regex::bytes::Regex;
|
use regex::bytes::Regex;
|
||||||
use syntax::{
|
use syntax::{
|
||||||
@@ -181,8 +180,6 @@ fn repeat_range_literals<F: FnMut(&Expr, &mut Literals)>(
|
|||||||
lits: &mut Literals,
|
lits: &mut Literals,
|
||||||
mut f: F,
|
mut f: F,
|
||||||
) {
|
) {
|
||||||
use syntax::Expr::*;
|
|
||||||
|
|
||||||
if min == 0 {
|
if min == 0 {
|
||||||
// This is a bit conservative. If `max` is set, then we could
|
// This is a bit conservative. If `max` is set, then we could
|
||||||
// treat this as a finite set of alternations. For now, we
|
// treat this as a finite set of alternations. For now, we
|
||||||
@@ -190,8 +187,12 @@ fn repeat_range_literals<F: FnMut(&Expr, &mut Literals)>(
|
|||||||
lits.cut();
|
lits.cut();
|
||||||
} else {
|
} else {
|
||||||
let n = cmp::min(lits.limit_size(), min as usize);
|
let n = cmp::min(lits.limit_size(), min as usize);
|
||||||
let es = iter::repeat(e.clone()).take(n).collect();
|
// We only extract literals from a single repetition, even though
|
||||||
f(&Concat(es), lits);
|
// we could do more. e.g., `a{3}` will have `a` extracted instead of
|
||||||
|
// `aaa`. The reason is that inner literal extraction can't be unioned
|
||||||
|
// across repetitions. e.g., extracting `foofoofoo` from `(\w+foo){3}`
|
||||||
|
// is wrong.
|
||||||
|
f(e, lits);
|
||||||
if n < min as usize {
|
if n < min as usize {
|
||||||
lits.cut();
|
lits.cut();
|
||||||
}
|
}
|
||||||
|
@@ -52,6 +52,7 @@ pub struct GrepBuilder {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct Options {
|
struct Options {
|
||||||
case_insensitive: bool,
|
case_insensitive: bool,
|
||||||
|
case_smart: bool,
|
||||||
line_terminator: u8,
|
line_terminator: u8,
|
||||||
size_limit: usize,
|
size_limit: usize,
|
||||||
dfa_size_limit: usize,
|
dfa_size_limit: usize,
|
||||||
@@ -61,6 +62,7 @@ impl Default for Options {
|
|||||||
fn default() -> Options {
|
fn default() -> Options {
|
||||||
Options {
|
Options {
|
||||||
case_insensitive: false,
|
case_insensitive: false,
|
||||||
|
case_smart: false,
|
||||||
line_terminator: b'\n',
|
line_terminator: b'\n',
|
||||||
size_limit: 10 * (1 << 20),
|
size_limit: 10 * (1 << 20),
|
||||||
dfa_size_limit: 10 * (1 << 20),
|
dfa_size_limit: 10 * (1 << 20),
|
||||||
@@ -98,6 +100,18 @@ impl GrepBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether to enable smart case search or not (disabled by default).
|
||||||
|
///
|
||||||
|
/// Smart case uses case insensitive search if the regex is contains all
|
||||||
|
/// lowercase literal characters. Otherwise, a case sensitive search is
|
||||||
|
/// used instead.
|
||||||
|
///
|
||||||
|
/// Enabling the case_insensitive flag overrides this.
|
||||||
|
pub fn case_smart(mut self, yes: bool) -> GrepBuilder {
|
||||||
|
self.opts.case_smart = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the approximate size limit of the compiled regular expression.
|
/// Set the approximate size limit of the compiled regular expression.
|
||||||
///
|
///
|
||||||
/// This roughly corresponds to the number of bytes occupied by a
|
/// This roughly corresponds to the number of bytes occupied by a
|
||||||
@@ -148,8 +162,11 @@ impl GrepBuilder {
|
|||||||
/// Creates a new regex from the given expression with the current
|
/// Creates a new regex from the given expression with the current
|
||||||
/// configuration.
|
/// configuration.
|
||||||
fn regex(&self, expr: &Expr) -> Result<Regex> {
|
fn regex(&self, expr: &Expr) -> Result<Regex> {
|
||||||
|
let casei =
|
||||||
|
self.opts.case_insensitive
|
||||||
|
|| (self.opts.case_smart && !has_uppercase_literal(expr));
|
||||||
RegexBuilder::new(&expr.to_string())
|
RegexBuilder::new(&expr.to_string())
|
||||||
.case_insensitive(self.opts.case_insensitive)
|
.case_insensitive(casei)
|
||||||
.multi_line(true)
|
.multi_line(true)
|
||||||
.unicode(true)
|
.unicode(true)
|
||||||
.size_limit(self.opts.size_limit)
|
.size_limit(self.opts.size_limit)
|
||||||
@@ -167,8 +184,9 @@ impl GrepBuilder {
|
|||||||
.unicode(true)
|
.unicode(true)
|
||||||
.case_insensitive(self.opts.case_insensitive)
|
.case_insensitive(self.opts.case_insensitive)
|
||||||
.parse(&self.pattern));
|
.parse(&self.pattern));
|
||||||
|
let expr = try!(nonl::remove(expr, self.opts.line_terminator));
|
||||||
debug!("regex ast:\n{:#?}", expr);
|
debug!("regex ast:\n{:#?}", expr);
|
||||||
Ok(try!(nonl::remove(expr, self.opts.line_terminator)))
|
Ok(expr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,6 +292,23 @@ impl<'b, 's> Iterator for Iter<'b, 's> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_uppercase_literal(expr: &Expr) -> bool {
|
||||||
|
use syntax::Expr::*;
|
||||||
|
match *expr {
|
||||||
|
Literal { ref chars, casei } => {
|
||||||
|
casei || chars.iter().any(|c| c.is_uppercase())
|
||||||
|
}
|
||||||
|
LiteralBytes { ref bytes, casei } => {
|
||||||
|
casei || bytes.iter().any(|&b| b'A' <= b && b <= b'Z')
|
||||||
|
}
|
||||||
|
Group { ref e, .. } => has_uppercase_literal(e),
|
||||||
|
Repeat { ref e, .. } => has_uppercase_literal(e),
|
||||||
|
Concat(ref es) => es.iter().any(has_uppercase_literal),
|
||||||
|
Alternate(ref es) => es.iter().any(has_uppercase_literal),
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# Contributor: Andrew Gallant <jamslam@gmail.com>
|
# Contributor: Andrew Gallant <jamslam@gmail.com>
|
||||||
# Maintainer: Andrew Gallant
|
# Maintainer: Andrew Gallant
|
||||||
pkgname=ripgrep
|
pkgname=ripgrep
|
||||||
pkgver=0.1.15
|
pkgver=0.1.16
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="A search tool that combines the usability of The Silver Searcher with the raw speed of grep."
|
pkgdesc="A search tool that combines the usability of The Silver Searcher with the raw speed of grep."
|
||||||
arch=('i686' 'x86_64')
|
arch=('i686' 'x86_64')
|
||||||
@@ -9,7 +9,7 @@ url="https://github.com/BurntSushi/ripgrep"
|
|||||||
license=('UNLICENSE')
|
license=('UNLICENSE')
|
||||||
makedepends=('cargo')
|
makedepends=('cargo')
|
||||||
source=("https://github.com/BurntSushi/$pkgname/archive/$pkgver.tar.gz")
|
source=("https://github.com/BurntSushi/$pkgname/archive/$pkgver.tar.gz")
|
||||||
sha256sums=('ced856378c4ca625e4798ccae85418badd22e099fc324bcb162df51824808622')
|
sha256sums=('6f877018742c9a7557102ccebeedb40d7c779b470a5910a7bdab50ca2ce21532')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$pkgname-$pkgver"
|
cd "$pkgname-$pkgver"
|
||||||
|
14
pkg/brew/ripgrep-bin.rb
Normal file
14
pkg/brew/ripgrep-bin.rb
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
class RipgrepBin < Formula
|
||||||
|
version '0.2.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 "f8b208239b988708da2e58f848a75bf70ad144e201b3ed99cd323cc5a699625f"
|
||||||
|
|
||||||
|
conflicts_with "ripgrep"
|
||||||
|
|
||||||
|
def install
|
||||||
|
bin.install "rg"
|
||||||
|
man1.install "rg.1"
|
||||||
|
end
|
||||||
|
end
|
@@ -1,19 +0,0 @@
|
|||||||
require 'formula'
|
|
||||||
class Ripgrep < Formula
|
|
||||||
version '0.1.15'
|
|
||||||
desc "Search tool like grep and The Silver Searcher."
|
|
||||||
homepage "https://github.com/BurntSushi/ripgrep"
|
|
||||||
|
|
||||||
if Hardware::CPU.is_64_bit?
|
|
||||||
url "https://github.com/BurntSushi/ripgrep/releases/download/#{version}/ripgrep-#{version}-x86_64-apple-darwin.tar.gz"
|
|
||||||
sha256 "fc138cd57b533bd65739f3f695322e483fe648736358d853ddb9bcd26d84fdc5"
|
|
||||||
else
|
|
||||||
url "https://github.com/BurntSushi/ripgrep/releases/download/#{version}/ripgrep-#{version}-i686-apple-darwin.tar.gz"
|
|
||||||
sha256 "3ce1f12e49a463bc9dd4cfe2537aa9989a0dc81f7aa6f959ee0d0d82b5f768cb"
|
|
||||||
end
|
|
||||||
|
|
||||||
def install
|
|
||||||
bin.install "rg"
|
|
||||||
man1.install "rg.1"
|
|
||||||
end
|
|
||||||
end
|
|
221
src/args.rs
221
src/args.rs
@@ -39,10 +39,10 @@ Usage: rg [options] -e PATTERN ... [<path> ...]
|
|||||||
rg [options] <pattern> [<path> ...]
|
rg [options] <pattern> [<path> ...]
|
||||||
rg [options] --files [<path> ...]
|
rg [options] --files [<path> ...]
|
||||||
rg [options] --type-list
|
rg [options] --type-list
|
||||||
rg --help
|
rg [options] --help
|
||||||
rg --version
|
rg [options] --version
|
||||||
|
|
||||||
rg combines the usability of The Silver Searcher with the raw speed of grep.
|
rg recursively searches your current directory for a regex pattern.
|
||||||
|
|
||||||
Common options:
|
Common options:
|
||||||
-a, --text Search binary files as if they were text.
|
-a, --text Search binary files as if they were text.
|
||||||
@@ -62,13 +62,12 @@ Common options:
|
|||||||
Precede a glob with a '!' to exclude it.
|
Precede a glob with a '!' to exclude it.
|
||||||
-h, --help Show this usage message.
|
-h, --help Show this usage message.
|
||||||
-i, --ignore-case Case insensitive search.
|
-i, --ignore-case Case insensitive search.
|
||||||
|
Overridden by --case-sensitive.
|
||||||
-n, --line-number Show line numbers (1-based). This is enabled
|
-n, --line-number Show line numbers (1-based). This is enabled
|
||||||
by default at a tty.
|
by default at a tty.
|
||||||
-N, --no-line-number Suppress line numbers.
|
-N, --no-line-number Suppress line numbers.
|
||||||
-q, --quiet Do not print anything to stdout.
|
-q, --quiet Do not print anything to stdout. If a match is
|
||||||
-r, --replace ARG Replace every match with the string given.
|
found in a file, stop searching that file.
|
||||||
Capture group indices (e.g., $5) and names
|
|
||||||
(e.g., $foo) are supported.
|
|
||||||
-t, --type TYPE ... Only search files matching TYPE. Multiple type
|
-t, --type TYPE ... Only search files matching TYPE. Multiple type
|
||||||
flags may be provided. Use the --type-list flag
|
flags may be provided. Use the --type-list flag
|
||||||
to list all available types.
|
to list all available types.
|
||||||
@@ -110,10 +109,17 @@ Less common options:
|
|||||||
--files
|
--files
|
||||||
Print each file that would be searched (but don't search).
|
Print each file that would be searched (but don't search).
|
||||||
|
|
||||||
|
-l, --files-with-matches
|
||||||
|
Only show path of each file with matches.
|
||||||
|
|
||||||
-H, --with-filename
|
-H, --with-filename
|
||||||
Prefix each match with the file name that contains it. This is the
|
Prefix each match with the file name that contains it. This is the
|
||||||
default when more than one file is searched.
|
default when more than one file is searched.
|
||||||
|
|
||||||
|
--no-filename
|
||||||
|
Never show the filename for a match. This is the default when
|
||||||
|
one file is searched.
|
||||||
|
|
||||||
--heading
|
--heading
|
||||||
Show the file name above clusters of matches from each file.
|
Show the file name above clusters of matches from each file.
|
||||||
This is the default mode at a tty.
|
This is the default mode at a tty.
|
||||||
@@ -128,6 +134,10 @@ Less common options:
|
|||||||
-L, --follow
|
-L, --follow
|
||||||
Follow symlinks.
|
Follow symlinks.
|
||||||
|
|
||||||
|
--maxdepth NUM
|
||||||
|
Descend at most NUM directories below the command line arguments.
|
||||||
|
A value of zero only searches the starting-points themselves.
|
||||||
|
|
||||||
--mmap
|
--mmap
|
||||||
Search using memory maps when possible. This is enabled by default
|
Search using memory maps when possible. This is enabled by default
|
||||||
when ripgrep thinks it will be faster. (Note that mmap searching
|
when ripgrep thinks it will be faster. (Note that mmap searching
|
||||||
@@ -137,15 +147,40 @@ Less common options:
|
|||||||
Never use memory maps, even when they might be faster.
|
Never use memory maps, even when they might be faster.
|
||||||
|
|
||||||
--no-ignore
|
--no-ignore
|
||||||
Don't respect ignore files (.gitignore, .rgignore, etc.)
|
Don't respect ignore files (.gitignore, .ignore, etc.)
|
||||||
This implies --no-ignore-parent.
|
This implies --no-ignore-parent.
|
||||||
|
|
||||||
--no-ignore-parent
|
--no-ignore-parent
|
||||||
Don't respect ignore files in parent directories.
|
Don't respect ignore files in parent directories.
|
||||||
|
|
||||||
|
--no-ignore-vcs
|
||||||
|
Don't respect version control ignore files (e.g., .gitignore).
|
||||||
|
Note that .ignore files will continue to be respected.
|
||||||
|
|
||||||
|
--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.
|
||||||
|
|
||||||
-p, --pretty
|
-p, --pretty
|
||||||
Alias for --color=always --heading -n.
|
Alias for --color=always --heading -n.
|
||||||
|
|
||||||
|
-r, --replace ARG
|
||||||
|
Replace every match with the string given when printing search results.
|
||||||
|
Neither this flag nor any other flag will modify your files.
|
||||||
|
|
||||||
|
Capture group indices (e.g., $5) and names (e.g., $foo) are supported
|
||||||
|
in the replacement string.
|
||||||
|
|
||||||
|
-s, --case-sensitive
|
||||||
|
Search case sensitively. This overrides --ignore-case and --smart-case.
|
||||||
|
|
||||||
|
-S, --smart-case
|
||||||
|
Search case insensitively if the pattern is all lowercase.
|
||||||
|
Search case sensitively otherwise. This is overridden by
|
||||||
|
either --case-sensitive or --ignore-case.
|
||||||
|
|
||||||
-j, --threads ARG
|
-j, --threads ARG
|
||||||
The number of threads to use. Defaults to the number of logical CPUs
|
The number of threads to use. Defaults to the number of logical CPUs
|
||||||
(capped at 6). [default: 0]
|
(capped at 6). [default: 0]
|
||||||
@@ -163,11 +198,18 @@ File type management options:
|
|||||||
Show all supported file types and their associated globs.
|
Show all supported file types and their associated globs.
|
||||||
|
|
||||||
--type-add ARG ...
|
--type-add ARG ...
|
||||||
Add a new glob for a particular file type.
|
Add a new glob for a particular file type. Only one glob can be
|
||||||
Example: --type-add html:*.html,*.htm
|
added at a time. Multiple --type-add flags can be provided.
|
||||||
|
Unless --type-clear is used, globs are added to any existing globs
|
||||||
|
inside of ripgrep. Note that this must be passed to every invocation of
|
||||||
|
rg. Type settings are NOT persisted.
|
||||||
|
|
||||||
|
Example: `rg --type-add 'foo:*.foo' -tfoo PATTERN`
|
||||||
|
|
||||||
--type-clear TYPE ...
|
--type-clear TYPE ...
|
||||||
Clear the file type globs for TYPE.
|
Clear the file type globs previously defined for TYPE. This only clears
|
||||||
|
the default type definitions that are found inside of ripgrep. Note
|
||||||
|
that this must be passed to every invocation of rg.
|
||||||
";
|
";
|
||||||
|
|
||||||
/// RawArgs are the args as they are parsed from Docopt. They aren't used
|
/// RawArgs are the args as they are parsed from Docopt. They aren't used
|
||||||
@@ -178,11 +220,13 @@ pub struct RawArgs {
|
|||||||
arg_path: Vec<String>,
|
arg_path: Vec<String>,
|
||||||
flag_after_context: usize,
|
flag_after_context: usize,
|
||||||
flag_before_context: usize,
|
flag_before_context: usize,
|
||||||
|
flag_case_sensitive: bool,
|
||||||
flag_color: String,
|
flag_color: String,
|
||||||
flag_column: bool,
|
flag_column: bool,
|
||||||
flag_context: usize,
|
flag_context: usize,
|
||||||
flag_context_separator: String,
|
flag_context_separator: String,
|
||||||
flag_count: bool,
|
flag_count: bool,
|
||||||
|
flag_files_with_matches: bool,
|
||||||
flag_debug: bool,
|
flag_debug: bool,
|
||||||
flag_files: bool,
|
flag_files: bool,
|
||||||
flag_follow: bool,
|
flag_follow: bool,
|
||||||
@@ -193,16 +237,21 @@ pub struct RawArgs {
|
|||||||
flag_invert_match: bool,
|
flag_invert_match: bool,
|
||||||
flag_line_number: bool,
|
flag_line_number: bool,
|
||||||
flag_fixed_strings: bool,
|
flag_fixed_strings: bool,
|
||||||
|
flag_maxdepth: Option<usize>,
|
||||||
flag_mmap: bool,
|
flag_mmap: bool,
|
||||||
flag_no_heading: bool,
|
flag_no_heading: bool,
|
||||||
flag_no_ignore: bool,
|
flag_no_ignore: bool,
|
||||||
flag_no_ignore_parent: bool,
|
flag_no_ignore_parent: bool,
|
||||||
|
flag_no_ignore_vcs: bool,
|
||||||
flag_no_line_number: bool,
|
flag_no_line_number: bool,
|
||||||
flag_no_mmap: bool,
|
flag_no_mmap: bool,
|
||||||
|
flag_no_filename: bool,
|
||||||
|
flag_null: bool,
|
||||||
flag_pretty: bool,
|
flag_pretty: bool,
|
||||||
flag_quiet: bool,
|
flag_quiet: bool,
|
||||||
flag_regexp: Vec<String>,
|
flag_regexp: Vec<String>,
|
||||||
flag_replace: Option<String>,
|
flag_replace: Option<String>,
|
||||||
|
flag_smart_case: bool,
|
||||||
flag_text: bool,
|
flag_text: bool,
|
||||||
flag_threads: usize,
|
flag_threads: usize,
|
||||||
flag_type: Vec<String>,
|
flag_type: Vec<String>,
|
||||||
@@ -219,7 +268,6 @@ pub struct RawArgs {
|
|||||||
/// Args are transformed/normalized from RawArgs.
|
/// Args are transformed/normalized from RawArgs.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
pattern: String,
|
|
||||||
paths: Vec<PathBuf>,
|
paths: Vec<PathBuf>,
|
||||||
after_context: usize,
|
after_context: usize,
|
||||||
before_context: usize,
|
before_context: usize,
|
||||||
@@ -227,6 +275,7 @@ pub struct Args {
|
|||||||
column: bool,
|
column: bool,
|
||||||
context_separator: Vec<u8>,
|
context_separator: Vec<u8>,
|
||||||
count: bool,
|
count: bool,
|
||||||
|
files_with_matches: bool,
|
||||||
eol: u8,
|
eol: u8,
|
||||||
files: bool,
|
files: bool,
|
||||||
follow: bool,
|
follow: bool,
|
||||||
@@ -238,14 +287,16 @@ pub struct Args {
|
|||||||
invert_match: bool,
|
invert_match: bool,
|
||||||
line_number: bool,
|
line_number: bool,
|
||||||
line_per_match: bool,
|
line_per_match: bool,
|
||||||
|
maxdepth: Option<usize>,
|
||||||
mmap: bool,
|
mmap: bool,
|
||||||
no_ignore: bool,
|
no_ignore: bool,
|
||||||
no_ignore_parent: bool,
|
no_ignore_parent: bool,
|
||||||
|
no_ignore_vcs: bool,
|
||||||
|
null: bool,
|
||||||
quiet: bool,
|
quiet: bool,
|
||||||
replace: Option<Vec<u8>>,
|
replace: Option<Vec<u8>>,
|
||||||
text: bool,
|
text: bool,
|
||||||
threads: usize,
|
threads: usize,
|
||||||
type_defs: Vec<FileTypeDef>,
|
|
||||||
type_list: bool,
|
type_list: bool,
|
||||||
types: Types,
|
types: Types,
|
||||||
with_filename: bool,
|
with_filename: bool,
|
||||||
@@ -254,12 +305,12 @@ pub struct Args {
|
|||||||
impl RawArgs {
|
impl RawArgs {
|
||||||
/// Convert arguments parsed into a configuration used by ripgrep.
|
/// Convert arguments parsed into a configuration used by ripgrep.
|
||||||
fn to_args(&self) -> Result<Args> {
|
fn to_args(&self) -> Result<Args> {
|
||||||
let pattern = self.pattern();
|
|
||||||
let paths =
|
let paths =
|
||||||
if self.arg_path.is_empty() {
|
if self.arg_path.is_empty() {
|
||||||
if atty::on_stdin()
|
if atty::on_stdin()
|
||||||
|| self.flag_files
|
|| self.flag_files
|
||||||
|| self.flag_type_list {
|
|| self.flag_type_list
|
||||||
|
|| !atty::stdin_is_readable() {
|
||||||
vec![Path::new("./").to_path_buf()]
|
vec![Path::new("./").to_path_buf()]
|
||||||
} else {
|
} else {
|
||||||
vec![Path::new("-").to_path_buf()]
|
vec![Path::new("-").to_path_buf()]
|
||||||
@@ -283,6 +334,9 @@ impl RawArgs {
|
|||||||
} else if cfg!(windows) {
|
} else if cfg!(windows) {
|
||||||
// On Windows, memory maps appear faster than read calls. Neat.
|
// On Windows, memory maps appear faster than read calls. Neat.
|
||||||
true
|
true
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
// On Mac, memory maps appear to suck. Neat.
|
||||||
|
false
|
||||||
} else {
|
} else {
|
||||||
// If we're only searching a few paths and all of them are
|
// If we're only searching a few paths and all of them are
|
||||||
// files, then memory maps are probably faster.
|
// files, then memory maps are probably faster.
|
||||||
@@ -309,33 +363,26 @@ impl RawArgs {
|
|||||||
self.flag_threads
|
self.flag_threads
|
||||||
};
|
};
|
||||||
let color =
|
let color =
|
||||||
if self.flag_vimgrep {
|
if self.flag_color == "always" {
|
||||||
|
true
|
||||||
|
} else if self.flag_vimgrep {
|
||||||
false
|
false
|
||||||
} else if self.flag_color == "auto" {
|
} else if self.flag_color == "auto" {
|
||||||
atty::on_stdout() || self.flag_pretty
|
atty::on_stdout() || self.flag_pretty
|
||||||
} else {
|
} else {
|
||||||
self.flag_color == "always"
|
false
|
||||||
};
|
};
|
||||||
let eol = b'\n';
|
|
||||||
let mut with_filename = self.flag_with_filename;
|
let mut with_filename = self.flag_with_filename;
|
||||||
if !with_filename {
|
if !with_filename {
|
||||||
with_filename = paths.len() > 1 || paths[0].is_dir();
|
with_filename = paths.len() > 1 || paths[0].is_dir();
|
||||||
}
|
}
|
||||||
let mut btypes = TypesBuilder::new();
|
with_filename = with_filename && !self.flag_no_filename;
|
||||||
btypes.add_defaults();
|
|
||||||
try!(self.add_types(&mut btypes));
|
|
||||||
let types = try!(btypes.build());
|
|
||||||
let grep = try!(
|
|
||||||
GrepBuilder::new(&pattern)
|
|
||||||
.case_insensitive(self.flag_ignore_case)
|
|
||||||
.line_terminator(eol)
|
|
||||||
.build()
|
|
||||||
);
|
|
||||||
let no_ignore = self.flag_no_ignore || self.flag_unrestricted >= 1;
|
let no_ignore = self.flag_no_ignore || self.flag_unrestricted >= 1;
|
||||||
let hidden = self.flag_hidden || self.flag_unrestricted >= 2;
|
let hidden = self.flag_hidden || self.flag_unrestricted >= 2;
|
||||||
let text = self.flag_text || self.flag_unrestricted >= 3;
|
let text = self.flag_text || self.flag_unrestricted >= 3;
|
||||||
let mut args = Args {
|
let mut args = Args {
|
||||||
pattern: pattern,
|
|
||||||
paths: paths,
|
paths: paths,
|
||||||
after_context: after_context,
|
after_context: after_context,
|
||||||
before_context: before_context,
|
before_context: before_context,
|
||||||
@@ -343,29 +390,34 @@ impl RawArgs {
|
|||||||
column: self.flag_column,
|
column: self.flag_column,
|
||||||
context_separator: unescape(&self.flag_context_separator),
|
context_separator: unescape(&self.flag_context_separator),
|
||||||
count: self.flag_count,
|
count: self.flag_count,
|
||||||
eol: eol,
|
files_with_matches: self.flag_files_with_matches,
|
||||||
|
eol: self.eol(),
|
||||||
files: self.flag_files,
|
files: self.flag_files,
|
||||||
follow: self.flag_follow,
|
follow: self.flag_follow,
|
||||||
glob_overrides: glob_overrides,
|
glob_overrides: glob_overrides,
|
||||||
grep: grep,
|
grep: try!(self.grep()),
|
||||||
heading: !self.flag_no_heading && self.flag_heading,
|
heading: !self.flag_no_heading && self.flag_heading,
|
||||||
hidden: hidden,
|
hidden: hidden,
|
||||||
ignore_case: self.flag_ignore_case,
|
ignore_case: self.flag_ignore_case,
|
||||||
invert_match: self.flag_invert_match,
|
invert_match: self.flag_invert_match,
|
||||||
line_number: !self.flag_no_line_number && self.flag_line_number,
|
line_number: !self.flag_no_line_number && self.flag_line_number,
|
||||||
line_per_match: self.flag_vimgrep,
|
line_per_match: self.flag_vimgrep,
|
||||||
|
maxdepth: self.flag_maxdepth,
|
||||||
mmap: mmap,
|
mmap: mmap,
|
||||||
no_ignore: no_ignore,
|
no_ignore: no_ignore,
|
||||||
no_ignore_parent:
|
no_ignore_parent:
|
||||||
// --no-ignore implies --no-ignore-parent
|
// --no-ignore implies --no-ignore-parent
|
||||||
self.flag_no_ignore_parent || no_ignore,
|
self.flag_no_ignore_parent || no_ignore,
|
||||||
|
no_ignore_vcs:
|
||||||
|
// --no-ignore implies --no-ignore-vcs
|
||||||
|
self.flag_no_ignore_vcs || no_ignore,
|
||||||
|
null: self.flag_null,
|
||||||
quiet: self.flag_quiet,
|
quiet: self.flag_quiet,
|
||||||
replace: self.flag_replace.clone().map(|s| s.into_bytes()),
|
replace: self.flag_replace.clone().map(|s| s.into_bytes()),
|
||||||
text: text,
|
text: text,
|
||||||
threads: threads,
|
threads: threads,
|
||||||
type_defs: btypes.definitions(),
|
|
||||||
type_list: self.flag_type_list,
|
type_list: self.flag_type_list,
|
||||||
types: types,
|
types: try!(self.types()),
|
||||||
with_filename: with_filename,
|
with_filename: with_filename,
|
||||||
};
|
};
|
||||||
// If stdout is a tty, then apply some special default options.
|
// If stdout is a tty, then apply some special default options.
|
||||||
@@ -384,20 +436,22 @@ impl RawArgs {
|
|||||||
Ok(args)
|
Ok(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_types(&self, types: &mut TypesBuilder) -> Result<()> {
|
fn types(&self) -> Result<Types> {
|
||||||
|
let mut btypes = TypesBuilder::new();
|
||||||
|
btypes.add_defaults();
|
||||||
for ty in &self.flag_type_clear {
|
for ty in &self.flag_type_clear {
|
||||||
types.clear(ty);
|
btypes.clear(ty);
|
||||||
}
|
}
|
||||||
for def in &self.flag_type_add {
|
for def in &self.flag_type_add {
|
||||||
try!(types.add_def(def));
|
try!(btypes.add_def(def));
|
||||||
}
|
}
|
||||||
for ty in &self.flag_type {
|
for ty in &self.flag_type {
|
||||||
types.select(ty);
|
btypes.select(ty);
|
||||||
}
|
}
|
||||||
for ty in &self.flag_type_not {
|
for ty in &self.flag_type_not {
|
||||||
types.negate(ty);
|
btypes.negate(ty);
|
||||||
}
|
}
|
||||||
Ok(())
|
btypes.build().map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pattern(&self) -> String {
|
fn pattern(&self) -> String {
|
||||||
@@ -427,6 +481,27 @@ impl RawArgs {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eol(&self) -> u8 {
|
||||||
|
// We might want to make this configurable.
|
||||||
|
b'\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grep(&self) -> Result<Grep> {
|
||||||
|
let smart =
|
||||||
|
self.flag_smart_case
|
||||||
|
&& !self.flag_ignore_case
|
||||||
|
&& !self.flag_case_sensitive;
|
||||||
|
let casei =
|
||||||
|
self.flag_ignore_case
|
||||||
|
&& !self.flag_case_sensitive;
|
||||||
|
GrepBuilder::new(&self.pattern())
|
||||||
|
.case_smart(smart)
|
||||||
|
.case_insensitive(casei)
|
||||||
|
.line_terminator(self.eol())
|
||||||
|
.build()
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Args {
|
impl Args {
|
||||||
@@ -451,7 +526,7 @@ impl Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let raw: RawArgs =
|
let mut raw: RawArgs =
|
||||||
Docopt::new(USAGE)
|
Docopt::new(USAGE)
|
||||||
.and_then(|d| d.argv(argv).version(Some(version())).decode())
|
.and_then(|d| d.argv(argv).version(Some(version())).decode())
|
||||||
.unwrap_or_else(|e| e.exit());
|
.unwrap_or_else(|e| e.exit());
|
||||||
@@ -466,6 +541,13 @@ impl Args {
|
|||||||
errored!("failed to initialize logger: {}", err);
|
errored!("failed to initialize logger: {}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *sigh*... If --files is given, then the first path ends up in
|
||||||
|
// pattern.
|
||||||
|
if raw.flag_files {
|
||||||
|
if !raw.arg_pattern.is_empty() {
|
||||||
|
raw.arg_path.insert(0, raw.arg_pattern.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
raw.to_args().map_err(From::from)
|
raw.to_args().map_err(From::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,6 +578,11 @@ impl Args {
|
|||||||
self.mmap
|
self.mmap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether ripgrep should be quiet or not.
|
||||||
|
pub fn quiet(&self) -> bool {
|
||||||
|
self.quiet
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new printer of individual search results that writes to the
|
/// Create a new printer of individual search results that writes to the
|
||||||
/// writer given.
|
/// writer given.
|
||||||
pub fn printer<W: Terminal + Send>(&self, wtr: W) -> Printer<W> {
|
pub fn printer<W: Terminal + Send>(&self, wtr: W) -> Printer<W> {
|
||||||
@@ -505,7 +592,7 @@ impl Args {
|
|||||||
.eol(self.eol)
|
.eol(self.eol)
|
||||||
.heading(self.heading)
|
.heading(self.heading)
|
||||||
.line_per_match(self.line_per_match)
|
.line_per_match(self.line_per_match)
|
||||||
.quiet(self.quiet)
|
.null(self.null)
|
||||||
.with_filename(self.with_filename);
|
.with_filename(self.with_filename);
|
||||||
if let Some(ref rep) = self.replace {
|
if let Some(ref rep) = self.replace {
|
||||||
p = p.replace(rep.clone());
|
p = p.replace(rep.clone());
|
||||||
@@ -517,14 +604,23 @@ impl Args {
|
|||||||
/// to the writer given.
|
/// to the writer given.
|
||||||
pub fn out(&self) -> Out {
|
pub fn out(&self) -> Out {
|
||||||
let mut out = Out::new(self.color);
|
let mut out = Out::new(self.color);
|
||||||
if self.heading && !self.count {
|
if let Some(filesep) = self.file_separator() {
|
||||||
out = out.file_separator(b"".to_vec());
|
out = out.file_separator(filesep);
|
||||||
} else if self.before_context > 0 || self.after_context > 0 {
|
|
||||||
out = out.file_separator(self.context_separator.clone());
|
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieve the configured file separator.
|
||||||
|
pub fn file_separator(&self) -> Option<Vec<u8>> {
|
||||||
|
if self.heading && !self.count && !self.files_with_matches {
|
||||||
|
Some(b"".to_vec())
|
||||||
|
} else if self.before_context > 0 || self.after_context > 0 {
|
||||||
|
Some(self.context_separator.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new buffer for use with searching.
|
/// Create a new buffer for use with searching.
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub fn outbuf(&self) -> ColoredTerminal<term::TerminfoTerminal<Vec<u8>>> {
|
pub fn outbuf(&self) -> ColoredTerminal<term::TerminfoTerminal<Vec<u8>>> {
|
||||||
@@ -571,9 +667,11 @@ impl Args {
|
|||||||
.after_context(self.after_context)
|
.after_context(self.after_context)
|
||||||
.before_context(self.before_context)
|
.before_context(self.before_context)
|
||||||
.count(self.count)
|
.count(self.count)
|
||||||
|
.files_with_matches(self.files_with_matches)
|
||||||
.eol(self.eol)
|
.eol(self.eol)
|
||||||
.line_number(self.line_number)
|
.line_number(self.line_number)
|
||||||
.invert_match(self.invert_match)
|
.invert_match(self.invert_match)
|
||||||
|
.quiet(self.quiet)
|
||||||
.text(self.text)
|
.text(self.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,9 +687,11 @@ impl Args {
|
|||||||
) -> BufferSearcher<'a, W> {
|
) -> BufferSearcher<'a, W> {
|
||||||
BufferSearcher::new(printer, grep, path, buf)
|
BufferSearcher::new(printer, grep, path, buf)
|
||||||
.count(self.count)
|
.count(self.count)
|
||||||
|
.files_with_matches(self.files_with_matches)
|
||||||
.eol(self.eol)
|
.eol(self.eol)
|
||||||
.line_number(self.line_number)
|
.line_number(self.line_number)
|
||||||
.invert_match(self.invert_match)
|
.invert_match(self.invert_match)
|
||||||
|
.quiet(self.quiet)
|
||||||
.text(self.text)
|
.text(self.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,7 +702,7 @@ impl Args {
|
|||||||
|
|
||||||
/// Returns a list of type definitions currently loaded.
|
/// Returns a list of type definitions currently loaded.
|
||||||
pub fn type_defs(&self) -> &[FileTypeDef] {
|
pub fn type_defs(&self) -> &[FileTypeDef] {
|
||||||
&self.type_defs
|
self.types.definitions()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if ripgrep should print the type definitions currently
|
/// Returns true if ripgrep should print the type definitions currently
|
||||||
@@ -613,16 +713,27 @@ impl Args {
|
|||||||
|
|
||||||
/// Create a new recursive directory iterator at the path given.
|
/// Create a new recursive directory iterator at the path given.
|
||||||
pub fn walker(&self, path: &Path) -> Result<walk::Iter> {
|
pub fn walker(&self, path: &Path) -> Result<walk::Iter> {
|
||||||
let wd = WalkDir::new(path).follow_links(self.follow);
|
// Always follow symlinks for explicitly specified files.
|
||||||
let mut ig = Ignore::new();
|
let mut wd = WalkDir::new(path).follow_links(
|
||||||
ig.ignore_hidden(!self.hidden);
|
self.follow || path.is_file());
|
||||||
ig.no_ignore(self.no_ignore);
|
if let Some(maxdepth) = self.maxdepth {
|
||||||
ig.add_types(self.types.clone());
|
wd = wd.max_depth(maxdepth);
|
||||||
if !self.no_ignore_parent {
|
|
||||||
try!(ig.push_parents(path));
|
|
||||||
}
|
}
|
||||||
if let Some(ref overrides) = self.glob_overrides {
|
let mut ig = Ignore::new();
|
||||||
ig.add_override(overrides.clone());
|
// Only register ignore rules if this is a directory. If it's a file,
|
||||||
|
// then it was explicitly given by the end user, so we always search
|
||||||
|
// it.
|
||||||
|
if path.is_dir() {
|
||||||
|
ig.ignore_hidden(!self.hidden);
|
||||||
|
ig.no_ignore(self.no_ignore);
|
||||||
|
ig.no_ignore_vcs(self.no_ignore_vcs);
|
||||||
|
ig.add_types(self.types.clone());
|
||||||
|
if !self.no_ignore_parent {
|
||||||
|
try!(ig.push_parents(path));
|
||||||
|
}
|
||||||
|
if let Some(ref overrides) = self.glob_overrides {
|
||||||
|
ig.add_override(overrides.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(walk::Iter::new(ig, wd))
|
Ok(walk::Iter::new(ig, wd))
|
||||||
}
|
}
|
||||||
|
44
src/atty.rs
44
src/atty.rs
@@ -4,30 +4,58 @@ from (or to) a terminal. Windows and Unix do this differently, so implement
|
|||||||
both here.
|
both here.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn stdin_is_readable() -> bool {
|
||||||
|
use std::fs::File;
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
use std::os::unix::io::{FromRawFd, IntoRawFd};
|
||||||
|
use libc;
|
||||||
|
|
||||||
|
let file = unsafe { File::from_raw_fd(libc::STDIN_FILENO) };
|
||||||
|
let md = file.metadata();
|
||||||
|
let _ = file.into_raw_fd();
|
||||||
|
let ft = match md {
|
||||||
|
Err(_) => return false,
|
||||||
|
Ok(md) => md.file_type(),
|
||||||
|
};
|
||||||
|
ft.is_file() || ft.is_fifo()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn stdin_is_readable() -> bool {
|
||||||
|
// ???
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there is a tty on stdin.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn on_stdin() -> bool {
|
pub fn on_stdin() -> bool {
|
||||||
use libc;
|
use libc;
|
||||||
0 < unsafe { libc::isatty(libc::STDIN_FILENO) }
|
0 < unsafe { libc::isatty(libc::STDIN_FILENO) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if there is a tty on stdout.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub fn on_stdout() -> bool {
|
pub fn on_stdout() -> bool {
|
||||||
use libc;
|
use libc;
|
||||||
0 < unsafe { libc::isatty(libc::STDOUT_FILENO) }
|
0 < unsafe { libc::isatty(libc::STDOUT_FILENO) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if there is a tty on stdin.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn on_stdin() -> bool {
|
pub fn on_stdin() -> bool {
|
||||||
use kernel32;
|
// BUG: https://github.com/BurntSushi/ripgrep/issues/19
|
||||||
use winapi;
|
// It's not clear to me how to determine whether there is a tty on stdin.
|
||||||
|
// Checking GetConsoleMode(GetStdHandle(stdin)) != 0 appears to report
|
||||||
unsafe {
|
// that stdin is a pipe, even if it's not in a cygwin terminal, for
|
||||||
let fd = winapi::winbase::STD_INPUT_HANDLE;
|
// example.
|
||||||
let mut out = 0;
|
//
|
||||||
kernel32::GetConsoleMode(kernel32::GetStdHandle(fd), &mut out) != 0
|
// To fix this, we just assume there is always a tty on stdin. If Windows
|
||||||
}
|
// users need to search stdin, they'll have to pass -. Ug.
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if there is a tty on stdout.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn on_stdout() -> bool {
|
pub fn on_stdout() -> bool {
|
||||||
use kernel32;
|
use kernel32;
|
||||||
|
128
src/gitignore.rs
128
src/gitignore.rs
@@ -9,7 +9,7 @@ The motivation for this submodule is performance and portability:
|
|||||||
2. We could shell out to a `git` sub-command like ls-files or status, but it
|
2. We could shell out to a `git` sub-command like ls-files or status, but it
|
||||||
seems better to not rely on the existence of external programs for a search
|
seems better to not rely on the existence of external programs for a search
|
||||||
tool. Besides, we need to implement this logic anyway to support things like
|
tool. Besides, we need to implement this logic anyway to support things like
|
||||||
an .rgignore file.
|
an .ignore file.
|
||||||
|
|
||||||
The key implementation detail here is that a single gitignore file is compiled
|
The key implementation detail here is that a single gitignore file is compiled
|
||||||
into a single RegexSet, which can be used to report which globs match a
|
into a single RegexSet, which can be used to report which globs match a
|
||||||
@@ -28,15 +28,15 @@ use std::fs::File;
|
|||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use globset::{self, Candidate, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||||
use regex;
|
use regex;
|
||||||
|
|
||||||
use glob;
|
use pathutil::{is_file_name, strip_prefix};
|
||||||
use pathutil::strip_prefix;
|
|
||||||
|
|
||||||
/// Represents an error that can occur when parsing a gitignore file.
|
/// Represents an error that can occur when parsing a gitignore file.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Glob(glob::Error),
|
Glob(globset::Error),
|
||||||
Regex(regex::Error),
|
Regex(regex::Error),
|
||||||
Io(io::Error),
|
Io(io::Error),
|
||||||
}
|
}
|
||||||
@@ -61,8 +61,8 @@ impl fmt::Display for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<glob::Error> for Error {
|
impl From<globset::Error> for Error {
|
||||||
fn from(err: glob::Error) -> Error {
|
fn from(err: globset::Error) -> Error {
|
||||||
Error::Glob(err)
|
Error::Glob(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ impl From<io::Error> for Error {
|
|||||||
/// Gitignore is a matcher for the glob patterns in a single gitignore file.
|
/// Gitignore is a matcher for the glob patterns in a single gitignore file.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Gitignore {
|
pub struct Gitignore {
|
||||||
set: glob::Set,
|
set: GlobSet,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
patterns: Vec<Pattern>,
|
patterns: Vec<Pattern>,
|
||||||
num_ignores: u64,
|
num_ignores: u64,
|
||||||
@@ -115,7 +115,17 @@ impl Gitignore {
|
|||||||
if let Some(p) = strip_prefix("./", path) {
|
if let Some(p) = strip_prefix("./", path) {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
if let Some(p) = strip_prefix(&self.root, path) {
|
// Strip any common prefix between the candidate path and the root
|
||||||
|
// of the gitignore, to make sure we get relative matching right.
|
||||||
|
// BUT, a file name might not have any directory components to it,
|
||||||
|
// in which case, we don't want to accidentally strip any part of the
|
||||||
|
// file name.
|
||||||
|
if !is_file_name(path) {
|
||||||
|
if let Some(p) = strip_prefix(&self.root, path) {
|
||||||
|
path = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(p) = strip_prefix("/", path) {
|
||||||
path = p;
|
path = p;
|
||||||
}
|
}
|
||||||
self.matched_stripped(path, is_dir)
|
self.matched_stripped(path, is_dir)
|
||||||
@@ -130,7 +140,8 @@ impl Gitignore {
|
|||||||
};
|
};
|
||||||
MATCHES.with(|matches| {
|
MATCHES.with(|matches| {
|
||||||
let mut matches = matches.borrow_mut();
|
let mut matches = matches.borrow_mut();
|
||||||
self.set.matches_into(path, &mut *matches);
|
let candidate = Candidate::new(path);
|
||||||
|
self.set.matches_candidate_into(&candidate, &mut *matches);
|
||||||
for &i in matches.iter().rev() {
|
for &i in matches.iter().rev() {
|
||||||
let pat = &self.patterns[i];
|
let pat = &self.patterns[i];
|
||||||
if !pat.only_dir || is_dir {
|
if !pat.only_dir || is_dir {
|
||||||
@@ -197,7 +208,7 @@ impl<'a> Match<'a> {
|
|||||||
/// GitignoreBuilder constructs a matcher for a single set of globs from a
|
/// GitignoreBuilder constructs a matcher for a single set of globs from a
|
||||||
/// .gitignore file.
|
/// .gitignore file.
|
||||||
pub struct GitignoreBuilder {
|
pub struct GitignoreBuilder {
|
||||||
builder: glob::SetBuilder,
|
builder: GlobSetBuilder,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
patterns: Vec<Pattern>,
|
patterns: Vec<Pattern>,
|
||||||
}
|
}
|
||||||
@@ -225,9 +236,10 @@ impl GitignoreBuilder {
|
|||||||
/// The path given should be the path at which the globs for this gitignore
|
/// The path given should be the path at which the globs for this gitignore
|
||||||
/// file should be matched.
|
/// file should be matched.
|
||||||
pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
|
pub fn new<P: AsRef<Path>>(root: P) -> GitignoreBuilder {
|
||||||
|
let root = strip_prefix("./", root.as_ref()).unwrap_or(root.as_ref());
|
||||||
GitignoreBuilder {
|
GitignoreBuilder {
|
||||||
builder: glob::SetBuilder::new(),
|
builder: GlobSetBuilder::new(),
|
||||||
root: root.as_ref().to_path_buf(),
|
root: root.to_path_buf(),
|
||||||
patterns: vec![],
|
patterns: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -250,8 +262,19 @@ impl GitignoreBuilder {
|
|||||||
/// Add each pattern line from the file path given.
|
/// Add each pattern line from the file path given.
|
||||||
pub fn add_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
|
pub fn add_path<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
|
||||||
let rdr = io::BufReader::new(try!(File::open(&path)));
|
let rdr = io::BufReader::new(try!(File::open(&path)));
|
||||||
for line in rdr.lines() {
|
debug!("gitignore: {}", path.as_ref().display());
|
||||||
try!(self.add(&path, &try!(line)));
|
for (i, line) in rdr.lines().enumerate() {
|
||||||
|
let line = match line {
|
||||||
|
Ok(line) => line,
|
||||||
|
Err(err) => {
|
||||||
|
debug!("error reading line {} in {}: {}",
|
||||||
|
i, path.as_ref().display(), err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Err(err) = self.add(&path, &line) {
|
||||||
|
debug!("error adding gitignore pattern: '{}': {}", line, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -272,6 +295,12 @@ impl GitignoreBuilder {
|
|||||||
from: P,
|
from: P,
|
||||||
mut line: &str,
|
mut line: &str,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
if line.starts_with("#") {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !line.ends_with("\\ ") {
|
||||||
|
line = line.trim_right();
|
||||||
|
}
|
||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -282,34 +311,24 @@ impl GitignoreBuilder {
|
|||||||
whitelist: false,
|
whitelist: false,
|
||||||
only_dir: false,
|
only_dir: false,
|
||||||
};
|
};
|
||||||
let mut opts = glob::MatchOptions::default();
|
let mut literal_separator = false;
|
||||||
let has_slash = line.chars().any(|c| c == '/');
|
let has_slash = line.chars().any(|c| c == '/');
|
||||||
// If the line starts with an escaped '!', then remove the escape.
|
let is_absolute = line.chars().nth(0).unwrap() == '/';
|
||||||
// Otherwise, if it starts with an unescaped '!', then this is a
|
if line.starts_with("\\!") || line.starts_with("\\#") {
|
||||||
// whitelist pattern.
|
line = &line[1..];
|
||||||
match line.chars().nth(0) {
|
} else {
|
||||||
Some('#') => return Ok(()),
|
if line.starts_with("!") {
|
||||||
Some('\\') => {
|
|
||||||
match line.chars().nth(1) {
|
|
||||||
Some('!') | Some('#') => {
|
|
||||||
line = &line[1..];
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some('!') => {
|
|
||||||
pat.whitelist = true;
|
pat.whitelist = true;
|
||||||
line = &line[1..];
|
line = &line[1..];
|
||||||
}
|
}
|
||||||
Some('/') => {
|
if line.starts_with("/") {
|
||||||
// `man gitignore` says that if a glob starts with a slash,
|
// `man gitignore` says that if a glob starts with a slash,
|
||||||
// then the glob can only match the beginning of a path
|
// then the glob can only match the beginning of a path
|
||||||
// (relative to the location of gitignore). We achieve this by
|
// (relative to the location of gitignore). We achieve this by
|
||||||
// simply banning wildcards from matching /.
|
// simply banning wildcards from matching /.
|
||||||
opts.require_literal_separator = true;
|
literal_separator = true;
|
||||||
line = &line[1..];
|
line = &line[1..];
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
// If it ends with a slash, then this should only match directories,
|
// If it ends with a slash, then this should only match directories,
|
||||||
// but the slash should otherwise not be used while globbing.
|
// but the slash should otherwise not be used while globbing.
|
||||||
@@ -320,16 +339,31 @@ impl GitignoreBuilder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If there is a literal slash, then we note that so that globbing
|
// If there is a literal slash, then we note that so that globbing
|
||||||
// doesn't let wildcards match slashes. Otherwise, we need to let
|
// doesn't let wildcards match slashes.
|
||||||
// the pattern match anywhere, so we add a `**/` prefix to achieve
|
|
||||||
// that behavior.
|
|
||||||
pat.pat = line.to_string();
|
pat.pat = line.to_string();
|
||||||
if has_slash {
|
if has_slash {
|
||||||
opts.require_literal_separator = true;
|
literal_separator = true;
|
||||||
} else {
|
|
||||||
pat.pat = format!("**/{}", pat.pat);
|
|
||||||
}
|
}
|
||||||
try!(self.builder.add_with(&pat.pat, &opts));
|
// If there was a leading slash, then this is a pattern that must
|
||||||
|
// match the entire path name. Otherwise, we should let it match
|
||||||
|
// anywhere, so use a **/ prefix.
|
||||||
|
if !is_absolute {
|
||||||
|
// ... but only if we don't already have a **/ prefix.
|
||||||
|
if !pat.pat.starts_with("**/") {
|
||||||
|
pat.pat = format!("**/{}", pat.pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the pattern ends with `/**`, then we should only match everything
|
||||||
|
// inside a directory, but not the directory itself. Standard globs
|
||||||
|
// will match the directory. So we add `/*` to force the issue.
|
||||||
|
if pat.pat.ends_with("/**") {
|
||||||
|
pat.pat = format!("{}/*", pat.pat);
|
||||||
|
}
|
||||||
|
let parsed = try!(
|
||||||
|
GlobBuilder::new(&pat.pat)
|
||||||
|
.literal_separator(literal_separator)
|
||||||
|
.build());
|
||||||
|
self.builder.add(parsed);
|
||||||
self.patterns.push(pat);
|
self.patterns.push(pat);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -393,10 +427,14 @@ mod tests {
|
|||||||
ignored!(ig24, ROOT, "target", "grep/target");
|
ignored!(ig24, ROOT, "target", "grep/target");
|
||||||
ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
|
ignored!(ig25, ROOT, "Cargo.lock", "./tabwriter-bin/Cargo.lock");
|
||||||
ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
|
ignored!(ig26, ROOT, "/foo/bar/baz", "./foo/bar/baz");
|
||||||
|
ignored!(ig27, ROOT, "foo/", "xyz/foo", true);
|
||||||
|
ignored!(ig28, ROOT, "src/*.rs", "src/grep/src/main.rs");
|
||||||
|
ignored!(ig29, "./src", "/llvm/", "./src/llvm", true);
|
||||||
|
ignored!(ig30, ROOT, "node_modules/ ", "node_modules", true);
|
||||||
|
|
||||||
not_ignored!(ignot1, ROOT, "amonths", "months");
|
not_ignored!(ignot1, ROOT, "amonths", "months");
|
||||||
not_ignored!(ignot2, ROOT, "monthsa", "months");
|
not_ignored!(ignot2, ROOT, "monthsa", "months");
|
||||||
not_ignored!(ignot3, ROOT, "src/*.rs", "src/grep/src/main.rs");
|
not_ignored!(ignot3, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||||
not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
|
not_ignored!(ignot4, ROOT, "/*.c", "mozilla-sha1/sha1.c");
|
||||||
not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
not_ignored!(ignot5, ROOT, "/src/*.rs", "src/grep/src/main.rs");
|
||||||
not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
|
not_ignored!(ignot6, ROOT, "*.rs\n!src/main.rs", "src/main.rs");
|
||||||
@@ -406,4 +444,14 @@ mod tests {
|
|||||||
not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
|
not_ignored!(ignot10, ROOT, "**/foo/bar", "foo/src/bar");
|
||||||
not_ignored!(ignot11, ROOT, "#foo", "#foo");
|
not_ignored!(ignot11, ROOT, "#foo", "#foo");
|
||||||
not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
|
not_ignored!(ignot12, ROOT, "\n\n\n", "foo");
|
||||||
|
not_ignored!(ignot13, ROOT, "foo/**", "foo", true);
|
||||||
|
not_ignored!(
|
||||||
|
ignot14, "./third_party/protobuf", "m4/ltoptions.m4",
|
||||||
|
"./third_party/protobuf/csharp/src/packages/repositories.config");
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/106
|
||||||
|
#[test]
|
||||||
|
fn regression_106() {
|
||||||
|
Gitignore::from_str("/", " ").unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
1061
src/glob.rs
1061
src/glob.rs
File diff suppressed because it is too large
Load Diff
130
src/ignore.rs
130
src/ignore.rs
@@ -5,7 +5,7 @@ whether a *single* file path should be searched or not.
|
|||||||
In general, there are two ways to ignore a particular file:
|
In general, there are two ways to ignore a particular file:
|
||||||
|
|
||||||
1. Specify an ignore rule in some "global" configuration, such as a
|
1. Specify an ignore rule in some "global" configuration, such as a
|
||||||
$HOME/.rgignore or on the command line.
|
$HOME/.ignore or on the command line.
|
||||||
2. A specific ignore file (like .gitignore) found during directory traversal.
|
2. A specific ignore file (like .gitignore) found during directory traversal.
|
||||||
|
|
||||||
The `IgnoreDir` type handles ignore patterns for any one particular directory
|
The `IgnoreDir` type handles ignore patterns for any one particular directory
|
||||||
@@ -14,16 +14,18 @@ of `IgnoreDir`s for use during directory traversal.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use std::error::Error as StdError;
|
use std::error::Error as StdError;
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use gitignore::{self, Gitignore, GitignoreBuilder, Match, Pattern};
|
use gitignore::{self, Gitignore, GitignoreBuilder, Match, Pattern};
|
||||||
use pathutil::is_hidden;
|
use pathutil::{file_name, is_hidden};
|
||||||
use types::Types;
|
use types::Types;
|
||||||
|
|
||||||
const IGNORE_NAMES: &'static [&'static str] = &[
|
const IGNORE_NAMES: &'static [&'static str] = &[
|
||||||
".gitignore",
|
".gitignore",
|
||||||
|
".ignore",
|
||||||
".rgignore",
|
".rgignore",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -77,7 +79,9 @@ impl From<gitignore::Error> for Error {
|
|||||||
pub struct Ignore {
|
pub struct Ignore {
|
||||||
/// A stack of ignore patterns at each directory level of traversal.
|
/// A stack of ignore patterns at each directory level of traversal.
|
||||||
/// A directory that contributes no ignore patterns is `None`.
|
/// A directory that contributes no ignore patterns is `None`.
|
||||||
stack: Vec<Option<IgnoreDir>>,
|
stack: Vec<IgnoreDir>,
|
||||||
|
/// A stack of parent directories above the root of the current search.
|
||||||
|
parent_stack: Vec<IgnoreDir>,
|
||||||
/// A set of override globs that are always checked first. A match (whether
|
/// A set of override globs that are always checked first. A match (whether
|
||||||
/// it's whitelist or blacklist) trumps anything in stack.
|
/// it's whitelist or blacklist) trumps anything in stack.
|
||||||
overrides: Overrides,
|
overrides: Overrides,
|
||||||
@@ -85,9 +89,11 @@ pub struct Ignore {
|
|||||||
types: Types,
|
types: Types,
|
||||||
/// Whether to ignore hidden files or not.
|
/// Whether to ignore hidden files or not.
|
||||||
ignore_hidden: bool,
|
ignore_hidden: bool,
|
||||||
/// When true, don't look at .gitignore or .agignore files for ignore
|
/// When true, don't look at .gitignore or .ignore files for ignore
|
||||||
/// rules.
|
/// rules.
|
||||||
no_ignore: bool,
|
no_ignore: bool,
|
||||||
|
/// When true, don't look at .gitignore files for ignore rules.
|
||||||
|
no_ignore_vcs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ignore {
|
impl Ignore {
|
||||||
@@ -95,10 +101,12 @@ impl Ignore {
|
|||||||
pub fn new() -> Ignore {
|
pub fn new() -> Ignore {
|
||||||
Ignore {
|
Ignore {
|
||||||
stack: vec![],
|
stack: vec![],
|
||||||
|
parent_stack: vec![],
|
||||||
overrides: Overrides::new(None),
|
overrides: Overrides::new(None),
|
||||||
types: Types::empty(),
|
types: Types::empty(),
|
||||||
ignore_hidden: true,
|
ignore_hidden: true,
|
||||||
no_ignore: false,
|
no_ignore: false,
|
||||||
|
no_ignore_vcs: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +122,12 @@ impl Ignore {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When set, VCS ignore files are ignored.
|
||||||
|
pub fn no_ignore_vcs(&mut self, yes: bool) -> &mut Ignore {
|
||||||
|
self.no_ignore_vcs = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a set of globs that overrides all other match logic.
|
/// Add a set of globs that overrides all other match logic.
|
||||||
pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore {
|
pub fn add_override(&mut self, gi: Gitignore) -> &mut Ignore {
|
||||||
self.overrides = Overrides::new(Some(gi));
|
self.overrides = Overrides::new(Some(gi));
|
||||||
@@ -138,10 +152,13 @@ impl Ignore {
|
|||||||
let mut path = &*path;
|
let mut path = &*path;
|
||||||
let mut saw_git = path.join(".git").is_dir();
|
let mut saw_git = path.join(".git").is_dir();
|
||||||
let mut ignore_names = IGNORE_NAMES.to_vec();
|
let mut ignore_names = IGNORE_NAMES.to_vec();
|
||||||
|
if self.no_ignore_vcs {
|
||||||
|
ignore_names.retain(|&name| name != ".gitignore");
|
||||||
|
}
|
||||||
let mut ignore_dir_results = vec![];
|
let mut ignore_dir_results = vec![];
|
||||||
while let Some(parent) = path.parent() {
|
while let Some(parent) = path.parent() {
|
||||||
if self.no_ignore {
|
if self.no_ignore {
|
||||||
ignore_dir_results.push(Ok(None));
|
ignore_dir_results.push(Ok(IgnoreDir::empty(parent)));
|
||||||
} else {
|
} else {
|
||||||
if saw_git {
|
if saw_git {
|
||||||
ignore_names.retain(|&name| name != ".gitignore");
|
ignore_names.retain(|&name| name != ".gitignore");
|
||||||
@@ -156,7 +173,7 @@ impl Ignore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for ignore_dir_result in ignore_dir_results.into_iter().rev() {
|
for ignore_dir_result in ignore_dir_results.into_iter().rev() {
|
||||||
try!(self.push_ignore_dir(ignore_dir_result));
|
self.parent_stack.push(try!(ignore_dir_result));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -167,10 +184,13 @@ impl Ignore {
|
|||||||
/// stack (and therefore should be popped).
|
/// stack (and therefore should be popped).
|
||||||
pub fn push<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
|
pub fn push<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
|
||||||
if self.no_ignore {
|
if self.no_ignore {
|
||||||
self.stack.push(None);
|
self.stack.push(IgnoreDir::empty(path));
|
||||||
return Ok(());
|
Ok(())
|
||||||
|
} else if self.no_ignore_vcs {
|
||||||
|
self.push_ignore_dir(IgnoreDir::without_vcs(path))
|
||||||
|
} else {
|
||||||
|
self.push_ignore_dir(IgnoreDir::new(path))
|
||||||
}
|
}
|
||||||
self.push_ignore_dir(IgnoreDir::new(path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pushes the result of building a directory matcher on to the stack.
|
/// Pushes the result of building a directory matcher on to the stack.
|
||||||
@@ -178,7 +198,7 @@ impl Ignore {
|
|||||||
/// If the result given contains an error, then it is returned.
|
/// If the result given contains an error, then it is returned.
|
||||||
pub fn push_ignore_dir(
|
pub fn push_ignore_dir(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: Result<Option<IgnoreDir>, Error>,
|
result: Result<IgnoreDir, Error>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
match result {
|
match result {
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
@@ -187,7 +207,7 @@ impl Ignore {
|
|||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Don't leave the stack in an inconsistent state.
|
// Don't leave the stack in an inconsistent state.
|
||||||
self.stack.push(None);
|
self.stack.push(IgnoreDir::empty("error"));
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,12 +227,9 @@ impl Ignore {
|
|||||||
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
||||||
return is_ignored;
|
return is_ignored;
|
||||||
}
|
}
|
||||||
if self.ignore_hidden && is_hidden(&path) {
|
let mut whitelisted = false;
|
||||||
debug!("{} ignored because it is hidden", path.display());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if !self.no_ignore {
|
if !self.no_ignore {
|
||||||
for id in self.stack.iter().rev().filter_map(|id| id.as_ref()) {
|
for id in self.stack.iter().rev() {
|
||||||
let mat = id.matched(path, is_dir);
|
let mat = id.matched(path, is_dir);
|
||||||
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
||||||
if is_ignored {
|
if is_ignored {
|
||||||
@@ -220,13 +237,43 @@ impl Ignore {
|
|||||||
}
|
}
|
||||||
// If this path is whitelisted by an ignore, then
|
// If this path is whitelisted by an ignore, then
|
||||||
// fallthrough and let the file type matcher have a say.
|
// fallthrough and let the file type matcher have a say.
|
||||||
|
whitelisted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If the file has been whitelisted, then we have to stop checking
|
||||||
|
// parent directories. The only thing that can override a whitelist
|
||||||
|
// at this point is a type filter.
|
||||||
|
if !whitelisted {
|
||||||
|
let mut path = path.to_path_buf();
|
||||||
|
for id in self.parent_stack.iter().rev() {
|
||||||
|
if let Some(ref dirname) = id.name {
|
||||||
|
path = Path::new(dirname).join(path);
|
||||||
|
}
|
||||||
|
let mat = id.matched(&*path, is_dir);
|
||||||
|
if let Some(is_ignored) = self.ignore_match(&*path, mat) {
|
||||||
|
if is_ignored {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If this path is whitelisted by an ignore, then
|
||||||
|
// fallthrough and let the file type matcher have a
|
||||||
|
// say.
|
||||||
|
whitelisted = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mat = self.types.matched(path, is_dir);
|
let mat = self.types.matched(path, is_dir);
|
||||||
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
if let Some(is_ignored) = self.ignore_match(path, mat) {
|
||||||
return is_ignored;
|
if is_ignored {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
whitelisted = true;
|
||||||
|
}
|
||||||
|
if !whitelisted && self.ignore_hidden && is_hidden(&path) {
|
||||||
|
debug!("{} ignored because it is hidden", path.display());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -256,9 +303,12 @@ impl Ignore {
|
|||||||
|
|
||||||
/// IgnoreDir represents a set of ignore patterns retrieved from a single
|
/// IgnoreDir represents a set of ignore patterns retrieved from a single
|
||||||
/// directory.
|
/// directory.
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct IgnoreDir {
|
pub struct IgnoreDir {
|
||||||
/// The path to this directory as given.
|
/// The path to this directory as given.
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
/// The directory name, if one exists.
|
||||||
|
name: Option<OsString>,
|
||||||
/// A single accumulation of glob patterns for this directory, matched
|
/// A single accumulation of glob patterns for this directory, matched
|
||||||
/// using gitignore semantics.
|
/// using gitignore semantics.
|
||||||
///
|
///
|
||||||
@@ -272,13 +322,27 @@ pub struct IgnoreDir {
|
|||||||
|
|
||||||
impl IgnoreDir {
|
impl IgnoreDir {
|
||||||
/// Create a new matcher for the given directory.
|
/// Create a new matcher for the given directory.
|
||||||
///
|
pub fn new<P: AsRef<Path>>(path: P) -> Result<IgnoreDir, Error> {
|
||||||
/// If no ignore glob patterns could be found in the directory then `None`
|
|
||||||
/// is returned.
|
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Result<Option<IgnoreDir>, Error> {
|
|
||||||
IgnoreDir::with_ignore_names(path, IGNORE_NAMES.iter())
|
IgnoreDir::with_ignore_names(path, IGNORE_NAMES.iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new matcher for the given directory.
|
||||||
|
///
|
||||||
|
/// Don't respect VCS ignore files.
|
||||||
|
pub fn without_vcs<P: AsRef<Path>>(path: P) -> Result<IgnoreDir, Error> {
|
||||||
|
let names = IGNORE_NAMES.iter().filter(|name| **name != ".gitignore");
|
||||||
|
IgnoreDir::with_ignore_names(path, names)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new IgnoreDir that never matches anything with the given path.
|
||||||
|
pub fn empty<P: AsRef<Path>>(path: P) -> IgnoreDir {
|
||||||
|
IgnoreDir {
|
||||||
|
path: path.as_ref().to_path_buf(),
|
||||||
|
name: file_name(path.as_ref()).map(|s| s.to_os_string()),
|
||||||
|
gi: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new matcher for the given directory using only the ignore
|
/// Create a new matcher for the given directory using only the ignore
|
||||||
/// patterns found in the file names given.
|
/// patterns found in the file names given.
|
||||||
///
|
///
|
||||||
@@ -291,12 +355,9 @@ impl IgnoreDir {
|
|||||||
pub fn with_ignore_names<P: AsRef<Path>, S, I>(
|
pub fn with_ignore_names<P: AsRef<Path>, S, I>(
|
||||||
path: P,
|
path: P,
|
||||||
names: I,
|
names: I,
|
||||||
) -> Result<Option<IgnoreDir>, Error>
|
) -> Result<IgnoreDir, Error>
|
||||||
where P: AsRef<Path>, S: AsRef<str>, I: Iterator<Item=S> {
|
where P: AsRef<Path>, S: AsRef<str>, I: Iterator<Item=S> {
|
||||||
let mut id = IgnoreDir {
|
let mut id = IgnoreDir::empty(path);
|
||||||
path: path.as_ref().to_path_buf(),
|
|
||||||
gi: None,
|
|
||||||
};
|
|
||||||
let mut ok = false;
|
let mut ok = false;
|
||||||
let mut builder = GitignoreBuilder::new(&id.path);
|
let mut builder = GitignoreBuilder::new(&id.path);
|
||||||
// The ordering here is important. Later globs have higher precedence.
|
// The ordering here is important. Later globs have higher precedence.
|
||||||
@@ -304,11 +365,10 @@ impl IgnoreDir {
|
|||||||
ok = builder.add_path(id.path.join(name.as_ref())).is_ok() || ok;
|
ok = builder.add_path(id.path.join(name.as_ref())).is_ok() || ok;
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
Ok(None)
|
return Ok(id);
|
||||||
} else {
|
|
||||||
id.gi = Some(try!(builder.build()));
|
|
||||||
Ok(Some(id))
|
|
||||||
}
|
}
|
||||||
|
id.gi = Some(try!(builder.build()));
|
||||||
|
Ok(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if and only if the given file path should be ignored
|
/// Returns true if and only if the given file path should be ignored
|
||||||
@@ -359,10 +419,6 @@ impl Overrides {
|
|||||||
/// Match::None (and interpreting non-matches as ignored) unless is_dir
|
/// Match::None (and interpreting non-matches as ignored) unless is_dir
|
||||||
/// is true.
|
/// is true.
|
||||||
pub fn matched<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match {
|
pub fn matched<P: AsRef<Path>>(&self, path: P, is_dir: bool) -> Match {
|
||||||
// File types don't apply to directories.
|
|
||||||
if is_dir {
|
|
||||||
return Match::None;
|
|
||||||
}
|
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
self.gi.as_ref()
|
self.gi.as_ref()
|
||||||
.map(|gi| {
|
.map(|gi| {
|
||||||
@@ -394,6 +450,9 @@ mod tests {
|
|||||||
let gi = builder.build().unwrap();
|
let gi = builder.build().unwrap();
|
||||||
let id = IgnoreDir {
|
let id = IgnoreDir {
|
||||||
path: Path::new($root).to_path_buf(),
|
path: Path::new($root).to_path_buf(),
|
||||||
|
name: Path::new($root).file_name().map(|s| {
|
||||||
|
s.to_os_string()
|
||||||
|
}),
|
||||||
gi: Some(gi),
|
gi: Some(gi),
|
||||||
};
|
};
|
||||||
assert!(id.matched($path, false).is_ignored());
|
assert!(id.matched($path, false).is_ignored());
|
||||||
@@ -411,6 +470,9 @@ mod tests {
|
|||||||
let gi = builder.build().unwrap();
|
let gi = builder.build().unwrap();
|
||||||
let id = IgnoreDir {
|
let id = IgnoreDir {
|
||||||
path: Path::new($root).to_path_buf(),
|
path: Path::new($root).to_path_buf(),
|
||||||
|
name: Path::new($root).file_name().map(|s| {
|
||||||
|
s.to_os_string()
|
||||||
|
}),
|
||||||
gi: Some(gi),
|
gi: Some(gi),
|
||||||
};
|
};
|
||||||
assert!(!id.matched($path, false).is_ignored());
|
assert!(!id.matched($path, false).is_ignored());
|
||||||
|
128
src/main.rs
128
src/main.rs
@@ -1,7 +1,7 @@
|
|||||||
extern crate deque;
|
extern crate deque;
|
||||||
extern crate docopt;
|
extern crate docopt;
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate fnv;
|
extern crate globset;
|
||||||
extern crate grep;
|
extern crate grep;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
extern crate kernel32;
|
extern crate kernel32;
|
||||||
@@ -23,11 +23,13 @@ extern crate winapi;
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::Path;
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::result;
|
use std::result;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
use std::cmp;
|
||||||
|
|
||||||
use deque::{Stealer, Stolen};
|
use deque::{Stealer, Stolen};
|
||||||
use grep::Grep;
|
use grep::Grep;
|
||||||
@@ -59,7 +61,6 @@ macro_rules! eprintln {
|
|||||||
mod args;
|
mod args;
|
||||||
mod atty;
|
mod atty;
|
||||||
mod gitignore;
|
mod gitignore;
|
||||||
mod glob;
|
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod out;
|
mod out;
|
||||||
mod pathutil;
|
mod pathutil;
|
||||||
@@ -87,24 +88,29 @@ fn main() {
|
|||||||
fn run(args: Args) -> Result<u64> {
|
fn run(args: Args) -> Result<u64> {
|
||||||
let args = Arc::new(args);
|
let args = Arc::new(args);
|
||||||
let paths = args.paths();
|
let paths = args.paths();
|
||||||
|
let threads = cmp::max(1, args.threads() - 1);
|
||||||
|
let isone =
|
||||||
|
paths.len() == 1 && (paths[0] == Path::new("-") || paths[0].is_file());
|
||||||
if args.files() {
|
if args.files() {
|
||||||
return run_files(args.clone());
|
return run_files(args.clone());
|
||||||
}
|
}
|
||||||
if args.type_list() {
|
if args.type_list() {
|
||||||
return run_types(args.clone());
|
return run_types(args.clone());
|
||||||
}
|
}
|
||||||
if paths.len() == 1 && (paths[0] == Path::new("-") || paths[0].is_file()) {
|
if threads == 1 || isone {
|
||||||
return run_one(args.clone(), &paths[0]);
|
return run_one_thread(args.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let out = Arc::new(Mutex::new(args.out()));
|
let out = Arc::new(Mutex::new(args.out()));
|
||||||
|
let quiet_matched = QuietMatched::new(args.quiet());
|
||||||
let mut workers = vec![];
|
let mut workers = vec![];
|
||||||
|
|
||||||
let workq = {
|
let workq = {
|
||||||
let (workq, stealer) = deque::new();
|
let (workq, stealer) = deque::new();
|
||||||
for _ in 0..args.threads() {
|
for _ in 0..threads {
|
||||||
let worker = MultiWorker {
|
let worker = MultiWorker {
|
||||||
chan_work: stealer.clone(),
|
chan_work: stealer.clone(),
|
||||||
|
quiet_matched: quiet_matched.clone(),
|
||||||
out: out.clone(),
|
out: out.clone(),
|
||||||
outbuf: Some(args.outbuf()),
|
outbuf: Some(args.outbuf()),
|
||||||
worker: Worker {
|
worker: Worker {
|
||||||
@@ -120,11 +126,17 @@ fn run(args: Args) -> Result<u64> {
|
|||||||
};
|
};
|
||||||
let mut paths_searched: u64 = 0;
|
let mut paths_searched: u64 = 0;
|
||||||
for p in paths {
|
for p in paths {
|
||||||
|
if quiet_matched.has_match() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
if p == Path::new("-") {
|
if p == Path::new("-") {
|
||||||
paths_searched += 1;
|
paths_searched += 1;
|
||||||
workq.push(Work::Stdin);
|
workq.push(Work::Stdin);
|
||||||
} else {
|
} else {
|
||||||
for ent in try!(args.walker(p)) {
|
for ent in try!(args.walker(p)) {
|
||||||
|
if quiet_matched.has_match() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
paths_searched += 1;
|
paths_searched += 1;
|
||||||
workq.push(Work::File(ent));
|
workq.push(Work::File(ent));
|
||||||
}
|
}
|
||||||
@@ -145,22 +157,58 @@ fn run(args: Args) -> Result<u64> {
|
|||||||
Ok(match_count)
|
Ok(match_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_one(args: Arc<Args>, path: &Path) -> Result<u64> {
|
fn run_one_thread(args: Arc<Args>) -> Result<u64> {
|
||||||
let mut worker = Worker {
|
let mut worker = Worker {
|
||||||
args: args.clone(),
|
args: args.clone(),
|
||||||
inpbuf: args.input_buffer(),
|
inpbuf: args.input_buffer(),
|
||||||
grep: args.grep(),
|
grep: args.grep(),
|
||||||
match_count: 0,
|
match_count: 0,
|
||||||
};
|
};
|
||||||
let term = args.stdout();
|
let paths = args.paths();
|
||||||
let mut printer = args.printer(term);
|
let mut term = args.stdout();
|
||||||
let work =
|
|
||||||
if path == Path::new("-") {
|
let mut paths_searched: u64 = 0;
|
||||||
WorkReady::Stdin
|
for p in paths {
|
||||||
|
if args.quiet() && worker.match_count > 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if p == Path::new("-") {
|
||||||
|
paths_searched += 1;
|
||||||
|
let mut printer = args.printer(&mut term);
|
||||||
|
if worker.match_count > 0 {
|
||||||
|
if let Some(sep) = args.file_separator() {
|
||||||
|
printer = printer.file_separator(sep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
worker.do_work(&mut printer, WorkReady::Stdin);
|
||||||
} else {
|
} else {
|
||||||
WorkReady::PathFile(path.to_path_buf(), try!(File::open(path)))
|
for ent in try!(args.walker(p)) {
|
||||||
};
|
paths_searched += 1;
|
||||||
worker.do_work(&mut printer, work);
|
let mut printer = args.printer(&mut term);
|
||||||
|
if worker.match_count > 0 {
|
||||||
|
if args.quiet() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if let Some(sep) = args.file_separator() {
|
||||||
|
printer = printer.file_separator(sep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let file = match File::open(ent.path()) {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}: {}", ent.path().display(), err);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
worker.do_work(&mut printer, WorkReady::DirFile(ent, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !paths.is_empty() && paths_searched == 0 {
|
||||||
|
eprintln!("No files were searched, which means ripgrep probably \
|
||||||
|
applied a filter you didn't expect. \
|
||||||
|
Try running again with --debug.");
|
||||||
|
}
|
||||||
Ok(worker.match_count)
|
Ok(worker.match_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,11 +250,11 @@ enum Work {
|
|||||||
enum WorkReady {
|
enum WorkReady {
|
||||||
Stdin,
|
Stdin,
|
||||||
DirFile(DirEntry, File),
|
DirFile(DirEntry, File),
|
||||||
PathFile(PathBuf, File),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MultiWorker {
|
struct MultiWorker {
|
||||||
chan_work: Stealer<Work>,
|
chan_work: Stealer<Work>,
|
||||||
|
quiet_matched: QuietMatched,
|
||||||
out: Arc<Mutex<Out>>,
|
out: Arc<Mutex<Out>>,
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
outbuf: Option<ColoredTerminal<term::TerminfoTerminal<Vec<u8>>>>,
|
outbuf: Option<ColoredTerminal<term::TerminfoTerminal<Vec<u8>>>>,
|
||||||
@@ -225,6 +273,9 @@ struct Worker {
|
|||||||
impl MultiWorker {
|
impl MultiWorker {
|
||||||
fn run(mut self) -> u64 {
|
fn run(mut self) -> u64 {
|
||||||
loop {
|
loop {
|
||||||
|
if self.quiet_matched.has_match() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let work = match self.chan_work.steal() {
|
let work = match self.chan_work.steal() {
|
||||||
Stolen::Empty | Stolen::Abort => continue,
|
Stolen::Empty | Stolen::Abort => continue,
|
||||||
Stolen::Data(Work::Quit) => break,
|
Stolen::Data(Work::Quit) => break,
|
||||||
@@ -243,6 +294,9 @@ impl MultiWorker {
|
|||||||
outbuf.clear();
|
outbuf.clear();
|
||||||
let mut printer = self.worker.args.printer(outbuf);
|
let mut printer = self.worker.args.printer(outbuf);
|
||||||
self.worker.do_work(&mut printer, work);
|
self.worker.do_work(&mut printer, work);
|
||||||
|
if self.quiet_matched.set_match(self.worker.match_count > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
let outbuf = printer.into_inner();
|
let outbuf = printer.into_inner();
|
||||||
if !outbuf.get_ref().is_empty() {
|
if !outbuf.get_ref().is_empty() {
|
||||||
let mut out = self.out.lock().unwrap();
|
let mut out = self.out.lock().unwrap();
|
||||||
@@ -277,17 +331,6 @@ impl Worker {
|
|||||||
self.search(printer, path, file)
|
self.search(printer, path, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WorkReady::PathFile(path, file) => {
|
|
||||||
let mut path = &*path;
|
|
||||||
if let Some(p) = strip_prefix("./", path) {
|
|
||||||
path = p;
|
|
||||||
}
|
|
||||||
if self.args.mmap() {
|
|
||||||
self.search_mmap(printer, path, &file)
|
|
||||||
} else {
|
|
||||||
self.search(printer, path, file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(count) => {
|
Ok(count) => {
|
||||||
@@ -322,7 +365,11 @@ impl Worker {
|
|||||||
) -> Result<u64> {
|
) -> Result<u64> {
|
||||||
if try!(file.metadata()).len() == 0 {
|
if try!(file.metadata()).len() == 0 {
|
||||||
// Opening a memory map with an empty file results in an error.
|
// Opening a memory map with an empty file results in an error.
|
||||||
return Ok(0);
|
// However, this may not actually be an empty file! For example,
|
||||||
|
// /proc/cpuinfo reports itself as an empty file, but it can
|
||||||
|
// produce data when it's read from. Therefore, we fall back to
|
||||||
|
// regular read calls.
|
||||||
|
return self.search(printer, path, file);
|
||||||
}
|
}
|
||||||
let mmap = try!(Mmap::open(file, Protection::Read));
|
let mmap = try!(Mmap::open(file, Protection::Read));
|
||||||
Ok(self.args.searcher_buffer(
|
Ok(self.args.searcher_buffer(
|
||||||
@@ -333,3 +380,28 @@ impl Worker {
|
|||||||
).run())
|
).run())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct QuietMatched(Arc<Option<AtomicBool>>);
|
||||||
|
|
||||||
|
impl QuietMatched {
|
||||||
|
fn new(quiet: bool) -> QuietMatched {
|
||||||
|
let atomic = if quiet { Some(AtomicBool::new(false)) } else { None };
|
||||||
|
QuietMatched(Arc::new(atomic))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_match(&self) -> bool {
|
||||||
|
match *self.0 {
|
||||||
|
None => false,
|
||||||
|
Some(ref matched) => matched.load(Ordering::SeqCst),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_match(&self, yes: bool) -> bool {
|
||||||
|
match *self.0 {
|
||||||
|
None => false,
|
||||||
|
Some(_) if !yes => false,
|
||||||
|
Some(ref m) => { m.store(true, Ordering::SeqCst); true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
59
src/out.rs
59
src/out.rs
@@ -48,8 +48,6 @@ impl Out {
|
|||||||
|
|
||||||
/// If set, the separator is printed between matches from different files.
|
/// If set, the separator is printed between matches from different files.
|
||||||
/// By default, no separator is printed.
|
/// By default, no separator is printed.
|
||||||
///
|
|
||||||
/// If sep is empty, then no file separator is printed.
|
|
||||||
pub fn file_separator(mut self, sep: Vec<u8>) -> Out {
|
pub fn file_separator(mut self, sep: Vec<u8>) -> Out {
|
||||||
self.file_separator = Some(sep);
|
self.file_separator = Some(sep);
|
||||||
self
|
self
|
||||||
@@ -317,3 +315,60 @@ impl<T: Terminal + Send> term::Terminal for ColoredTerminal<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Terminal + Send> term::Terminal for &'a mut ColoredTerminal<T> {
|
||||||
|
type Output = T::Output;
|
||||||
|
|
||||||
|
fn fg(&mut self, fg: term::color::Color) -> term::Result<()> {
|
||||||
|
(**self).fg(fg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bg(&mut self, bg: term::color::Color) -> term::Result<()> {
|
||||||
|
(**self).bg(bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr(&mut self, attr: term::Attr) -> term::Result<()> {
|
||||||
|
(**self).attr(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_attr(&self, attr: term::Attr) -> bool {
|
||||||
|
(**self).supports_attr(attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) -> term::Result<()> {
|
||||||
|
(**self).reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_reset(&self) -> bool {
|
||||||
|
(**self).supports_reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_color(&self) -> bool {
|
||||||
|
(**self).supports_color()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_up(&mut self) -> term::Result<()> {
|
||||||
|
(**self).cursor_up()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_line(&mut self) -> term::Result<()> {
|
||||||
|
(**self).delete_line()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn carriage_return(&mut self) -> term::Result<()> {
|
||||||
|
(**self).carriage_return()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ref(&self) -> &Self::Output {
|
||||||
|
(**self).get_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(&mut self) -> &mut Self::Output {
|
||||||
|
(**self).get_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_inner(self) -> Self::Output {
|
||||||
|
// Good golly miss molly...
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -98,3 +98,21 @@ pub fn is_hidden<P: AsRef<Path>>(path: P) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||||
|
/// the empty string.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use memchr::memchr;
|
||||||
|
|
||||||
|
let path = path.as_ref().as_os_str().as_bytes();
|
||||||
|
memchr(b'/', path).is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this file path is just a file name. i.e., Its parent is
|
||||||
|
/// the empty string.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn is_file_name<P: AsRef<Path>>(path: P) -> bool {
|
||||||
|
path.as_ref().parent().map(|p| p.as_os_str().is_empty()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
145
src/printer.rs
145
src/printer.rs
@@ -4,6 +4,7 @@ use regex::bytes::Regex;
|
|||||||
use term::{Attr, Terminal};
|
use term::{Attr, Terminal};
|
||||||
use term::color;
|
use term::color;
|
||||||
|
|
||||||
|
use pathutil::strip_prefix;
|
||||||
use types::FileTypeDef;
|
use types::FileTypeDef;
|
||||||
|
|
||||||
/// Printer encapsulates all output logic for searching.
|
/// Printer encapsulates all output logic for searching.
|
||||||
@@ -24,18 +25,49 @@ pub struct Printer<W> {
|
|||||||
/// printed via the match directly, but occasionally we need to insert them
|
/// printed via the match directly, but occasionally we need to insert them
|
||||||
/// ourselves (for example, to print a context separator).
|
/// ourselves (for example, to print a context separator).
|
||||||
eol: u8,
|
eol: u8,
|
||||||
|
/// A file separator to show before any matches are printed.
|
||||||
|
file_separator: Option<Vec<u8>>,
|
||||||
/// Whether to show file name as a heading or not.
|
/// Whether to show file name as a heading or not.
|
||||||
///
|
///
|
||||||
/// N.B. If with_filename is false, then this setting has no effect.
|
/// N.B. If with_filename is false, then this setting has no effect.
|
||||||
heading: bool,
|
heading: bool,
|
||||||
/// Whether to show every match on its own line.
|
/// Whether to show every match on its own line.
|
||||||
line_per_match: bool,
|
line_per_match: bool,
|
||||||
/// Whether to suppress all output.
|
/// Whether to print NUL bytes after a file path instead of new lines
|
||||||
quiet: bool,
|
/// or `:`.
|
||||||
|
null: bool,
|
||||||
/// A string to use as a replacement of each match in a matching line.
|
/// A string to use as a replacement of each match in a matching line.
|
||||||
replace: Option<Vec<u8>>,
|
replace: Option<Vec<u8>>,
|
||||||
/// Whether to prefix each match with the corresponding file name.
|
/// Whether to prefix each match with the corresponding file name.
|
||||||
with_filename: bool,
|
with_filename: bool,
|
||||||
|
/// The choice of colors.
|
||||||
|
color_choice: ColorChoice
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ColorChoice {
|
||||||
|
matched_line: color::Color,
|
||||||
|
heading: color::Color,
|
||||||
|
line_number: color::Color
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorChoice {
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn new() -> ColorChoice {
|
||||||
|
ColorChoice {
|
||||||
|
matched_line: color::RED,
|
||||||
|
heading: color::GREEN,
|
||||||
|
line_number: color::BLUE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn new() -> ColorChoice {
|
||||||
|
ColorChoice {
|
||||||
|
matched_line: color::BRIGHT_RED,
|
||||||
|
heading: color::BRIGHT_GREEN,
|
||||||
|
line_number: color::BRIGHT_BLUE
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Terminal + Send> Printer<W> {
|
impl<W: Terminal + Send> Printer<W> {
|
||||||
@@ -47,11 +79,13 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
column: false,
|
column: false,
|
||||||
context_separator: "--".to_string().into_bytes(),
|
context_separator: "--".to_string().into_bytes(),
|
||||||
eol: b'\n',
|
eol: b'\n',
|
||||||
|
file_separator: None,
|
||||||
heading: false,
|
heading: false,
|
||||||
line_per_match: false,
|
line_per_match: false,
|
||||||
quiet: false,
|
null: false,
|
||||||
replace: None,
|
replace: None,
|
||||||
with_filename: false,
|
with_filename: false,
|
||||||
|
color_choice: ColorChoice::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +108,13 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If set, the separator is printed before any matches. By default, no
|
||||||
|
/// separator is printed.
|
||||||
|
pub fn file_separator(mut self, sep: Vec<u8>) -> Printer<W> {
|
||||||
|
self.file_separator = Some(sep);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether to show file name as a heading or not.
|
/// Whether to show file name as a heading or not.
|
||||||
///
|
///
|
||||||
/// N.B. If with_filename is false, then this setting has no effect.
|
/// N.B. If with_filename is false, then this setting has no effect.
|
||||||
@@ -88,9 +129,10 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When set, all output is suppressed.
|
/// Whether to cause NUL bytes to follow file paths instead of other
|
||||||
pub fn quiet(mut self, yes: bool) -> Printer<W> {
|
/// visual separators (like `:`, `-` and `\n`).
|
||||||
self.quiet = yes;
|
pub fn null(mut self, yes: bool) -> Printer<W> {
|
||||||
|
self.null = yes;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,15 +180,24 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
|
|
||||||
/// Prints the given path.
|
/// Prints the given path.
|
||||||
pub fn path<P: AsRef<Path>>(&mut self, path: P) {
|
pub fn path<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
self.write(path.as_ref().to_string_lossy().as_bytes());
|
let path = strip_prefix("./", path.as_ref()).unwrap_or(path.as_ref());
|
||||||
self.write_eol();
|
self.write_path(path);
|
||||||
|
if self.null {
|
||||||
|
self.write(b"\x00");
|
||||||
|
} else {
|
||||||
|
self.write_eol();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prints the given path and a count of the number of matches found.
|
/// Prints the given path and a count of the number of matches found.
|
||||||
pub fn path_count<P: AsRef<Path>>(&mut self, path: P, count: u64) {
|
pub fn path_count<P: AsRef<Path>>(&mut self, path: P, count: u64) {
|
||||||
if self.with_filename {
|
if self.with_filename {
|
||||||
self.write(path.as_ref().to_string_lossy().as_bytes());
|
self.write_path(path);
|
||||||
self.write(b":");
|
if self.null {
|
||||||
|
self.write(b"\x00");
|
||||||
|
} else {
|
||||||
|
self.write(b":");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.write(count.to_string().as_bytes());
|
self.write(count.to_string().as_bytes());
|
||||||
self.write_eol();
|
self.write_eol();
|
||||||
@@ -155,9 +206,6 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
/// Prints the context separator.
|
/// Prints the context separator.
|
||||||
pub fn context_separate(&mut self) {
|
pub fn context_separate(&mut self) {
|
||||||
// N.B. We can't use `write` here because of borrowing restrictions.
|
// N.B. We can't use `write` here because of borrowing restrictions.
|
||||||
if self.quiet {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if self.context_separator.is_empty() {
|
if self.context_separator.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -179,7 +227,7 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
let column =
|
let column =
|
||||||
if self.column {
|
if self.column {
|
||||||
Some(re.find(&buf[start..end])
|
Some(re.find(&buf[start..end])
|
||||||
.map(|(s, _)| s + 1).unwrap_or(0) as u64)
|
.map(|(s, _)| s).unwrap_or(0) as u64)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -204,16 +252,16 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
column: Option<u64>,
|
column: Option<u64>,
|
||||||
) {
|
) {
|
||||||
if self.heading && self.with_filename && !self.has_printed {
|
if self.heading && self.with_filename && !self.has_printed {
|
||||||
|
self.write_file_sep();
|
||||||
self.write_heading(path.as_ref());
|
self.write_heading(path.as_ref());
|
||||||
} else if !self.heading && self.with_filename {
|
} else if !self.heading && self.with_filename {
|
||||||
self.write(path.as_ref().to_string_lossy().as_bytes());
|
self.write_non_heading_path(path.as_ref());
|
||||||
self.write(b":");
|
|
||||||
}
|
}
|
||||||
if let Some(line_number) = line_number {
|
if let Some(line_number) = line_number {
|
||||||
self.line_number(line_number, b':');
|
self.line_number(line_number, b':');
|
||||||
}
|
}
|
||||||
if let Some(c) = column {
|
if let Some(c) = column {
|
||||||
self.write(c.to_string().as_bytes());
|
self.write((c + 1).to_string().as_bytes());
|
||||||
self.write(b":");
|
self.write(b":");
|
||||||
}
|
}
|
||||||
if self.replace.is_some() {
|
if self.replace.is_some() {
|
||||||
@@ -236,7 +284,7 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
let mut last_written = 0;
|
let mut last_written = 0;
|
||||||
for (s, e) in re.find_iter(buf) {
|
for (s, e) in re.find_iter(buf) {
|
||||||
self.write(&buf[last_written..s]);
|
self.write(&buf[last_written..s]);
|
||||||
let _ = self.wtr.fg(color::BRIGHT_RED);
|
let _ = self.wtr.fg(self.color_choice.matched_line);
|
||||||
let _ = self.wtr.attr(Attr::Bold);
|
let _ = self.wtr.attr(Attr::Bold);
|
||||||
self.write(&buf[s..e]);
|
self.write(&buf[s..e]);
|
||||||
let _ = self.wtr.reset();
|
let _ = self.wtr.reset();
|
||||||
@@ -254,10 +302,15 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
line_number: Option<u64>,
|
line_number: Option<u64>,
|
||||||
) {
|
) {
|
||||||
if self.heading && self.with_filename && !self.has_printed {
|
if self.heading && self.with_filename && !self.has_printed {
|
||||||
|
self.write_file_sep();
|
||||||
self.write_heading(path.as_ref());
|
self.write_heading(path.as_ref());
|
||||||
} else if !self.heading && self.with_filename {
|
} else if !self.heading && self.with_filename {
|
||||||
self.write(path.as_ref().to_string_lossy().as_bytes());
|
self.write_path(path.as_ref());
|
||||||
self.write(b"-");
|
if self.null {
|
||||||
|
self.write(b"\x00");
|
||||||
|
} else {
|
||||||
|
self.write(b"-");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some(line_number) = line_number {
|
if let Some(line_number) = line_number {
|
||||||
self.line_number(line_number, b'-');
|
self.line_number(line_number, b'-');
|
||||||
@@ -270,19 +323,39 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
|
|
||||||
fn write_heading<P: AsRef<Path>>(&mut self, path: P) {
|
fn write_heading<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
if self.wtr.supports_color() {
|
if self.wtr.supports_color() {
|
||||||
let _ = self.wtr.fg(color::BRIGHT_GREEN);
|
let _ = self.wtr.fg(self.color_choice.heading);
|
||||||
let _ = self.wtr.attr(Attr::Bold);
|
let _ = self.wtr.attr(Attr::Bold);
|
||||||
}
|
}
|
||||||
self.write(path.as_ref().to_string_lossy().as_bytes());
|
self.write_path(path.as_ref());
|
||||||
self.write_eol();
|
if self.null {
|
||||||
|
self.write(b"\x00");
|
||||||
|
} else {
|
||||||
|
self.write_eol();
|
||||||
|
}
|
||||||
if self.wtr.supports_color() {
|
if self.wtr.supports_color() {
|
||||||
let _ = self.wtr.reset();
|
let _ = self.wtr.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_non_heading_path<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
|
if self.wtr.supports_color() {
|
||||||
|
let _ = self.wtr.fg(self.color_choice.heading);
|
||||||
|
let _ = self.wtr.attr(Attr::Bold);
|
||||||
|
}
|
||||||
|
self.write_path(path.as_ref());
|
||||||
|
if self.wtr.supports_color() {
|
||||||
|
let _ = self.wtr.reset();
|
||||||
|
}
|
||||||
|
if self.null {
|
||||||
|
self.write(b"\x00");
|
||||||
|
} else {
|
||||||
|
self.write(b":");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn line_number(&mut self, n: u64, sep: u8) {
|
fn line_number(&mut self, n: u64, sep: u8) {
|
||||||
if self.wtr.supports_color() {
|
if self.wtr.supports_color() {
|
||||||
let _ = self.wtr.fg(color::BRIGHT_BLUE);
|
let _ = self.wtr.fg(self.color_choice.line_number);
|
||||||
let _ = self.wtr.attr(Attr::Bold);
|
let _ = self.wtr.attr(Attr::Bold);
|
||||||
}
|
}
|
||||||
self.write(n.to_string().as_bytes());
|
self.write(n.to_string().as_bytes());
|
||||||
@@ -292,10 +365,20 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
self.write(&[sep]);
|
self.write(&[sep]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn write_path<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
|
let path = path.as_ref().as_os_str().as_bytes();
|
||||||
|
self.write(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn write_path<P: AsRef<Path>>(&mut self, path: P) {
|
||||||
|
self.write(path.as_ref().to_string_lossy().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
fn write(&mut self, buf: &[u8]) {
|
fn write(&mut self, buf: &[u8]) {
|
||||||
if self.quiet {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.has_printed = true;
|
self.has_printed = true;
|
||||||
let _ = self.wtr.write_all(buf);
|
let _ = self.wtr.write_all(buf);
|
||||||
}
|
}
|
||||||
@@ -304,4 +387,12 @@ impl<W: Terminal + Send> Printer<W> {
|
|||||||
let eol = self.eol;
|
let eol = self.eol;
|
||||||
self.write(&[eol]);
|
self.write(&[eol]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_file_sep(&mut self) {
|
||||||
|
if let Some(ref sep) = self.file_separator {
|
||||||
|
self.has_printed = true;
|
||||||
|
let _ = self.wtr.write_all(sep);
|
||||||
|
let _ = self.wtr.write_all(b"\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -53,6 +53,14 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If enabled, searching will print the path instead of each match.
|
||||||
|
///
|
||||||
|
/// Disabled by default.
|
||||||
|
pub fn files_with_matches(mut self, yes: bool) -> Self {
|
||||||
|
self.opts.files_with_matches = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the end-of-line byte used by this searcher.
|
/// Set the end-of-line byte used by this searcher.
|
||||||
pub fn eol(mut self, eol: u8) -> Self {
|
pub fn eol(mut self, eol: u8) -> Self {
|
||||||
self.opts.eol = eol;
|
self.opts.eol = eol;
|
||||||
@@ -73,6 +81,13 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If enabled, don't show any output and quit searching after the first
|
||||||
|
/// match is found.
|
||||||
|
pub fn quiet(mut self, yes: bool) -> Self {
|
||||||
|
self.opts.quiet = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// If enabled, search binary files as if they were text.
|
/// If enabled, search binary files as if they were text.
|
||||||
pub fn text(mut self, yes: bool) -> Self {
|
pub fn text(mut self, yes: bool) -> Self {
|
||||||
self.opts.text = yes;
|
self.opts.text = yes;
|
||||||
@@ -96,6 +111,9 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> {
|
|||||||
self.print_match(m.start(), m.end());
|
self.print_match(m.start(), m.end());
|
||||||
}
|
}
|
||||||
last_end = m.end();
|
last_end = m.end();
|
||||||
|
if self.opts.stop_after_first_match() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if self.opts.invert_match {
|
if self.opts.invert_match {
|
||||||
let upto = self.buf.len();
|
let upto = self.buf.len();
|
||||||
@@ -104,13 +122,16 @@ impl<'a, W: Send + Terminal> BufferSearcher<'a, W> {
|
|||||||
if self.opts.count && self.match_count > 0 {
|
if self.opts.count && self.match_count > 0 {
|
||||||
self.printer.path_count(self.path, self.match_count);
|
self.printer.path_count(self.path, self.match_count);
|
||||||
}
|
}
|
||||||
|
if self.opts.files_with_matches && self.match_count > 0 {
|
||||||
|
self.printer.path(self.path);
|
||||||
|
}
|
||||||
self.match_count
|
self.match_count
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn print_match(&mut self, start: usize, end: usize) {
|
pub fn print_match(&mut self, start: usize, end: usize) {
|
||||||
self.match_count += 1;
|
self.match_count += 1;
|
||||||
if self.opts.count {
|
if self.opts.skip_matches() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.count_lines(start);
|
self.count_lines(start);
|
||||||
@@ -237,6 +258,14 @@ and exhibited clearly, with a label attached.\
|
|||||||
assert_eq!(out, "/baz.rs:2\n");
|
assert_eq!(out, "/baz.rs:2\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn files_with_matches() {
|
||||||
|
let (count, out) = search(
|
||||||
|
"Sherlock", SHERLOCK, |s| s.files_with_matches(true));
|
||||||
|
assert_eq!(1, count);
|
||||||
|
assert_eq!(out, "/baz.rs\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invert_match() {
|
fn invert_match() {
|
||||||
let (count, out) = search(
|
let (count, out) = search(
|
||||||
|
@@ -80,9 +80,11 @@ pub struct Options {
|
|||||||
pub after_context: usize,
|
pub after_context: usize,
|
||||||
pub before_context: usize,
|
pub before_context: usize,
|
||||||
pub count: bool,
|
pub count: bool,
|
||||||
|
pub files_with_matches: bool,
|
||||||
pub eol: u8,
|
pub eol: u8,
|
||||||
pub invert_match: bool,
|
pub invert_match: bool,
|
||||||
pub line_number: bool,
|
pub line_number: bool,
|
||||||
|
pub quiet: bool,
|
||||||
pub text: bool,
|
pub text: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,12 +94,29 @@ impl Default for Options {
|
|||||||
after_context: 0,
|
after_context: 0,
|
||||||
before_context: 0,
|
before_context: 0,
|
||||||
count: false,
|
count: false,
|
||||||
|
files_with_matches: false,
|
||||||
eol: b'\n',
|
eol: b'\n',
|
||||||
invert_match: false,
|
invert_match: false,
|
||||||
line_number: false,
|
line_number: false,
|
||||||
|
quiet: false,
|
||||||
text: false,
|
text: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Options {
|
||||||
|
/// Several options (--quiet, --count, --files-with-matches) imply that
|
||||||
|
/// we shouldn't ever display matches.
|
||||||
|
pub fn skip_matches(&self) -> bool {
|
||||||
|
self.count || self.files_with_matches || self.quiet
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some options (--quiet, --files-with-matches) imply that we can stop
|
||||||
|
/// searching after the first match.
|
||||||
|
pub fn stop_after_first_match(&self) -> bool {
|
||||||
|
self.files_with_matches || self.quiet
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
||||||
@@ -158,6 +177,14 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If enabled, searching will print the path instead of each match.
|
||||||
|
///
|
||||||
|
/// Disabled by default.
|
||||||
|
pub fn files_with_matches(mut self, yes: bool) -> Self {
|
||||||
|
self.opts.files_with_matches = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Set the end-of-line byte used by this searcher.
|
/// Set the end-of-line byte used by this searcher.
|
||||||
pub fn eol(mut self, eol: u8) -> Self {
|
pub fn eol(mut self, eol: u8) -> Self {
|
||||||
self.opts.eol = eol;
|
self.opts.eol = eol;
|
||||||
@@ -178,6 +205,13 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If enabled, don't show any output and quit searching after the first
|
||||||
|
/// match is found.
|
||||||
|
pub fn quiet(mut self, yes: bool) -> Self {
|
||||||
|
self.opts.quiet = yes;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// If enabled, search binary files as if they were text.
|
/// If enabled, search binary files as if they were text.
|
||||||
pub fn text(mut self, yes: bool) -> Self {
|
pub fn text(mut self, yes: bool) -> Self {
|
||||||
self.opts.text = yes;
|
self.opts.text = yes;
|
||||||
@@ -193,7 +227,7 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
self.line_count = if self.opts.line_number { Some(0) } else { None };
|
self.line_count = if self.opts.line_number { Some(0) } else { None };
|
||||||
self.last_match = Match::default();
|
self.last_match = Match::default();
|
||||||
self.after_context_remaining = 0;
|
self.after_context_remaining = 0;
|
||||||
loop {
|
while !self.terminate() {
|
||||||
let upto = self.inp.lastnl;
|
let upto = self.inp.lastnl;
|
||||||
self.print_after_context(upto);
|
self.print_after_context(upto);
|
||||||
if !try!(self.fill()) {
|
if !try!(self.fill()) {
|
||||||
@@ -202,7 +236,7 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
if !self.opts.text && self.inp.is_binary {
|
if !self.opts.text && self.inp.is_binary {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
while self.inp.pos < self.inp.lastnl {
|
while !self.terminate() && self.inp.pos < self.inp.lastnl {
|
||||||
let matched = self.grep.read_match(
|
let matched = self.grep.read_match(
|
||||||
&mut self.last_match,
|
&mut self.last_match,
|
||||||
&mut self.inp.buf[..self.inp.lastnl],
|
&mut self.inp.buf[..self.inp.lastnl],
|
||||||
@@ -234,12 +268,21 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.opts.count && self.match_count > 0 {
|
if self.match_count > 0 {
|
||||||
self.printer.path_count(self.path, self.match_count);
|
if self.opts.count {
|
||||||
|
self.printer.path_count(self.path, self.match_count);
|
||||||
|
} else if self.opts.files_with_matches {
|
||||||
|
self.printer.path(self.path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(self.match_count)
|
Ok(self.match_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn terminate(&self) -> bool {
|
||||||
|
self.match_count > 0 && self.opts.stop_after_first_match()
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn fill(&mut self) -> Result<bool, Error> {
|
fn fill(&mut self) -> Result<bool, Error> {
|
||||||
let mut keep = self.inp.lastnl;
|
let mut keep = self.inp.lastnl;
|
||||||
@@ -281,7 +324,7 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn print_before_context(&mut self, upto: usize) {
|
fn print_before_context(&mut self, upto: usize) {
|
||||||
if self.opts.count || self.opts.before_context == 0 {
|
if self.opts.skip_matches() || self.opts.before_context == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let start = self.last_printed;
|
let start = self.last_printed;
|
||||||
@@ -304,7 +347,7 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn print_after_context(&mut self, upto: usize) {
|
fn print_after_context(&mut self, upto: usize) {
|
||||||
if self.opts.count || self.after_context_remaining == 0 {
|
if self.opts.skip_matches() || self.after_context_remaining == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let start = self.last_printed;
|
let start = self.last_printed;
|
||||||
@@ -322,7 +365,7 @@ impl<'a, R: io::Read, W: Terminal + Send> Searcher<'a, R, W> {
|
|||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
fn print_match(&mut self, start: usize, end: usize) {
|
fn print_match(&mut self, start: usize, end: usize) {
|
||||||
self.match_count += 1;
|
self.match_count += 1;
|
||||||
if self.opts.count {
|
if self.opts.skip_matches() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.print_separator(start);
|
self.print_separator(start);
|
||||||
@@ -503,10 +546,6 @@ impl InputBuffer {
|
|||||||
if self.first && is_binary(&self.buf[self.end..self.end + n]) {
|
if self.first && is_binary(&self.buf[self.end..self.end + n]) {
|
||||||
self.is_binary = true;
|
self.is_binary = true;
|
||||||
}
|
}
|
||||||
if self.is_binary {
|
|
||||||
replace_buf(
|
|
||||||
&mut self.buf[self.end..self.end + n], b'\x00', self.eol);
|
|
||||||
}
|
|
||||||
self.first = false;
|
self.first = false;
|
||||||
// We assume that reading 0 bytes means we've hit EOF.
|
// We assume that reading 0 bytes means we've hit EOF.
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
@@ -629,6 +668,7 @@ pub fn count_lines(buf: &[u8], eol: u8) -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces a with b in buf.
|
/// Replaces a with b in buf.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn replace_buf(buf: &mut [u8], a: u8, b: u8) {
|
fn replace_buf(buf: &mut [u8], a: u8, b: u8) {
|
||||||
if a == b {
|
if a == b {
|
||||||
return;
|
return;
|
||||||
@@ -970,7 +1010,7 @@ fn main() {
|
|||||||
let text = "Sherlock\n\x00Holmes\n";
|
let text = "Sherlock\n\x00Holmes\n";
|
||||||
let (count, out) = search("Sherlock|Holmes", text, |s| s.text(true));
|
let (count, out) = search("Sherlock|Holmes", text, |s| s.text(true));
|
||||||
assert_eq!(2, count);
|
assert_eq!(2, count);
|
||||||
assert_eq!(out, "/baz.rs:Sherlock\n/baz.rs:Holmes\n");
|
assert_eq!(out, "/baz.rs:Sherlock\n/baz.rs:\x00Holmes\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -992,6 +1032,14 @@ fn main() {
|
|||||||
assert_eq!(out, "/baz.rs:2\n");
|
assert_eq!(out, "/baz.rs:2\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn files_with_matches() {
|
||||||
|
let (count, out) = search_smallcap(
|
||||||
|
"Sherlock", SHERLOCK, |s| s.files_with_matches(true));
|
||||||
|
assert_eq!(1, count);
|
||||||
|
assert_eq!(out, "/baz.rs\n");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invert_match() {
|
fn invert_match() {
|
||||||
let (count, out) = search_smallcap(
|
let (count, out) = search_smallcap(
|
||||||
|
73
src/types.rs
73
src/types.rs
@@ -11,16 +11,17 @@ use std::path::Path;
|
|||||||
use regex;
|
use regex;
|
||||||
|
|
||||||
use gitignore::{Match, Pattern};
|
use gitignore::{Match, Pattern};
|
||||||
use glob::{self, MatchOptions};
|
use globset::{self, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||||
|
|
||||||
const TYPE_EXTENSIONS: &'static [(&'static str, &'static [&'static str])] = &[
|
const TYPE_EXTENSIONS: &'static [(&'static str, &'static [&'static str])] = &[
|
||||||
("asm", &["*.asm", "*.s", "*.S"]),
|
("asm", &["*.asm", "*.s", "*.S"]),
|
||||||
("awk", &["*.awk"]),
|
("awk", &["*.awk"]),
|
||||||
("c", &["*.c", "*.h", "*.H"]),
|
("c", &["*.c", "*.h", "*.H"]),
|
||||||
("cbor", &["*.cbor"]),
|
("cbor", &["*.cbor"]),
|
||||||
("clojure", &["*.clj", "*.cljs"]),
|
("clojure", &["*.clj", "*.cljc", "*.cljs", "*.cljx"]),
|
||||||
("cmake", &["CMakeLists.txt"]),
|
("cmake", &["*.cmake", "CMakeLists.txt"]),
|
||||||
("coffeescript", &["*.coffee"]),
|
("coffeescript", &["*.coffee"]),
|
||||||
|
("config", &["*.config"]),
|
||||||
("cpp", &[
|
("cpp", &[
|
||||||
"*.C", "*.cc", "*.cpp", "*.cxx",
|
"*.C", "*.cc", "*.cpp", "*.cxx",
|
||||||
"*.h", "*.H", "*.hh", "*.hpp",
|
"*.h", "*.H", "*.hh", "*.hpp",
|
||||||
@@ -36,12 +37,16 @@ const TYPE_EXTENSIONS: &'static [(&'static str, &'static [&'static str])] = &[
|
|||||||
"*.f", "*.F", "*.f77", "*.F77", "*.pfo",
|
"*.f", "*.F", "*.f77", "*.F77", "*.pfo",
|
||||||
"*.f90", "*.F90", "*.f95", "*.F95",
|
"*.f90", "*.F90", "*.f95", "*.F95",
|
||||||
]),
|
]),
|
||||||
|
("fsharp", &["*.fs", "*.fsx", "*.fsi"]),
|
||||||
("go", &["*.go"]),
|
("go", &["*.go"]),
|
||||||
("groovy", &["*.groovy"]),
|
("groovy", &["*.groovy"]),
|
||||||
("haskell", &["*.hs", "*.lhs"]),
|
("haskell", &["*.hs", "*.lhs"]),
|
||||||
("html", &["*.htm", "*.html"]),
|
("html", &["*.htm", "*.html"]),
|
||||||
("java", &["*.java"]),
|
("java", &["*.java"]),
|
||||||
("js", &["*.js"]),
|
("jinja", &["*.jinja", "*.jinja2"]),
|
||||||
|
("js", &[
|
||||||
|
"*.js", "*.jsx", "*.vue",
|
||||||
|
]),
|
||||||
("json", &["*.json"]),
|
("json", &["*.json"]),
|
||||||
("jsonl", &["*.jsonl"]),
|
("jsonl", &["*.jsonl"]),
|
||||||
("lisp", &["*.el", "*.jl", "*.lisp", "*.lsp", "*.sc", "*.scm"]),
|
("lisp", &["*.el", "*.jl", "*.lisp", "*.lsp", "*.sc", "*.scm"]),
|
||||||
@@ -49,9 +54,11 @@ const TYPE_EXTENSIONS: &'static [(&'static str, &'static [&'static str])] = &[
|
|||||||
("m4", &["*.ac", "*.m4"]),
|
("m4", &["*.ac", "*.m4"]),
|
||||||
("make", &["gnumakefile", "Gnumakefile", "makefile", "Makefile", "*.mk"]),
|
("make", &["gnumakefile", "Gnumakefile", "makefile", "Makefile", "*.mk"]),
|
||||||
("markdown", &["*.md"]),
|
("markdown", &["*.md"]),
|
||||||
|
("md", &["*.md"]),
|
||||||
("matlab", &["*.m"]),
|
("matlab", &["*.m"]),
|
||||||
("mk", &["mkfile"]),
|
("mk", &["mkfile"]),
|
||||||
("ml", &["*.ml"]),
|
("ml", &["*.ml"]),
|
||||||
|
("nim", &["*.nim"]),
|
||||||
("objc", &["*.h", "*.m"]),
|
("objc", &["*.h", "*.m"]),
|
||||||
("objcpp", &["*.h", "*.mm"]),
|
("objcpp", &["*.h", "*.mm"]),
|
||||||
("ocaml", &["*.ml", "*.mli", "*.mll", "*.mly"]),
|
("ocaml", &["*.ml", "*.mli", "*.mll", "*.mly"]),
|
||||||
@@ -59,17 +66,22 @@ const TYPE_EXTENSIONS: &'static [(&'static str, &'static [&'static str])] = &[
|
|||||||
("php", &["*.php", "*.php3", "*.php4", "*.php5", "*.phtml"]),
|
("php", &["*.php", "*.php3", "*.php4", "*.php5", "*.phtml"]),
|
||||||
("py", &["*.py"]),
|
("py", &["*.py"]),
|
||||||
("readme", &["README*", "*README"]),
|
("readme", &["README*", "*README"]),
|
||||||
("rr", &["*.R"]),
|
("r", &["*.R", "*.r", "*.Rmd", "*.Rnw"]),
|
||||||
("rst", &["*.rst"]),
|
("rst", &["*.rst"]),
|
||||||
("ruby", &["*.rb"]),
|
("ruby", &["*.rb"]),
|
||||||
("rust", &["*.rs"]),
|
("rust", &["*.rs"]),
|
||||||
("scala", &["*.scala"]),
|
("scala", &["*.scala"]),
|
||||||
("sh", &["*.bash", "*.csh", "*.ksh", "*.sh", "*.tcsh"]),
|
("sh", &["*.bash", "*.csh", "*.ksh", "*.sh", "*.tcsh"]),
|
||||||
|
("spark", &["*.spark"]),
|
||||||
("sql", &["*.sql"]),
|
("sql", &["*.sql"]),
|
||||||
|
("sv", &["*.v", "*.vg", "*.sv", "*.svh", "*.h"]),
|
||||||
|
("swift", &["*.swift"]),
|
||||||
("tex", &["*.tex", "*.cls", "*.sty"]),
|
("tex", &["*.tex", "*.cls", "*.sty"]),
|
||||||
|
("ts", &["*.ts", "*.tsx"]),
|
||||||
("txt", &["*.txt"]),
|
("txt", &["*.txt"]),
|
||||||
("toml", &["*.toml", "Cargo.lock"]),
|
("toml", &["*.toml", "Cargo.lock"]),
|
||||||
("vala", &["*.vala"]),
|
("vala", &["*.vala"]),
|
||||||
|
("vb", &["*.vb"]),
|
||||||
("vimscript", &["*.vim"]),
|
("vimscript", &["*.vim"]),
|
||||||
("xml", &["*.xml"]),
|
("xml", &["*.xml"]),
|
||||||
("yacc", &["*.y"]),
|
("yacc", &["*.y"]),
|
||||||
@@ -85,7 +97,7 @@ pub enum Error {
|
|||||||
/// A user specified file type definition could not be parsed.
|
/// A user specified file type definition could not be parsed.
|
||||||
InvalidDefinition,
|
InvalidDefinition,
|
||||||
/// There was an error building the matcher (probably a bad glob).
|
/// There was an error building the matcher (probably a bad glob).
|
||||||
Glob(glob::Error),
|
Glob(globset::Error),
|
||||||
/// There was an error compiling a glob as a regex.
|
/// There was an error compiling a glob as a regex.
|
||||||
Regex(regex::Error),
|
Regex(regex::Error),
|
||||||
}
|
}
|
||||||
@@ -117,8 +129,8 @@ impl fmt::Display for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<glob::Error> for Error {
|
impl From<globset::Error> for Error {
|
||||||
fn from(err: glob::Error) -> Error {
|
fn from(err: globset::Error) -> Error {
|
||||||
Error::Glob(err)
|
Error::Glob(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,8 +163,9 @@ impl FileTypeDef {
|
|||||||
/// Types is a file type matcher.
|
/// Types is a file type matcher.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Types {
|
pub struct Types {
|
||||||
selected: Option<glob::SetYesNo>,
|
defs: Vec<FileTypeDef>,
|
||||||
negated: Option<glob::SetYesNo>,
|
selected: Option<GlobSet>,
|
||||||
|
negated: Option<GlobSet>,
|
||||||
has_selected: bool,
|
has_selected: bool,
|
||||||
unmatched_pat: Pattern,
|
unmatched_pat: Pattern,
|
||||||
}
|
}
|
||||||
@@ -165,11 +178,13 @@ impl Types {
|
|||||||
/// If has_selected is true, then at least one file type was selected.
|
/// If has_selected is true, then at least one file type was selected.
|
||||||
/// Therefore, any non-matches should be ignored.
|
/// Therefore, any non-matches should be ignored.
|
||||||
fn new(
|
fn new(
|
||||||
selected: Option<glob::SetYesNo>,
|
selected: Option<GlobSet>,
|
||||||
negated: Option<glob::SetYesNo>,
|
negated: Option<GlobSet>,
|
||||||
has_selected: bool,
|
has_selected: bool,
|
||||||
|
defs: Vec<FileTypeDef>,
|
||||||
) -> Types {
|
) -> Types {
|
||||||
Types {
|
Types {
|
||||||
|
defs: defs,
|
||||||
selected: selected,
|
selected: selected,
|
||||||
negated: negated,
|
negated: negated,
|
||||||
has_selected: has_selected,
|
has_selected: has_selected,
|
||||||
@@ -185,7 +200,7 @@ impl Types {
|
|||||||
|
|
||||||
/// Creates a new file type matcher that never matches.
|
/// Creates a new file type matcher that never matches.
|
||||||
pub fn empty() -> Types {
|
pub fn empty() -> Types {
|
||||||
Types::new(None, None, false)
|
Types::new(None, None, false, vec![])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a match for the given path against this file type matcher.
|
/// Returns a match for the given path against this file type matcher.
|
||||||
@@ -225,6 +240,11 @@ impl Types {
|
|||||||
Match::None
|
Match::None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the set of current file type definitions.
|
||||||
|
pub fn definitions(&self) -> &[FileTypeDef] {
|
||||||
|
&self.defs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TypesBuilder builds a type matcher from a set of file type definitions and
|
/// TypesBuilder builds a type matcher from a set of file type definitions and
|
||||||
@@ -248,14 +268,11 @@ impl TypesBuilder {
|
|||||||
/// Build the current set of file type definitions *and* selections into
|
/// Build the current set of file type definitions *and* selections into
|
||||||
/// a file type matcher.
|
/// a file type matcher.
|
||||||
pub fn build(&self) -> Result<Types, Error> {
|
pub fn build(&self) -> Result<Types, Error> {
|
||||||
let opts = MatchOptions {
|
|
||||||
require_literal_separator: true, ..MatchOptions::default()
|
|
||||||
};
|
|
||||||
let selected_globs =
|
let selected_globs =
|
||||||
if self.selected.is_empty() {
|
if self.selected.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let mut bset = glob::SetBuilder::new();
|
let mut bset = GlobSetBuilder::new();
|
||||||
for name in &self.selected {
|
for name in &self.selected {
|
||||||
let globs = match self.types.get(name) {
|
let globs = match self.types.get(name) {
|
||||||
Some(globs) => globs,
|
Some(globs) => globs,
|
||||||
@@ -265,16 +282,19 @@ impl TypesBuilder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
for glob in globs {
|
for glob in globs {
|
||||||
try!(bset.add_with(glob, &opts));
|
let pat = try!(
|
||||||
|
GlobBuilder::new(glob)
|
||||||
|
.literal_separator(true).build());
|
||||||
|
bset.add(pat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(try!(bset.build_yesno()))
|
Some(try!(bset.build()))
|
||||||
};
|
};
|
||||||
let negated_globs =
|
let negated_globs =
|
||||||
if self.negated.is_empty() {
|
if self.negated.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let mut bset = glob::SetBuilder::new();
|
let mut bset = GlobSetBuilder::new();
|
||||||
for name in &self.negated {
|
for name in &self.negated {
|
||||||
let globs = match self.types.get(name) {
|
let globs = match self.types.get(name) {
|
||||||
Some(globs) => globs,
|
Some(globs) => globs,
|
||||||
@@ -284,13 +304,20 @@ impl TypesBuilder {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
for glob in globs {
|
for glob in globs {
|
||||||
try!(bset.add_with(glob, &opts));
|
let pat = try!(
|
||||||
|
GlobBuilder::new(glob)
|
||||||
|
.literal_separator(true).build());
|
||||||
|
bset.add(pat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(try!(bset.build_yesno()))
|
Some(try!(bset.build()))
|
||||||
};
|
};
|
||||||
Ok(Types::new(
|
Ok(Types::new(
|
||||||
selected_globs, negated_globs, !self.selected.is_empty()))
|
selected_globs,
|
||||||
|
negated_globs,
|
||||||
|
!self.selected.is_empty(),
|
||||||
|
self.definitions(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the set of current file type definitions.
|
/// Return the set of current file type definitions.
|
||||||
|
447
tests/tests.rs
447
tests/tests.rs
@@ -34,6 +34,33 @@ macro_rules! sherlock {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! clean {
|
||||||
|
($name:ident, $query:expr, $path:expr, $fun:expr) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
let wd = WorkDir::new(stringify!($name));
|
||||||
|
let mut cmd = wd.command();
|
||||||
|
cmd.arg($query).arg($path);
|
||||||
|
$fun(wd, cmd);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(unix: &str) -> String {
|
||||||
|
if cfg!(windows) {
|
||||||
|
unix.replace("/", "\\")
|
||||||
|
} else {
|
||||||
|
unix.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_lines(lines: &str) -> String {
|
||||||
|
let mut lines: Vec<String> =
|
||||||
|
lines.trim().lines().map(|s| s.to_owned()).collect();
|
||||||
|
lines.sort();
|
||||||
|
format!("{}\n", lines.join("\n"))
|
||||||
|
}
|
||||||
|
|
||||||
sherlock!(single_file, |wd: WorkDir, mut cmd| {
|
sherlock!(single_file, |wd: WorkDir, mut cmd| {
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
let expected = "\
|
let expected = "\
|
||||||
@@ -118,7 +145,11 @@ be, to a very large extent, the result of luck. Sherlock Holmes
|
|||||||
foo
|
foo
|
||||||
Sherlock Holmes lives on Baker Street.
|
Sherlock Holmes lives on Baker Street.
|
||||||
";
|
";
|
||||||
assert!(lines == expected1 || lines == expected2);
|
if lines != expected1 {
|
||||||
|
assert_eq!(lines, expected2);
|
||||||
|
} else {
|
||||||
|
assert_eq!(lines, expected1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
sherlock!(inverted, |wd: WorkDir, mut cmd: Command| {
|
sherlock!(inverted, |wd: WorkDir, mut cmd: Command| {
|
||||||
@@ -280,6 +311,20 @@ sherlock!(glob_negate, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
assert_eq!(lines, "file.py:Sherlock\n");
|
assert_eq!(lines, "file.py:Sherlock\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sherlock!(count, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--count");
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "sherlock:2\n";
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
sherlock!(files_with_matches, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--files-with-matches");
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "sherlock\n";
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
sherlock!(after_context, |wd: WorkDir, mut cmd: Command| {
|
sherlock!(after_context, |wd: WorkDir, mut cmd: Command| {
|
||||||
cmd.arg("-A").arg("1");
|
cmd.arg("-A").arg("1");
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
@@ -377,6 +422,11 @@ sherlock!(ignore_git, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
wd.assert_err(&mut cmd);
|
wd.assert_err(&mut cmd);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sherlock!(ignore_generic, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".ignore", "sherlock\n");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
sherlock!(ignore_ripgrep, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
sherlock!(ignore_ripgrep, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
wd.create(".rgignore", "sherlock\n");
|
wd.create(".rgignore", "sherlock\n");
|
||||||
wd.assert_err(&mut cmd);
|
wd.assert_err(&mut cmd);
|
||||||
@@ -492,7 +542,7 @@ sherlock!(symlink_nofollow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
wd.remove("sherlock");
|
wd.remove("sherlock");
|
||||||
wd.create_dir("foo");
|
wd.create_dir("foo");
|
||||||
wd.create_dir("foo/bar");
|
wd.create_dir("foo/bar");
|
||||||
wd.link("foo/baz", "foo/bar/baz");
|
wd.link_dir("foo/baz", "foo/bar/baz");
|
||||||
wd.create_dir("foo/baz");
|
wd.create_dir("foo/baz");
|
||||||
wd.create("foo/baz/sherlock", hay::SHERLOCK);
|
wd.create("foo/baz/sherlock", hay::SHERLOCK);
|
||||||
cmd.current_dir(wd.path().join("foo/bar"));
|
cmd.current_dir(wd.path().join("foo/bar"));
|
||||||
@@ -505,24 +555,16 @@ sherlock!(symlink_follow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
wd.create_dir("foo/bar");
|
wd.create_dir("foo/bar");
|
||||||
wd.create_dir("foo/baz");
|
wd.create_dir("foo/baz");
|
||||||
wd.create("foo/baz/sherlock", hay::SHERLOCK);
|
wd.create("foo/baz/sherlock", hay::SHERLOCK);
|
||||||
wd.link("foo/baz", "foo/bar/baz");
|
wd.link_dir("foo/baz", "foo/bar/baz");
|
||||||
cmd.arg("-L");
|
cmd.arg("-L");
|
||||||
cmd.current_dir(wd.path().join("foo/bar"));
|
cmd.current_dir(wd.path().join("foo/bar"));
|
||||||
|
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
if cfg!(windows) {
|
let expected = "\
|
||||||
let expected = "\
|
|
||||||
baz\\sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
|
|
||||||
baz\\sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
|
|
||||||
";
|
|
||||||
assert_eq!(lines, expected);
|
|
||||||
} else {
|
|
||||||
let expected = "\
|
|
||||||
baz/sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
|
baz/sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
baz/sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
|
baz/sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
";
|
";
|
||||||
assert_eq!(lines, expected);
|
assert_eq!(lines, path(expected));
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
sherlock!(unrestricted1, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
sherlock!(unrestricted1, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
@@ -550,17 +592,6 @@ sherlock!(unrestricted2, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
assert_eq!(lines, expected);
|
assert_eq!(lines, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
sherlock!(unrestricted3, "foo", ".", |wd: WorkDir, mut cmd: Command| {
|
|
||||||
wd.create("file", "foo\x00bar\nfoo\x00baz\n");
|
|
||||||
cmd.arg("-uuu");
|
|
||||||
|
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
|
||||||
assert_eq!(lines, "file:foo\nfile:foo\n");
|
|
||||||
});
|
|
||||||
|
|
||||||
// On Windows, this test uses memory maps, so the NUL bytes don't get replaced.
|
|
||||||
#[cfg(windows)]
|
|
||||||
sherlock!(unrestricted3, "foo", ".", |wd: WorkDir, mut cmd: Command| {
|
sherlock!(unrestricted3, "foo", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
wd.create("file", "foo\x00bar\nfoo\x00baz\n");
|
wd.create("file", "foo\x00bar\nfoo\x00baz\n");
|
||||||
cmd.arg("-uuu");
|
cmd.arg("-uuu");
|
||||||
@@ -574,14 +605,348 @@ sherlock!(vimgrep, "Sherlock|Watson", ".", |wd: WorkDir, mut cmd: Command| {
|
|||||||
|
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
let expected = "\
|
let expected = "\
|
||||||
sherlock:1:15:For the Doctor Watsons of this world, as opposed to the Sherlock
|
sherlock:1:16:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
sherlock:1:56:For the Doctor Watsons of this world, as opposed to the Sherlock
|
sherlock:1:57:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
sherlock:3:48:be, to a very large extent, the result of luck. Sherlock Holmes
|
sherlock:3:49:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
sherlock:5:11:but Doctor Watson has to have it taken out for him and dusted,
|
sherlock:5:12:but Doctor Watson has to have it taken out for him and dusted,
|
||||||
";
|
";
|
||||||
assert_eq!(lines, expected);
|
assert_eq!(lines, expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/16
|
||||||
|
clean!(regression_16, "xyz", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "ghi/");
|
||||||
|
wd.create_dir("ghi");
|
||||||
|
wd.create_dir("def/ghi");
|
||||||
|
wd.create("ghi/toplevel.txt", "xyz");
|
||||||
|
wd.create("def/ghi/subdir.txt", "xyz");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/25
|
||||||
|
clean!(regression_25, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "/llvm/");
|
||||||
|
wd.create_dir("src/llvm");
|
||||||
|
wd.create("src/llvm/foo", "test");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = path("src/llvm/foo:test\n");
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
|
||||||
|
cmd.current_dir(wd.path().join("src"));
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = path("llvm/foo:test\n");
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/30
|
||||||
|
clean!(regression_30, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
if cfg!(windows) {
|
||||||
|
wd.create(".gitignore", "vendor/**\n!vendor\\manifest");
|
||||||
|
} else {
|
||||||
|
wd.create(".gitignore", "vendor/**\n!vendor/manifest");
|
||||||
|
}
|
||||||
|
wd.create_dir("vendor");
|
||||||
|
wd.create("vendor/manifest", "test");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = path("vendor/manifest:test\n");
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/49
|
||||||
|
clean!(regression_49, "xyz", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "foo/bar");
|
||||||
|
wd.create_dir("test/foo/bar");
|
||||||
|
wd.create("test/foo/bar/baz", "test");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/50
|
||||||
|
clean!(regression_50, "xyz", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "XXX/YYY/");
|
||||||
|
wd.create_dir("abc/def/XXX/YYY");
|
||||||
|
wd.create_dir("ghi/XXX/YYY");
|
||||||
|
wd.create("abc/def/XXX/YYY/bar", "test");
|
||||||
|
wd.create("ghi/XXX/YYY/bar", "test");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/65
|
||||||
|
clean!(regression_65, "xyz", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "a/");
|
||||||
|
wd.create_dir("a");
|
||||||
|
wd.create("a/foo", "xyz");
|
||||||
|
wd.create("a/bar", "xyz");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/67
|
||||||
|
clean!(regression_67, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "/*\n!/dir");
|
||||||
|
wd.create_dir("dir");
|
||||||
|
wd.create_dir("foo");
|
||||||
|
wd.create("foo/bar", "test");
|
||||||
|
wd.create("dir/bar", "test");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, path("dir/bar:test\n"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/87
|
||||||
|
clean!(regression_87, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "foo\n**no-vcs**");
|
||||||
|
wd.create("foo", "test");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/90
|
||||||
|
clean!(regression_90, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "!.foo");
|
||||||
|
wd.create(".foo", "test");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, ".foo:test\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/93
|
||||||
|
clean!(regression_93, r"(\d{1,3}\.){3}\d{1,3}", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo", "192.168.1.1");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "foo:192.168.1.1\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/99
|
||||||
|
clean!(regression_99, "test", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo1", "test");
|
||||||
|
wd.create("foo2", "zzz");
|
||||||
|
wd.create("bar", "test");
|
||||||
|
cmd.arg("-j1").arg("--heading");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(sort_lines(&lines), sort_lines("bar\ntest\n\nfoo1\ntest\n"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/105
|
||||||
|
clean!(regression_105_part1, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo", "zztest");
|
||||||
|
cmd.arg("--vimgrep");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "foo:1:3:zztest\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/105
|
||||||
|
clean!(regression_105_part2, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo", "zztest");
|
||||||
|
cmd.arg("--column");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "foo:3:zztest\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/127
|
||||||
|
clean!(regression_127, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
// Set up a directory hierarchy like this:
|
||||||
|
//
|
||||||
|
// .gitignore
|
||||||
|
// foo/
|
||||||
|
// sherlock
|
||||||
|
// watson
|
||||||
|
//
|
||||||
|
// Where `.gitignore` contains `foo/sherlock`.
|
||||||
|
//
|
||||||
|
// ripgrep should ignore 'foo/sherlock' giving us results only from
|
||||||
|
// 'foo/watson' but on Windows ripgrep will include both 'foo/sherlock' and
|
||||||
|
// 'foo/watson' in the search results.
|
||||||
|
wd.create(".gitignore", "foo/sherlock\n");
|
||||||
|
wd.create_dir("foo");
|
||||||
|
wd.create("foo/sherlock", hay::SHERLOCK);
|
||||||
|
wd.create("foo/watson", hay::SHERLOCK);
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = format!("\
|
||||||
|
{path}:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
{path}:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
", path=path("foo/watson"));
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/131
|
||||||
|
//
|
||||||
|
// TODO(burntsushi): Darwin doesn't like this test for some reason.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
clean!(regression_131, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "TopÑapa");
|
||||||
|
wd.create("TopÑapa", "test");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/137
|
||||||
|
//
|
||||||
|
// TODO(burntsushi): Figure out why Windows gives "access denied" errors
|
||||||
|
// when trying to create a file symlink. For now, disable test on Windows.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
sherlock!(regression_137, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.link_file("sherlock", "sym1");
|
||||||
|
wd.link_file("sherlock", "sym2");
|
||||||
|
cmd.arg("sym1");
|
||||||
|
cmd.arg("sym2");
|
||||||
|
cmd.arg("-j1");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "\
|
||||||
|
sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
sym1:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
sym1:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
sym2:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
sym2:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
";
|
||||||
|
assert_eq!(lines, path(expected));
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/156
|
||||||
|
clean!(
|
||||||
|
regression_156,
|
||||||
|
r#"#(?:parse|include)\s*\(\s*(?:"|')[./A-Za-z_-]+(?:"|')"#,
|
||||||
|
"testcase.txt",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
const TESTCASE: &'static str = r#"#parse('widgets/foo_bar_macros.vm')
|
||||||
|
#parse ( 'widgets/mobile/foo_bar_macros.vm' )
|
||||||
|
#parse ("widgets/foobarhiddenformfields.vm")
|
||||||
|
#parse ( "widgets/foo_bar_legal.vm" )
|
||||||
|
#include( 'widgets/foo_bar_tips.vm' )
|
||||||
|
#include('widgets/mobile/foo_bar_macros.vm')
|
||||||
|
#include ("widgets/mobile/foo_bar_resetpw.vm")
|
||||||
|
#parse('widgets/foo-bar-macros.vm')
|
||||||
|
#parse ( 'widgets/mobile/foo-bar-macros.vm' )
|
||||||
|
#parse ("widgets/foo-bar-hiddenformfields.vm")
|
||||||
|
#parse ( "widgets/foo-bar-legal.vm" )
|
||||||
|
#include( 'widgets/foo-bar-tips.vm' )
|
||||||
|
#include('widgets/mobile/foo-bar-macros.vm')
|
||||||
|
#include ("widgets/mobile/foo-bar-resetpw.vm")
|
||||||
|
"#;
|
||||||
|
wd.create("testcase.txt", TESTCASE);
|
||||||
|
cmd.arg("-N");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, TESTCASE);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/20
|
||||||
|
sherlock!(feature_20_no_filename, "Sherlock", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--no-filename");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "\
|
||||||
|
For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
";
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/68
|
||||||
|
clean!(feature_68_no_ignore_vcs, "test", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create(".gitignore", "foo");
|
||||||
|
wd.create(".ignore", "bar");
|
||||||
|
wd.create("foo", "test");
|
||||||
|
wd.create("bar", "test");
|
||||||
|
cmd.arg("--no-ignore-vcs");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "foo:test\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/70
|
||||||
|
sherlock!(feature_70_smart_case, "sherlock", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--smart-case");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "\
|
||||||
|
sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
";
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/89
|
||||||
|
sherlock!(feature_89_files_with_matches, "Sherlock", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--null").arg("--files-with-matches");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "sherlock\x00");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/89
|
||||||
|
sherlock!(feature_89_count, "Sherlock", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--null").arg("--count");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "sherlock\x002\n");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/89
|
||||||
|
sherlock!(feature_89_files, "NADA", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--null").arg("--files");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, "sherlock\x00");
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/89
|
||||||
|
sherlock!(feature_89_match, "Sherlock", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
cmd.arg("--null").arg("-C1");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = "\
|
||||||
|
sherlock\x00For the Doctor Watsons of this world, as opposed to the Sherlock
|
||||||
|
sherlock\x00Holmeses, success in the province of detective work must always
|
||||||
|
sherlock\x00be, to a very large extent, the result of luck. Sherlock Holmes
|
||||||
|
sherlock\x00can extract a clew from a wisp of straw or a flake of cigar ash;
|
||||||
|
";
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/109
|
||||||
|
clean!(feature_109_max_depth, "far", ".", |wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create_dir("one");
|
||||||
|
wd.create("one/pass", "far");
|
||||||
|
wd.create_dir("one/too");
|
||||||
|
wd.create("one/too/many", "far");
|
||||||
|
|
||||||
|
cmd.arg("--maxdepth").arg("2");
|
||||||
|
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
let expected = path("one/pass:far\n");
|
||||||
|
assert_eq!(lines, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/124
|
||||||
|
clean!(feature_109_case_sensitive_part1, "test", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo", "tEsT");
|
||||||
|
cmd.arg("--smart-case").arg("--case-sensitive");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See: https://github.com/BurntSushi/ripgrep/issues/124
|
||||||
|
clean!(feature_109_case_sensitive_part2, "test", ".",
|
||||||
|
|wd: WorkDir, mut cmd: Command| {
|
||||||
|
wd.create("foo", "tEsT");
|
||||||
|
cmd.arg("--ignore-case").arg("--case-sensitive");
|
||||||
|
wd.assert_err(&mut cmd);
|
||||||
|
});
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn binary_nosearch() {
|
fn binary_nosearch() {
|
||||||
let wd = WorkDir::new("binary_nosearch");
|
let wd = WorkDir::new("binary_nosearch");
|
||||||
@@ -619,7 +984,7 @@ fn binary_search_no_mmap() {
|
|||||||
let mut cmd = wd.command();
|
let mut cmd = wd.command();
|
||||||
cmd.arg("-a").arg("--no-mmap").arg("foo").arg("file");
|
cmd.arg("-a").arg("--no-mmap").arg("foo").arg("file");
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
assert_eq!(lines, "foo\nfoo\n");
|
assert_eq!(lines, "foo\x00bar\nfoo\x00baz\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -632,13 +997,23 @@ fn files() {
|
|||||||
let mut cmd = wd.command();
|
let mut cmd = wd.command();
|
||||||
cmd.arg("--files");
|
cmd.arg("--files");
|
||||||
let lines: String = wd.stdout(&mut cmd);
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
if cfg!(windows) {
|
assert!(lines == path("file\ndir/file\n")
|
||||||
assert!(lines == "./dir\\file\n./file\n"
|
|| lines == path("dir/file\nfile\n"));
|
||||||
|| lines == "./file\n./dir\\file\n");
|
}
|
||||||
} else {
|
|
||||||
assert!(lines == "./file\n./dir/file\n"
|
// See: https://github.com/BurntSushi/ripgrep/issues/64
|
||||||
|| lines == "./dir/file\n./file\n");
|
#[test]
|
||||||
}
|
fn regression_64() {
|
||||||
|
let wd = WorkDir::new("regression_64");
|
||||||
|
wd.create_dir("dir");
|
||||||
|
wd.create_dir("foo");
|
||||||
|
wd.create("dir/abc", "");
|
||||||
|
wd.create("foo/abc", "");
|
||||||
|
|
||||||
|
let mut cmd = wd.command();
|
||||||
|
cmd.arg("--files").arg("foo");
|
||||||
|
let lines: String = wd.stdout(&mut cmd);
|
||||||
|
assert_eq!(lines, path("foo/abc\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@@ -83,7 +83,7 @@ impl WorkDir {
|
|||||||
/// Creates a directory symlink to the src with the given target name
|
/// Creates a directory symlink to the src with the given target name
|
||||||
/// in this directory.
|
/// in this directory.
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
pub fn link<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
|
pub fn link_dir<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
let src = self.dir.join(src);
|
let src = self.dir.join(src);
|
||||||
let target = self.dir.join(target);
|
let target = self.dir.join(target);
|
||||||
@@ -91,8 +91,10 @@ impl WorkDir {
|
|||||||
nice_err(&target, symlink(&src, &target));
|
nice_err(&target, symlink(&src, &target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a directory symlink to the src with the given target name
|
||||||
|
/// in this directory.
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub fn link<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
|
pub fn link_dir<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
|
||||||
use std::os::windows::fs::symlink_dir;
|
use std::os::windows::fs::symlink_dir;
|
||||||
let src = self.dir.join(src);
|
let src = self.dir.join(src);
|
||||||
let target = self.dir.join(target);
|
let target = self.dir.join(target);
|
||||||
@@ -100,6 +102,32 @@ impl WorkDir {
|
|||||||
nice_err(&target, symlink_dir(&src, &target));
|
nice_err(&target, symlink_dir(&src, &target));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates a file symlink to the src with the given target name
|
||||||
|
/// in this directory.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
src: S,
|
||||||
|
target: T,
|
||||||
|
) {
|
||||||
|
self.link_dir(src, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a file symlink to the src with the given target name
|
||||||
|
/// in this directory.
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
src: S,
|
||||||
|
target: T,
|
||||||
|
) {
|
||||||
|
use std::os::windows::fs::symlink_file;
|
||||||
|
let src = self.dir.join(src);
|
||||||
|
let target = self.dir.join(target);
|
||||||
|
let _ = fs::remove_file(&target);
|
||||||
|
nice_err(&target, symlink_file(&src, &target));
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs and captures the stdout of the given command.
|
/// Runs and captures the stdout of the given command.
|
||||||
///
|
///
|
||||||
/// If the return type could not be created from a string, then this
|
/// If the return type could not be created from a string, then this
|
||||||
|
Reference in New Issue
Block a user