├── .github └── workflows │ └── build-release.yml ├── .gitignore ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── colored.png └── src ├── context.rs ├── contexts ├── children.rs ├── indentation.rs ├── mod.rs ├── preprocessor.rs └── textual.rs ├── error.rs ├── io.rs ├── main.rs ├── options.rs ├── printer.rs ├── tests.rs └── util.rs /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build: 12 | name: release ${{ matrix.target }} 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | include: 19 | - target: x86_64-pc-windows-gnu 20 | archive: zip 21 | - target: x86_64-unknown-linux-musl 22 | archive: tar.gz 23 | - target: x86_64-apple-darwin 24 | archive: zip 25 | steps: 26 | - uses: actions/checkout@master 27 | - name: Compile and release 28 | uses: rust-build/rust-build.action@latest 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | RUSTTARGET: ${{ matrix.target }} 32 | ARCHIVE_TYPES: ${{ matrix.archive }} 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | os: 7 | - linux 8 | - osx 9 | matrix: 10 | allow_failures: 11 | - rust: nighly 12 | fast_finish: true 13 | cache: cargo 14 | 15 | script: 16 | - cargo build --release --verbose 17 | - cargo test --release --verbose 18 | 19 | before_deploy: 20 | - ln -s target/release/ogrep target/release/ogrep-$TRAVIS_OS_NAME 21 | deploy: 22 | provider: releases 23 | api_key: 24 | secure: p2k43gPRcuQP43gayLvyf+6FOYGhZ7XVAQ/I2XKX5V/rLtcqjXd2fhig4BbDqvsxLBwHqf13LD/s04vJcpmiRSvb5nI1B/WryCcndGokpAy5DVSmfsPxe63OnZnZ/nM9Cnxdoz1772BO76SDPIB8aGte/0SSYHpgAOOm0vYHwSXqnWE7N3ka87FW/ZRJJvlH+DqTPJqFiBEfenyiLojRnjvxboTMAsLYniOlaQB1W+t+Xvvc2XO3AB0x9/zYCyDgtVUg3elkiJyYI4OMu8dlBtlZt9Dc142cwfiYUaziGWCC+rSaSR8s2VSq52G8GXsXXfVeo/Yv/3lYDiSR7Bk7FuN00umbLW7kodGFuW74ZndNTbcgsXL34Zr3wVsKL/RyFUEjZE7fmi7tfsJcq8yK2RhVxqbSdqoDiCt7iA7c9Rgk34JEQ5HJVRNsqD6gDjY+kAiAIYORIumYUqJmumSuQU8nuoaKGqX9gyHrTWcuiu7IiCe2AiR9jb+R1zphH7ww5LNEGMNhwdpDRAy7kBbUxrXgn1lSU61bmTvs1kDXJQiNl0cgyQ7c/i+hlTgPqDSQ/6b/7EpEVMYusZW1IFPBJRP1f1xGmsdsQ/INASLRd7a3dyLulNkD6CWPICy4dk1zlTEOp41J6/TbGWVZChU3SIaOQRDFr3O30JwVqiH8lLc= 25 | file: target/release/ogrep-$TRAVIS_OS_NAME 26 | on: 27 | rust: stable 28 | repo: kriomant/ogrep-rs 29 | tags: true 30 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.6.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d6531d44de723825aa81398a6415283229725a00fa30713812ab9323faa82fc4" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "ansi_term" 16 | version = "0.11.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 19 | dependencies = [ 20 | "winapi", 21 | ] 22 | 23 | [[package]] 24 | name = "atty" 25 | version = "0.2.6" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859" 28 | dependencies = [ 29 | "libc", 30 | "termion", 31 | "winapi", 32 | ] 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "1.0.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" 39 | 40 | [[package]] 41 | name = "clap" 42 | version = "2.33.3" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 45 | dependencies = [ 46 | "ansi_term", 47 | "atty", 48 | "bitflags", 49 | "strsim", 50 | "textwrap", 51 | "unicode-width", 52 | "vec_map", 53 | ] 54 | 55 | [[package]] 56 | name = "difference" 57 | version = "2.0.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 60 | 61 | [[package]] 62 | name = "either" 63 | version = "1.4.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "740178ddf48b1a9e878e6d6509a1442a2d42fd2928aae8e7a6f8a36fb01981b3" 66 | 67 | [[package]] 68 | name = "itertools" 69 | version = "0.7.7" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "23d53b4c7394338044c3b9c8c5b2caaf7b40ae049ecd321578ebdc2e13738cd1" 72 | dependencies = [ 73 | "either", 74 | ] 75 | 76 | [[package]] 77 | name = "lazy_static" 78 | version = "1.0.0" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "c8f31047daa365f19be14b47c29df4f7c3b581832407daabe6ae77397619237d" 81 | 82 | [[package]] 83 | name = "libc" 84 | version = "0.2.39" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "f54263ad99207254cf58b5f701ecb432c717445ea2ee8af387334bdd1a03fdff" 87 | 88 | [[package]] 89 | name = "memchr" 90 | version = "2.0.1" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "796fba70e76612589ed2ce7f45282f5af869e0fdd7cc6199fa1aa1f1d591ba9d" 93 | dependencies = [ 94 | "libc", 95 | ] 96 | 97 | [[package]] 98 | name = "ogrep" 99 | version = "0.6.0" 100 | dependencies = [ 101 | "clap", 102 | "difference", 103 | "itertools", 104 | "regex", 105 | "termion", 106 | ] 107 | 108 | [[package]] 109 | name = "redox_syscall" 110 | version = "0.1.37" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" 113 | 114 | [[package]] 115 | name = "redox_termios" 116 | version = "0.1.1" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 119 | dependencies = [ 120 | "redox_syscall", 121 | ] 122 | 123 | [[package]] 124 | name = "regex" 125 | version = "0.2.6" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "5be5347bde0c48cfd8c3fdc0766cdfe9d8a755ef84d620d6794c778c91de8b2b" 128 | dependencies = [ 129 | "aho-corasick", 130 | "memchr", 131 | "regex-syntax", 132 | "thread_local", 133 | "utf8-ranges", 134 | ] 135 | 136 | [[package]] 137 | name = "regex-syntax" 138 | version = "0.4.2" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "8e931c58b93d86f080c734bfd2bce7dd0079ae2331235818133c8be7f422e20e" 141 | 142 | [[package]] 143 | name = "strsim" 144 | version = "0.8.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 147 | 148 | [[package]] 149 | name = "termion" 150 | version = "1.5.1" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 153 | dependencies = [ 154 | "libc", 155 | "redox_syscall", 156 | "redox_termios", 157 | ] 158 | 159 | [[package]] 160 | name = "textwrap" 161 | version = "0.11.0" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 164 | dependencies = [ 165 | "unicode-width", 166 | ] 167 | 168 | [[package]] 169 | name = "thread_local" 170 | version = "0.3.5" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "279ef31c19ededf577bfd12dfae728040a21f635b06a24cd670ff510edd38963" 173 | dependencies = [ 174 | "lazy_static", 175 | "unreachable", 176 | ] 177 | 178 | [[package]] 179 | name = "unicode-width" 180 | version = "0.1.4" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" 183 | 184 | [[package]] 185 | name = "unreachable" 186 | version = "1.0.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" 189 | dependencies = [ 190 | "void", 191 | ] 192 | 193 | [[package]] 194 | name = "utf8-ranges" 195 | version = "1.0.0" 196 | source = "registry+https://github.com/rust-lang/crates.io-index" 197 | checksum = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122" 198 | 199 | [[package]] 200 | name = "vec_map" 201 | version = "0.8.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" 204 | 205 | [[package]] 206 | name = "void" 207 | version = "1.0.2" 208 | source = "registry+https://github.com/rust-lang/crates.io-index" 209 | checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 210 | 211 | [[package]] 212 | name = "winapi" 213 | version = "0.3.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" 216 | dependencies = [ 217 | "winapi-i686-pc-windows-gnu", 218 | "winapi-x86_64-pc-windows-gnu", 219 | ] 220 | 221 | [[package]] 222 | name = "winapi-i686-pc-windows-gnu" 223 | version = "0.4.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 226 | 227 | [[package]] 228 | name = "winapi-x86_64-pc-windows-gnu" 229 | version = "0.4.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 232 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Mikhail Trishchenkov "] 3 | name = "ogrep" 4 | version = "0.6.0" 5 | description = "Tool for searching in indentation-structured texts." 6 | homepage = "https://github.com/kriomant/ogrep-rs" 7 | repository = "https://github.com/kriomant/ogrep-rs.git" 8 | readme = "README.md" 9 | keywords = ["grep", "search", "regex", "outline", "indentation"] 10 | categories = ["command-line-utilities", "text-processing"] 11 | license = "MIT" 12 | 13 | [dependencies] 14 | clap = "2.33.3" 15 | itertools = "0.7.7" 16 | regex = "0.2.6" 17 | termion = "1.5.1" 18 | 19 | [dev-dependencies] 20 | difference = "2.0.0" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mikhail Trishchenkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Outline grep 2 | 3 | Featureful tool for searching in indentation-structured text files. 4 | 5 | Inspired by [ogrep by Matt Brubeck](https://github.com/mbrubeck/outline-grep). That ogrep is compact and beautiful, but not featureful. 6 | 7 | See also [ogrep](https://github.com/kriomant/ogrep) — port of this tool written in Python (to be truly, it was first). 8 | 9 | ## Brief 10 | 11 | `ogrep` is much like `grep`, both can search for matches and display their context. But context in `grep` is “N lines before/after match”, and in `ogrep` it is “lines above matched one with lower indentation”. 12 | 13 | Let me explain. I use this tool mostly when working with GN build files, so I'll use [some large BUILD.gn](https://cs.chromium.org/codesearch/f/chromium/src/net/BUILD.gn?cl=a94640abec90972d53ed35816363df2e8eabef63) file as an example. Usual task is to search for source file name and understand which target includes this file and under which conditions. 14 | 15 | Let's find mentions of “arena.cc” file: 16 | 17 | ``` 18 | # grep arena.cc BUILD.gn 19 | "base/arena.cc", 20 | ``` 21 | 22 | Ok, now we now that our file is here, but don't know target. Let's ask for some context: 23 | 24 | ``` 25 | # grep -C2 arena.cc BUILD.gn 26 | "base/address_tracker_linux.cc", 27 | "base/address_tracker_linux.h", 28 | "base/arena.cc", 29 | "base/arena.h", 30 | "base/backoff_entry.cc", 31 | ``` 32 | 33 | Nope, not that useful. Let's try `ogrep`: 34 | 35 | ``` 36 | ogrep arena.cc BUILD.gn 37 | 102: component("net") { 38 | 385: if (!is_nacl) { 39 | 386: sources += [ 40 | 409: "base/arena.cc", 41 | ``` 42 | 43 | Now that's useful! We immediately know that file in included into “net“ target under “!is_nacl” condition. 44 | 45 | It is even better, because `ogrep` can use colors, here is a picture: 46 | 47 | ![](colored.png) 48 | 49 | ## Installation 50 | 51 | ### Using Cargo (any platform) 52 | 53 | Install [Rust and Cargo](https://www.rust-lang.org/install.html), if you haven't yet, then 54 | 55 | ```sh 56 | cargo install ogrep 57 | ``` 58 | 59 | ### MacOS 60 | 61 | Install [Homebrew](https://brew.sh), then 62 | 63 | ```sh 64 | brew install kriomant/ogrep-rs/ogrep-rs 65 | ``` 66 | 67 | ### Other platforms 68 | 69 | Sorry, not yet, but I'm working on it. Use Cargo for now. 70 | 71 | ## Options 72 | 73 | There are plently of available options, run with `--help` to list them. 74 | 75 | Tool is useful not only for strict indentation-based files (like Python source) or GN build files, but for wide range of text files, because even not-indentation based ones are usually formatted for convenience. 76 | 77 | There are even some C-related hacks built-in. 78 | 79 | Here is brief feature list: 80 | 81 | * Pattern is fixed text by default, but you may use arbitrary regular expression with `-e`. 82 | 83 | * Usual `-w` (match whole words) and `-i` (case-insensitive search) are available. 84 | 85 | * Tool preserve some blank lines between matches, because it helps to visually separate groups of related matches, you can turn it off with `--no-breaks`. 86 | 87 | * Sometimes it is useful to see whether there were other lines between matched ones. Use `--ellipsis` for that. 88 | 89 | * If you integrate `otool` with external tools, `--print-filename` options may be useful, it tells to print filename if any match found. 90 | 91 | * By default “if-else” branches are treated specially: if-branches are preserved so you know conditions even when match is found in “else” branch: 92 | 93 | * Traditional context (displaying N leading and/or trailing lines around 94 | matched one) is also supported with `--context/-C`, `--before-context/-B` 95 | and `--after-context/-A` options. 96 | 97 | ``` 98 | # ./ogrep filename_util_icu BUILD.gn 99 |  102: component("net") { 100 | 2106: if (!is_nacl) { 101 | 2210: if (use_platform_icu_alternatives) { 102 | 2222: } else { 103 | 2228: sources += [ 104 | 2229: "base/filename_util_icu.cc", 105 | ``` 106 | 107 | This can be turned off with `--no-smart-branches`. 108 | 109 | * Preprocessor instructions in C files are often written without any indentation (or indentation is inserted after “#”). So tool ignores preprocessor instructions by default until `--no-ignore-preprocessor` is given. 110 | 111 | More intelligent handling of preprocessor instructions (parallel context) is planned. 112 | 113 | ## Searching in directory 114 | 115 | ### Integration with external tools 116 | 117 | `otool` in intended to search in single file only. And it is not so fast to be used for searching through many files. But you can integrate it with other search tools like this: 118 | 119 | ``` 120 | grep -l cache_used -r . --include='*.cc' | xargs -n1 ogrep --print-filename cache_used 121 | ``` 122 | 123 | ### Builtin `git grep` support 124 | 125 | `ogrep` has builtin integration with `git grep`: when `-g` option is given, second argument is passed to `git grep` as path specification. All relevant options (`-w`, `-i`, etc.) are also passed to `git grep` automatically, `--print-filename` is forced. 126 | 127 | ``` 128 | ogrep -g cache_used '*.cc' 129 | ``` 130 | -------------------------------------------------------------------------------- /colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kriomant/ogrep-rs/9425f115448b9fa1a22b656a3fbdb8fc6c8b2764/colored.png -------------------------------------------------------------------------------- /src/context.rs: -------------------------------------------------------------------------------- 1 | use printer::Printer; 2 | 3 | #[derive(Clone)] 4 | pub struct Line { 5 | pub number: usize, 6 | pub text: String, 7 | } 8 | 9 | pub enum Action { 10 | Continue, 11 | Skip, 12 | } 13 | 14 | pub trait Context { 15 | /// Handle line before it is checked for matches. Context must update its state based 16 | /// on new line, but do not add this line into context yet. If line matches, `dump` will 17 | /// be called to get actual context. Otherwise, `post_line` will be called to put line into 18 | /// context. 19 | fn pre_line(&mut self, line: &Line, indentation: Option, printer: &mut Printer) -> Action; 20 | 21 | /// Put non-matching line into context, if needed. 22 | fn post_line(&mut self, line: &Line, indentation: Option); 23 | 24 | /// Returns current context lines. 25 | fn dump<'a>(&'a mut self) -> Box + 'a>; 26 | 27 | /// Clears context. Called after `dump`. 28 | fn clear(&mut self); 29 | 30 | /// Handle end of source text, flush all remaining lines, if needed. 31 | fn end(&mut self, printer: &mut Printer); 32 | } 33 | -------------------------------------------------------------------------------- /src/contexts/children.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use context::{Context, Line, Action}; 4 | use options::Options; 5 | use printer::Printer; 6 | 7 | pub struct ChildrenContext<'p> { 8 | filepath: Option<&'p Path>, 9 | 10 | last_line_indentation: usize, 11 | match_indentation: Option, 12 | context: Vec, 13 | } 14 | 15 | impl<'p> ChildrenContext<'p> { 16 | pub fn new<'o>(_options: &'o Options, filepath: Option<&'p Path>) -> Self { 17 | ChildrenContext { 18 | filepath: filepath, 19 | last_line_indentation: 0, 20 | match_indentation: None, 21 | context: Vec::new(), 22 | } 23 | } 24 | } 25 | 26 | impl<'p> Context for ChildrenContext<'p> { 27 | fn pre_line(&mut self, _line: &Line, indentation: Option, _printer: &mut Printer) -> Action { 28 | if let Some(indentation) = indentation { 29 | self.last_line_indentation = indentation; 30 | } 31 | Action::Continue 32 | } 33 | 34 | fn post_line(&mut self, line: &Line, indentation: Option) { 35 | let match_indentation = match self.match_indentation { 36 | Some(ind) => ind, 37 | None => return, 38 | }; 39 | if indentation.map(|i| i > match_indentation).unwrap_or(true) { 40 | self.context.push(line.clone()); 41 | } else { 42 | self.match_indentation = None; 43 | } 44 | } 45 | 46 | fn dump<'a>(&'a mut self) -> Box + 'a> { 47 | Box::new(self.context.iter()) 48 | } 49 | 50 | fn clear(&mut self) { 51 | if self.match_indentation.is_none() { 52 | self.match_indentation = Some(self.last_line_indentation); 53 | } 54 | self.context.clear(); 55 | } 56 | 57 | fn end(&mut self, printer: &mut Printer) { 58 | for line in &self.context { 59 | printer.print_context(self.filepath, line.number, &line.text); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/contexts/indentation.rs: -------------------------------------------------------------------------------- 1 | use options::Options; 2 | use context::{Context, Line, Action}; 3 | use printer::Printer; 4 | use util::starts_with_word; 5 | 6 | // This prefixes are used when "smart branches" feature 7 | // is turned on. When line starts with given prefix, then retain 8 | // lines with same indentation starting with given prefixes in context. 9 | const SMART_BRANCH_PREFIXES: &[(&str, &[&str])] = &[ 10 | ("} else ", &["if", "} else if"]), 11 | ("else:", &["if", "else if"]), 12 | ("case", &["switch"]), 13 | ]; 14 | 15 | struct ContextEntry { 16 | line: Line, 17 | indentation: usize, 18 | } 19 | 20 | /// Indentation context — the heart of ogrep. 21 | pub struct IndentationContext<'o> { 22 | options: &'o Options, 23 | 24 | /// Indentation context: last-processed line and it's parents (in terms of indentaion). 25 | /// 26 | /// ```text 27 | /// a ← context[0], indentation=0 28 | /// b 29 | /// c 30 | /// d ← context[1], indentation=1 31 | /// e ← context[2], indentation=2 32 | /// ``` 33 | /// 34 | /// Invariant: lines in context are sorted by indentation (ascending). 35 | /// Formal: 36 | /// ```ignore 37 | /// context.iter().tuple_windows().all(|(a,b)| { 38 | /// if options.smart_branches { 39 | /// a.indentation <= b.indentation 40 | /// } else { 41 | /// a.indentation < b.indentation 42 | /// } 43 | /// }) 44 | /// ``` 45 | context: Vec, 46 | } 47 | impl<'o> IndentationContext<'o> { 48 | pub fn new(options: &'o Options) -> Self { 49 | IndentationContext { 50 | options: options, 51 | context: Vec::new(), 52 | } 53 | } 54 | } 55 | impl<'o> Context for IndentationContext<'o> { 56 | /// Handle next line. 57 | fn pre_line(&mut self, line: &Line, indentation: Option, _printer: &mut Printer) -> Action { 58 | let indentation = match indentation { 59 | Some(i) => i, 60 | // Empty lines shouldn't reset indentation, skip them 61 | None => return Action::Continue, 62 | }; 63 | 64 | // Drop lines with indentation less than current line's one. 65 | // a ↑ 66 | // b | context ← drop this line 67 | // c ↓ ← and this 68 | // ------ 69 | // d ← current line 70 | // 71 | // `top` is an index of last context line to leave, or None 72 | // if whole context must be dropped. 73 | let top = self.context.iter().rposition(|e: &ContextEntry| { 74 | // Upper scopes are always preserved. 75 | if e.indentation < indentation { return true; } 76 | if e.indentation > indentation { return false; } 77 | 78 | // Indentation is the same as of current line, push it out 79 | // when `smart_branches` option is off. 80 | if !self.options.smart_branches { return false; } 81 | 82 | // When `smart_branches` option is on, things are little harder. 83 | // We still pushes lines with greater or equal indentation out of 84 | // context, but we want to leave e.g. line with 'if' corresponding to 85 | // 'else' in current line. 86 | let stripped_line = &line.text[indentation..]; 87 | let stripped_context_line = &e.line.text[e.indentation..]; 88 | for &(prefix, context_prefixes) in SMART_BRANCH_PREFIXES { 89 | if starts_with_word(stripped_line, prefix) { 90 | return context_prefixes.iter().any(|p| starts_with_word(stripped_context_line, p)); 91 | } 92 | } 93 | 94 | // Current line is not part of branch statement, push it out. 95 | return false; 96 | }); 97 | 98 | // Drop all context lines after one with `top` index. 99 | self.context.truncate(top.map(|t| t+1).unwrap_or(0)); 100 | 101 | Action::Continue 102 | } 103 | 104 | fn post_line(&mut self, line: &Line, indentation: Option) { 105 | if let Some(indentation) = indentation { 106 | // We already pushed out all lines with greater or equal indentation 107 | // out of context in `pre_line`, … 108 | assert!(match self.context.last() { 109 | Some(last) => 110 | if self.options.smart_branches { 111 | last.indentation <= indentation 112 | } else { 113 | last.indentation < indentation 114 | } 115 | None => true 116 | }); 117 | 118 | // … so just put new line into context. 119 | self.context.push(ContextEntry { line: line.clone(), indentation }); 120 | } 121 | } 122 | 123 | /// Returns current context lines. 124 | fn dump<'a>(&'a mut self) -> Box + 'a> { 125 | Box::new(self.context.iter().map(|e| &e.line)) 126 | } 127 | 128 | fn clear(&mut self) { 129 | self.context.clear(); 130 | } 131 | 132 | fn end(&mut self, _printer: &mut Printer) {} 133 | } 134 | -------------------------------------------------------------------------------- /src/contexts/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod indentation; 2 | pub mod preprocessor; 3 | pub mod textual; 4 | pub mod children; 5 | -------------------------------------------------------------------------------- /src/contexts/preprocessor.rs: -------------------------------------------------------------------------------- 1 | use context::{Context, Line, Action}; 2 | use options::{Options, Preprocessor}; 3 | use printer::Printer; 4 | use regex::Regex; 5 | 6 | struct Entry { 7 | line: Line, 8 | level: usize, 9 | } 10 | 11 | enum PreprocessorKind { If, Else, Endif, Other } 12 | 13 | pub struct PreprocessorContext<'o> { 14 | options: &'o Options, 15 | level: usize, 16 | context: Vec, 17 | 18 | if_regex: Regex, 19 | else_regex: Regex, 20 | endif_regex: Regex, 21 | other_regex: Regex, 22 | } 23 | impl<'o> PreprocessorContext<'o> { 24 | pub fn new(options: &'o Options) -> Self { 25 | PreprocessorContext { 26 | options: options, 27 | level: 0usize, 28 | context: Vec::new(), 29 | 30 | if_regex: Regex::new(r"^\s*(?:#|\{%-?)\s*if\b").unwrap(), 31 | else_regex: Regex::new(r"^\s*(?:#|\{%-?)\s*else\b").unwrap(), 32 | endif_regex: Regex::new(r"^\s*(?:#|\{%-?)\s*endif\b").unwrap(), 33 | other_regex: Regex::new(r"^\s*(?:#|\{%-?)").unwrap(), 34 | } 35 | } 36 | 37 | fn preprocessor_instruction_kind(&self, s: &str) -> Option { 38 | if self.if_regex.is_match(s) { return Some(PreprocessorKind::If) } 39 | if self.else_regex.is_match(s) { return Some(PreprocessorKind::Else) } 40 | if self.endif_regex.is_match(s) { return Some(PreprocessorKind::Endif) } 41 | if self.other_regex.is_match(s) { return Some(PreprocessorKind::Other) } 42 | return None; 43 | } 44 | } 45 | 46 | impl<'o> Context for PreprocessorContext<'o> { 47 | /// Handle next line. 48 | fn pre_line(&mut self, line: &Line, indentation: Option, _printer: &mut Printer) -> Action { 49 | let indentation = match indentation { 50 | Some(i) => i, 51 | None => return Action::Continue, 52 | }; 53 | 54 | // Ignore lines looking like C preprocessor instruction, because they 55 | // are often written without indentation and this breaks context. 56 | match self.options.preprocessor { 57 | Preprocessor::Preserve => Action::Continue, // Do nothing, handle line as usual 58 | Preprocessor::Ignore => 59 | if self.preprocessor_instruction_kind(&line.text[indentation..]).is_some() { 60 | Action::Skip 61 | } else { 62 | Action::Continue 63 | }, 64 | Preprocessor::Context => 65 | match self.preprocessor_instruction_kind(&line.text[indentation..]) { 66 | None => Action::Continue, 67 | Some(PreprocessorKind::If) => { 68 | self.level += 1; 69 | Action::Skip 70 | }, 71 | Some(PreprocessorKind::Else) => { 72 | Action::Skip 73 | }, 74 | Some(PreprocessorKind::Endif) => { 75 | let top = self.context.iter().rposition(|e: &Entry| { 76 | e.level < self.level 77 | }); 78 | self.context.truncate(top.map(|t| t+1).unwrap_or(0)); 79 | self.level -= 1; 80 | Action::Skip 81 | }, 82 | Some(PreprocessorKind::Other) => Action::Skip 83 | } 84 | } 85 | } 86 | 87 | fn post_line(&mut self, line: &Line, indentation: Option) { 88 | let indentation = match indentation { 89 | Some(i) => i, 90 | None => return, 91 | }; 92 | 93 | // Ignore lines looking like C preprocessor instruction, because they 94 | // are often written without indentation and this breaks context. 95 | match self.options.preprocessor { 96 | Preprocessor::Preserve => (), 97 | Preprocessor::Ignore => (), 98 | Preprocessor::Context => 99 | match self.preprocessor_instruction_kind(&line.text[indentation..]) { 100 | None => (), 101 | Some(PreprocessorKind::If) => { 102 | self.context.push(Entry { line: line.clone(), level: self.level }); 103 | }, 104 | Some(PreprocessorKind::Else) => { 105 | self.context.push(Entry { line: line.clone(), level: self.level }); 106 | }, 107 | Some(PreprocessorKind::Endif) => (), 108 | Some(PreprocessorKind::Other) => (), 109 | } 110 | } 111 | } 112 | 113 | /// Returns current context lines. 114 | fn dump<'a>(&'a mut self) -> Box + 'a> { 115 | Box::new(self.context.iter().map(|e| &e.line)) 116 | } 117 | 118 | fn clear(&mut self) { 119 | self.context.clear(); 120 | } 121 | 122 | /// Handle end of source text. 123 | fn end(&mut self, _printer: &mut Printer) {} 124 | } 125 | -------------------------------------------------------------------------------- /src/contexts/textual.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::path::Path; 3 | 4 | use context::{Context, Line, Action}; 5 | use options::Options; 6 | use printer::Printer; 7 | 8 | struct Entry { 9 | line: Line, 10 | print_on_discard: bool, 11 | } 12 | 13 | /// Maintains textual context — several lines before and after 14 | /// matched line. 15 | pub struct TextualContext<'p, 'o> { 16 | filepath: Option<&'p Path>, 17 | options: &'o Options, 18 | context: std::collections::VecDeque, 19 | 20 | // How many trailing lines after match left to print. 21 | trailing_lines_left: usize, 22 | } 23 | 24 | impl<'p, 'o> TextualContext<'p, 'o> { 25 | pub fn new(options: &'o Options, filepath: Option<&'p Path>) -> Self { 26 | TextualContext { 27 | filepath: filepath, 28 | options: options, 29 | context: std::collections::VecDeque::with_capacity( 30 | options.context_lines_before + options.context_lines_after), 31 | trailing_lines_left: 0, 32 | } 33 | } 34 | } 35 | 36 | impl<'p, 'o> Context for TextualContext<'p, 'o> { 37 | fn pre_line(&mut self, line: &Line, _indentation: Option, printer: &mut Printer) -> Action { 38 | while !self.context.is_empty() && 39 | self.context[0].line.number < 40 | line.number - self.options.context_lines_before { 41 | let entry = self.context.pop_front().unwrap(); 42 | if entry.print_on_discard { 43 | printer.print_context(self.filepath, entry.line.number, &entry.line.text); 44 | } 45 | } 46 | Action::Continue 47 | } 48 | 49 | fn post_line(&mut self, line: &Line, _indentation: Option) { 50 | self.context.push_back( 51 | Entry { line: line.clone(), print_on_discard: self.trailing_lines_left > 0 }); 52 | if self.trailing_lines_left > 0 { self.trailing_lines_left -= 1; } 53 | } 54 | 55 | fn dump<'a>(&'a mut self) -> Box + 'a> { 56 | Box::new(self.context.iter().map(|e| &e.line)) 57 | } 58 | 59 | fn clear(&mut self) { 60 | // Start counting trailing lines after match. 61 | self.trailing_lines_left = self.options.context_lines_after; 62 | } 63 | 64 | fn end(&mut self, printer: &mut Printer) { 65 | while let Some(&Entry { print_on_discard: true, ..}) = self.context.front() { 66 | let entry = self.context.pop_front().unwrap(); 67 | printer.print_context(self.filepath, entry.line.number, &entry.line.text); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use clap; 3 | 4 | #[derive(Debug)] 5 | pub enum OgrepError { 6 | ClapError(clap::Error), 7 | GitGrepFailed, 8 | InvalidOgrepOptions, 9 | } 10 | impl std::error::Error for OgrepError { 11 | /*fn description(&self) -> &str { 12 | match *self { 13 | OgrepError::ClapError(ref e) => e.to_string(), 14 | OgrepError::GitGrepFailed => "git grep failed", 15 | OgrepError::InvalidOgrepOptions => "OGREP_OPTIONS environment variable contains invalid UTF-8", 16 | } 17 | }*/ 18 | } 19 | impl std::fmt::Display for OgrepError { 20 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 21 | match *self { 22 | OgrepError::ClapError(ref e) => write!(f, "{}", e), 23 | OgrepError::GitGrepFailed => write!(f, "git grep failed"), 24 | OgrepError::InvalidOgrepOptions => write!(f, "OGREP_OPTIONS environment variable contains invalid UTF-8"), 25 | } 26 | } 27 | } 28 | impl From for OgrepError { 29 | fn from(e: clap::Error) -> OgrepError { 30 | OgrepError::ClapError(e) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/io.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | 3 | pub enum Output { 4 | Pager(std::process::Child), 5 | Stdout(std::io::Stdout), 6 | } 7 | pub enum OutputLock<'a> { 8 | Pager(&'a mut std::process::ChildStdin), 9 | Stdout(std::io::StdoutLock<'a>), 10 | } 11 | impl Output { 12 | pub fn lock(&mut self) -> OutputLock { 13 | match *self { 14 | Output::Pager(ref mut process) => OutputLock::Pager(process.stdin.as_mut().unwrap()), 15 | Output::Stdout(ref mut stdout) => OutputLock::Stdout(stdout.lock()), 16 | } 17 | } 18 | 19 | pub fn close(mut self) -> Result<(), Box> { 20 | self.close_impl() 21 | } 22 | 23 | pub fn close_impl(&mut self) -> Result<(), Box> { 24 | match self { 25 | &mut Output::Pager(ref mut process) => { process.wait()?; Ok(()) }, 26 | &mut Output::Stdout(_) => Ok(()), 27 | } 28 | } 29 | } 30 | impl Drop for Output { 31 | fn drop(&mut self) { 32 | let _ = self.close_impl(); 33 | } 34 | } 35 | impl<'a> OutputLock<'a> { 36 | pub fn as_write(&mut self) -> &mut dyn std::io::Write { 37 | match self { 38 | &mut OutputLock::Pager(ref mut stdin) => stdin, 39 | &mut OutputLock::Stdout(ref mut lock) => lock, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] extern crate clap; 2 | extern crate regex; 3 | extern crate itertools; 4 | extern crate termion; 5 | 6 | mod error; 7 | mod options; 8 | mod printer; 9 | mod io; 10 | mod context; 11 | mod contexts; 12 | mod util; 13 | 14 | #[cfg(test)] #[macro_use(assert_diff)] extern crate difference; 15 | #[cfg(test)] mod tests; 16 | 17 | use std::ffi::{OsStr, OsString}; 18 | use std::borrow::Cow; 19 | use regex::{Regex, RegexBuilder}; 20 | 21 | use std::io::BufRead; 22 | use std::io::Write as IoWrite; 23 | use itertools::Itertools; 24 | 25 | use error::OgrepError; 26 | use options::{InputSpec, ColorSchemeSpec, Options, UseColors, parse_arguments}; 27 | use printer::{AppearanceOptions, ColorScheme, Printer}; 28 | use io::Output; 29 | use context::{Context, Line, Action}; 30 | use contexts::indentation::IndentationContext; 31 | use contexts::preprocessor::PreprocessorContext; 32 | use contexts::textual::TextualContext; 33 | use contexts::children::ChildrenContext; 34 | 35 | const LESS_ARGS: &[&str] = &["--quit-if-one-screen", "--RAW-CONTROL-CHARS", 36 | "--quit-on-intr", "--no-init"]; 37 | 38 | fn calculate_indentation(s: &str) -> Option { 39 | s.find(|c: char| !c.is_whitespace()) 40 | } 41 | 42 | fn process_input(input: &mut dyn BufRead, 43 | pattern: &Regex, 44 | options: &Options, 45 | filepath: Option<&std::path::Path>, 46 | printer: &mut Printer) -> std::io::Result { 47 | // Context of current line. Last context item contains closest line above current 48 | // whose indentation is lower than one of a current line. One before last 49 | // item contains closest line above last context line with lower indentation and 50 | // so on. Once line is printed, it is removed from context. 51 | // Context may contain lines with identical identation due to smart if-else branches 52 | // handling. 53 | let mut indentation_context = IndentationContext::new(&options); 54 | 55 | // Secondary stack for preprocessor instructions. 56 | let mut preprocessor_context = PreprocessorContext::new(&options); 57 | 58 | // Context as it is understood by usual 'grep' - fixed number of 59 | // lines before and after matched one. 60 | let mut textual_context = TextualContext::new(&options, filepath); 61 | 62 | // Children context prints all children of matched lines. 63 | let mut children_context = ChildrenContext::new(&options, filepath); 64 | 65 | let mut contexts = vec![ 66 | &mut textual_context as &mut dyn Context, 67 | &mut preprocessor_context, 68 | &mut indentation_context, 69 | ]; 70 | 71 | if options.children { 72 | contexts.push(&mut children_context); 73 | } 74 | 75 | // Whether at least one match was already found. 76 | let mut match_found = false; 77 | 78 | // Whether empty line was met since last match. 79 | let mut was_empty_line = false; 80 | 81 | for (line_number, line) in input.lines().enumerate().map(|(n, l)| (n+1, l)) { 82 | let line = line?; 83 | 84 | let indentation = calculate_indentation(&line); 85 | if indentation.is_none() { 86 | was_empty_line = true; 87 | } 88 | 89 | // Number of contexts queried before one of them returned Skip (including that one). 90 | // Only those contexts must then be used for dumping/adjusting/clearing during handling 91 | // current line. 92 | let mut n = 0; 93 | for context in contexts.iter_mut() { 94 | n += 1; 95 | match context.pre_line(&Line { text: line.clone(), number: line_number }, 96 | indentation, printer) { 97 | Action::Skip => break, 98 | Action::Continue => (), 99 | } 100 | } 101 | let contexts = &mut contexts[..n]; 102 | 103 | let matched = { 104 | let mut matches = pattern.find_iter(&line).peekable(); 105 | if matches.peek().is_some() { 106 | // `match_found` is checked to avoid extra line break before first match. 107 | if !match_found { 108 | if let Some(ref path) = filepath { 109 | printer.print_heading_filename(path) 110 | } 111 | } 112 | if was_empty_line && match_found { 113 | printer.print_break(); 114 | } 115 | 116 | { 117 | // Merge all contexts. 118 | let combined_context = contexts.iter_mut() 119 | .map(|c| c.dump()) 120 | .kmerge_by(|first, second| first.number < second.number) 121 | .coalesce(|first, second| { 122 | if first.number == second.number { 123 | Ok(first) 124 | } else { 125 | Err((first, second)) 126 | } 127 | }) 128 | .enumerate(); 129 | 130 | for (_, line) in combined_context { 131 | printer.print_context(filepath, line.number, &line.text); 132 | } 133 | 134 | printer.print_match(filepath, line_number, &line, matches); 135 | } 136 | 137 | for context in contexts.iter_mut() { 138 | context.clear(); 139 | } 140 | was_empty_line = false; 141 | match_found = true; 142 | 143 | true 144 | } else { 145 | false 146 | } 147 | }; 148 | 149 | if !matched { 150 | for context in contexts { 151 | context.post_line(&Line { number: line_number, text: line.clone() }, 152 | indentation); 153 | } 154 | } 155 | } 156 | 157 | for context in &mut contexts { 158 | context.end(printer); 159 | } 160 | 161 | Ok(match_found) 162 | } 163 | 164 | fn real_main() -> std::result::Result> { 165 | // Read default options from OGREP_OPTIONS environment variable. 166 | let env_var = std::env::var("OGREP_OPTIONS"); 167 | let env_var_ref = match env_var { 168 | Ok(ref opts) => opts.as_str(), 169 | Err(std::env::VarError::NotPresent) => "", 170 | Err(std::env::VarError::NotUnicode(_)) => 171 | return Err(Box::new(OgrepError::InvalidOgrepOptions)), 172 | }; 173 | let env_args = env_var_ref 174 | .split_whitespace() 175 | .map(|b| OsString::from(b)); 176 | let cmdline_args = std::env::args_os(); 177 | let args = env_args.chain(cmdline_args.skip(1)); 178 | let (input_spec, options) = parse_arguments(args)?; 179 | 180 | let appearance = AppearanceOptions { 181 | color_scheme: { 182 | use termion::style::{Faint, NoFaint, Bold, Underline, NoUnderline, Reset as ResetStyle}; 183 | use termion::color::{Fg, Blue, Red, Reset}; 184 | let use_colors = match options.use_colors { 185 | UseColors::Always => true, 186 | UseColors::Never => false, 187 | UseColors::Auto => termion::is_tty(&std::io::stdout()), 188 | }; 189 | if use_colors { 190 | match options.color_scheme { 191 | ColorSchemeSpec::Grey => ColorScheme { 192 | filename: (format!("{}", Underline), format!("{}", NoUnderline)), 193 | // I wish to use `NoBold` here, but it doesn't work, at least on 194 | // Mac with iTerm2. So use `Reset`. 195 | matched_part: (format!("{}", Bold), format!("{}", ResetStyle)), 196 | context_line: (format!("{}", Faint), format!("{}", NoFaint)), 197 | }, 198 | ColorSchemeSpec::Colored => ColorScheme { 199 | filename: (format!("{}", Fg(Blue)), format!("{}", Fg(Reset))), 200 | matched_part: (format!("{}", Fg(Red)), format!("{}", Fg(Reset))), 201 | context_line: (format!("{}", Faint), format!("{}", ResetStyle)), 202 | }, 203 | } 204 | } else { 205 | ColorScheme { 206 | filename: (String::new(), String::new()), 207 | matched_part: (String::new(), String::new()), 208 | context_line: (String::new(), String::new()), 209 | } 210 | } 211 | }, 212 | breaks: options.breaks, 213 | ellipsis: options.ellipsis, 214 | print_filename: options.print_filename, 215 | }; 216 | 217 | let mut output = if options.use_pager { 218 | let pager_process = match std::env::var_os("PAGER") { 219 | Some(pager_cmdline) => { 220 | // User configured custom pager via environment variable. 221 | // Since pager can contain parameters, not only command name, 222 | // it is needed to start it using shell. Find which shell to use. 223 | let shell_var = std::env::var_os("SHELL"); 224 | let shell_path = shell_var.as_ref().map(|v| v.as_os_str()).unwrap_or(OsStr::new("/bin/sh")); 225 | std::process::Command::new(shell_path) 226 | .args(&[OsStr::new("-c"), &pager_cmdline]) 227 | .stdin(std::process::Stdio::piped()) 228 | .spawn()? 229 | }, 230 | None => std::process::Command::new("less") 231 | .args(LESS_ARGS) 232 | .stdin(std::process::Stdio::piped()) 233 | .spawn()? 234 | }; 235 | Output::Pager(pager_process) 236 | } else { 237 | Output::Stdout(std::io::stdout()) 238 | }; 239 | 240 | let mut match_found = false; 241 | { 242 | let mut output_lock = output.lock(); 243 | 244 | let mut printer = Printer::new(output_lock.as_write(), appearance); 245 | 246 | let mut pattern: Cow = 247 | if options.regex { 248 | Cow::from(options.pattern.as_str()) 249 | } else { 250 | Cow::from(regex::escape(&options.pattern)) 251 | }; 252 | if options.whole_word { 253 | let p = pattern.to_mut(); 254 | p.insert_str(0, r"\b"); 255 | p.push_str(r"\b"); 256 | } 257 | let re = RegexBuilder::new(&pattern).case_insensitive(options.case_insensitive).build()?; 258 | 259 | match input_spec { 260 | InputSpec::GitGrep(args) => { 261 | let mut git_grep_args = vec![OsStr::new("grep"), OsStr::new("--files-with-matches")]; 262 | if options.case_insensitive { 263 | git_grep_args.push(OsStr::new("--ignore-case")) 264 | } 265 | if !options.regex { 266 | git_grep_args.push(OsStr::new("--fixed-strings")) 267 | } 268 | if options.whole_word { 269 | git_grep_args.push(OsStr::new("--word-regexp")) 270 | } 271 | git_grep_args.push(OsStr::new("-e")); 272 | git_grep_args.push(OsStr::new(&options.pattern)); 273 | git_grep_args.push(OsStr::new("--")); 274 | git_grep_args.extend(args.iter().map(|a| a.as_os_str())); 275 | let mut git_grep_process = std::process::Command::new("git") 276 | .args(&git_grep_args) 277 | .stdout(std::process::Stdio::piped()) 278 | .spawn()?; 279 | 280 | { 281 | let out = git_grep_process.stdout.as_mut().unwrap(); 282 | let mut reader = std::io::BufReader::new(out); 283 | let mut line = String::new(); 284 | while let Ok(bytes_count) = reader.read_line(&mut line) { 285 | if bytes_count == 0 { break } 286 | 287 | { 288 | let filepath = std::path::Path::new(line.trim_end_matches('\n')); 289 | 290 | let file = std::fs::File::open(&filepath)?; 291 | let mut input = std::io::BufReader::new(file); 292 | 293 | printer.reset(); 294 | match_found |= process_input(&mut input, &re, &options, Some(filepath), &mut printer)?; 295 | } 296 | 297 | line.clear(); 298 | } 299 | }; 300 | 301 | match git_grep_process.wait()?.code() { 302 | Some(0) | Some(1) => (), 303 | _ => return Err(Box::new(OgrepError::GitGrepFailed)), 304 | } 305 | } 306 | 307 | InputSpec::Stdin => { 308 | // Read from stdin if no input specification is given. 309 | let stdio = std::io::stdin(); 310 | let mut stdio_locked = stdio.lock(); 311 | let filename = None; 312 | match_found = process_input(&mut stdio_locked, &re, &options, filename, &mut printer)? 313 | } 314 | 315 | InputSpec::Files(files) => { 316 | for path in files { 317 | let file = std::fs::File::open(&path)?; 318 | let mut reader = std::io::BufReader::new(&file); 319 | 320 | printer.reset(); 321 | match_found |= process_input(&mut reader, &re, &options, Some(&path), &mut printer)? 322 | } 323 | } 324 | } 325 | } 326 | 327 | output.close()?; 328 | Ok(if match_found { 0 } else { 1 }) 329 | } 330 | 331 | fn main() { 332 | match real_main() { 333 | Ok(code) => std::process::exit(code), 334 | Err(err) => { 335 | writeln!(std::io::stderr(), "{}", err).unwrap(); 336 | std::process::exit(2); 337 | }, 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::path::PathBuf; 3 | use clap; 4 | 5 | pub enum InputSpec { 6 | Stdin, 7 | Files(Vec), 8 | GitGrep(Vec), 9 | } 10 | 11 | arg_enum!{ 12 | #[derive(Debug)] 13 | pub enum UseColors { Always, Auto, Never } 14 | } 15 | 16 | arg_enum!{ 17 | #[derive(Debug)] 18 | pub enum Preprocessor { Context, Ignore, Preserve } 19 | } 20 | 21 | arg_enum!{ 22 | #[derive(Debug)] 23 | pub enum ColorSchemeSpec { Grey, Colored } 24 | } 25 | 26 | arg_enum!{ 27 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 28 | pub enum PrintFilename { No, PerFile, PerLine } 29 | } 30 | 31 | pub struct Options { 32 | pub pattern: String, 33 | pub regex: bool, 34 | pub case_insensitive: bool, 35 | pub whole_word: bool, 36 | pub use_colors: UseColors, 37 | pub color_scheme: ColorSchemeSpec, 38 | pub use_pager: bool, 39 | pub use_git_grep: bool, 40 | pub breaks: bool, 41 | pub ellipsis: bool, 42 | pub print_filename: PrintFilename, 43 | pub smart_branches: bool, 44 | pub preprocessor: Preprocessor, 45 | pub context_lines_before: usize, 46 | pub context_lines_after: usize, 47 | pub children: bool, 48 | } 49 | 50 | pub fn parse_arguments<'i, Iter: Iterator>(args: Iter) 51 | -> Result<(InputSpec, Options), clap::Error> { 52 | use clap::{App, Arg}; 53 | 54 | let colors_default = UseColors::Auto.to_string(); 55 | let color_scheme_default = ColorSchemeSpec::Grey.to_string(); 56 | let preprocessor_default = Preprocessor::Context.to_string(); 57 | 58 | let matches = App::new(crate_name!()) 59 | .about(crate_description!()) 60 | .author(crate_authors!("\n")) 61 | .version(crate_version!()) 62 | .setting(clap::AppSettings::NoBinaryName) 63 | .after_help("\ 64 | ENVIRONMENT VARIABLES: 65 | OGREP_OPTIONS Default options 66 | 67 | EXIT STATUS: 68 | 0 Some matches found 69 | 1 No matches found 70 | 2 An error occurred") 71 | .arg(Arg::with_name("pattern") 72 | .help("Pattern to search for") 73 | .required(true)) 74 | .arg(Arg::with_name("input") 75 | .multiple(true) 76 | .help("Files to search in, omit to search in stdin")) 77 | .arg(Arg::with_name("regex") 78 | .short("e") 79 | .long("regex") 80 | .help("Treat pattern as regular expression")) 81 | .arg(Arg::with_name("case-insensitive") 82 | .short("i") 83 | .long("case-insensitive") 84 | .help("Perform case-insensitive matching")) 85 | .arg(Arg::with_name("whole-word") 86 | .short("w") 87 | .long("word") 88 | .help("Search for whole words matching pattern")) 89 | .arg(Arg::with_name("children") 90 | .short("c") 91 | .long("children") 92 | .help("Show all lines with greater indentation (children) after matching line")) 93 | .arg(Arg::with_name("before_context") 94 | .short("B") 95 | .long("before-context") 96 | .takes_value(true) 97 | .help("Show specified number of leading lines before matched one")) 98 | .arg(Arg::with_name("after_context") 99 | .short("A") 100 | .long("after-context") 101 | .takes_value(true) 102 | .help("Show specified number of trailing lines after matched one")) 103 | .arg(Arg::with_name("both_contexts") 104 | .short("C") 105 | .long("context") 106 | .takes_value(true) 107 | .conflicts_with_all(&["before_context", "after_context"]) 108 | .help("Show specified number of leading and trailing lines before/after matched one")) 109 | .arg(Arg::with_name("color") 110 | .long("color") 111 | .takes_value(true) 112 | .default_value(&colors_default) 113 | .possible_values(&UseColors::variants()) 114 | .case_insensitive(true) 115 | .help("Whether to use colors")) 116 | .arg(Arg::with_name("color-scheme") 117 | .long("color-scheme") 118 | .takes_value(true) 119 | .default_value(&color_scheme_default) 120 | .possible_values(&ColorSchemeSpec::variants()) 121 | .case_insensitive(true) 122 | .help("Color scheme to use")) 123 | .arg(Arg::with_name("no-pager") 124 | .long("no-pager") 125 | .help("Don't use pager even when output is terminal")) 126 | .arg(Arg::with_name("use-git-grep") 127 | .long("use-git-grep") 128 | .short("g") 129 | .help("Use git grep for prior search")) 130 | .arg(Arg::with_name("no-breaks") 131 | .long("no-breaks") 132 | .help("Don't preserve line breaks")) 133 | .arg(Arg::with_name("ellipsis") 134 | .long("ellipsis") 135 | .help("Print ellipsis when lines were skipped")) 136 | .arg(Arg::with_name("print-filename") 137 | .long("print-filename") 138 | .takes_value(true) 139 | .possible_values(&PrintFilename::variants()) 140 | .case_insensitive(true) 141 | .help("When to print filename")) 142 | .arg(Arg::with_name("print-filename-per-file") 143 | .short("f") 144 | .conflicts_with_all(&["print-filename", "F"]) 145 | .help("Print filename before first match in file, shortcut for --print-filename=per-file")) 146 | .arg(Arg::with_name("print-filename-per-line") 147 | .short("F") 148 | .conflicts_with_all(&["print-filename", "f"]) 149 | .help("Print filename on each line, shortcut for --print-filename=per-line")) 150 | .arg(Arg::with_name("no-smart-branches") 151 | .long("no-smart-branches") 152 | .help("Don't handle if/if-else/else conditionals specially")) 153 | .arg(Arg::with_name("preprocessor") 154 | .long("preprocessor") 155 | .takes_value(true) 156 | .default_value(&preprocessor_default) 157 | .possible_values(&Preprocessor::variants()) 158 | .case_insensitive(true) 159 | .help("How to handle C preprocessor instructions 160 | • context: maintain and print separate context of preprocessor instructions 161 | • ignore: totally ignore preprocessor instructions 162 | • preserve: don't try to detect preprocessor instructions and treat them as any other lines 163 | " 164 | ) 165 | .hide_possible_values(true)) 166 | .get_matches_from(args); 167 | 168 | let (before_context, after_context) = 169 | if matches.is_present("both_contexts") { 170 | let c: usize = value_t!(matches.value_of("both_contexts"), usize)?; 171 | (c, c) 172 | } else { 173 | let before = 174 | if matches.is_present("before_context") { 175 | value_t!(matches.value_of("before_context"), usize)? 176 | } else { 177 | 0 178 | }; 179 | let after = 180 | if matches.is_present("after_context") { 181 | value_t!(matches.value_of("after_context"), usize)? 182 | } else { 183 | 0 184 | }; 185 | (before, after) 186 | }; 187 | 188 | let inputs = matches.values_of_os("input").unwrap_or_default().map(OsString::from).collect(); 189 | let input = if matches.is_present("use-git-grep") { 190 | // All inputs are just parameters for 'git grep' 191 | InputSpec::GitGrep(inputs) 192 | } else { 193 | if inputs.is_empty() || (inputs.len() == 1 && inputs[0] == "-") { 194 | InputSpec::Stdin 195 | } else { 196 | if inputs.iter().any(|i| *i == "-") { 197 | return Err(clap::Error::with_description( 198 | r#""-" must be the only argument"#, 199 | clap::ErrorKind::InvalidValue)); 200 | } 201 | InputSpec::Files(inputs.iter().map(PathBuf::from).collect()) 202 | } 203 | }; 204 | 205 | let options = Options { 206 | pattern: matches.value_of("pattern").expect("pattern").to_string(), 207 | regex: matches.is_present("regex"), 208 | case_insensitive: matches.is_present("case-insensitive"), 209 | whole_word: matches.is_present("whole-word"), 210 | use_colors: value_t!(matches, "color", UseColors)?, 211 | color_scheme: value_t!(matches, "color-scheme", ColorSchemeSpec)?, 212 | use_pager: !matches.is_present("no-pager"), 213 | use_git_grep: matches.is_present("use-git-grep"), 214 | breaks: !matches.is_present("no-breaks") && !matches.is_present("children"), 215 | ellipsis: matches.is_present("ellipsis"), 216 | print_filename: 217 | if matches.is_present("print-filename") { 218 | value_t!(matches, "print-filename", PrintFilename)? 219 | } else if matches.is_present("print-filename-per-file") { 220 | PrintFilename::PerFile 221 | } else if matches.is_present("print-filename-per-line") { 222 | PrintFilename::PerLine 223 | } else { 224 | PrintFilename::PerFile 225 | }, 226 | smart_branches: !matches.is_present("no-smart-branches"), 227 | preprocessor: value_t!(matches, "preprocessor", Preprocessor)?, 228 | context_lines_before: before_context, 229 | context_lines_after: after_context, 230 | children: matches.is_present("children"), 231 | }; 232 | Ok((input, options)) 233 | } 234 | -------------------------------------------------------------------------------- /src/printer.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use regex; 3 | 4 | use std::os::unix::ffi::OsStrExt; 5 | use std::fmt::Write as FmtWrite; 6 | use std::path::Path; 7 | 8 | use options::PrintFilename; 9 | 10 | pub struct ColorScheme { 11 | pub filename: (String, String), 12 | pub matched_part: (String, String), 13 | pub context_line: (String, String), 14 | } 15 | 16 | pub struct AppearanceOptions { 17 | pub color_scheme: ColorScheme, 18 | pub breaks: bool, 19 | pub ellipsis: bool, 20 | pub print_filename: PrintFilename, 21 | } 22 | 23 | pub struct Printer<'o> { 24 | pub output: &'o mut dyn std::io::Write, 25 | pub options: AppearanceOptions, 26 | last_printed_lineno: usize, 27 | was_break: bool, 28 | } 29 | 30 | impl<'o> Printer<'o> { 31 | pub fn new(output: &'o mut dyn std::io::Write, options: AppearanceOptions) -> Self { 32 | Printer { 33 | output: output, 34 | options: options, 35 | last_printed_lineno: 0, 36 | was_break: false, 37 | } 38 | } 39 | 40 | pub fn reset(&mut self) { 41 | self.last_printed_lineno = 0; 42 | self.was_break = false; 43 | } 44 | 45 | pub fn print_context(&mut self, filepath: Option<&Path>, line_number: usize, line: &str) { 46 | assert!(line_number > self.last_printed_lineno); 47 | self.maybe_print_ellipsis(line_number); 48 | 49 | match (self.options.print_filename, filepath) { 50 | (PrintFilename::PerLine, Some(path)) => 51 | write!(self.output, "{color}{}:{:04}:{nocolor} ", 52 | path.to_string_lossy(), line_number, 53 | color=self.options.color_scheme.context_line.0, 54 | nocolor=self.options.color_scheme.context_line.1).unwrap(), 55 | _ => write!(self.output, "{color}{:4}:{nocolor} ", 56 | line_number, 57 | color=self.options.color_scheme.context_line.0, 58 | nocolor=self.options.color_scheme.context_line.1).unwrap(), 59 | } 60 | 61 | writeln!(self.output, "{color}{}{nocolor}", line, 62 | color=self.options.color_scheme.context_line.0, 63 | nocolor=self.options.color_scheme.context_line.1).unwrap(); 64 | self.last_printed_lineno = line_number; 65 | } 66 | 67 | pub fn print_match<'m, M>(&mut self, filepath: Option<&Path>, line_number: usize, 68 | line: &str, matches: M) 69 | where M: Iterator> { 70 | assert!(line_number > self.last_printed_lineno); 71 | self.maybe_print_ellipsis(line_number); 72 | 73 | match (self.options.print_filename, filepath) { 74 | (PrintFilename::PerLine, Some(path)) => 75 | write!(self.output, "{}:{:04}: ", 76 | path.to_string_lossy(), line_number).unwrap(), 77 | _ => write!(self.output, "{:4}: ", line_number).unwrap(), 78 | } 79 | 80 | let mut buf = String::new(); 81 | let mut pos = 0usize; 82 | for m in matches { 83 | buf.push_str(&line[pos..m.start()]); 84 | write!(&mut buf, "{color}{}{nocolor}", m.as_str(), 85 | color=self.options.color_scheme.matched_part.0, 86 | nocolor=self.options.color_scheme.matched_part.1).unwrap(); 87 | pos = m.end(); 88 | } 89 | buf.push_str(&line[pos..]); 90 | 91 | writeln!(self.output, "{}", buf).unwrap(); 92 | self.last_printed_lineno = line_number; 93 | } 94 | 95 | pub fn print_break(&mut self) { 96 | if self.options.breaks { 97 | writeln!(self.output).unwrap(); 98 | self.was_break = true; 99 | } 100 | } 101 | 102 | fn maybe_print_ellipsis(&mut self, line_number: usize) { 103 | if self.was_break { 104 | self.was_break = false; 105 | return; 106 | } 107 | if line_number > self.last_printed_lineno + 1 { 108 | self.print_ellipsis(); 109 | } 110 | } 111 | 112 | pub fn print_ellipsis(&mut self) { 113 | if self.options.ellipsis { 114 | writeln!(self.output, " {color}{}{nocolor}", "…", 115 | color=self.options.color_scheme.context_line.0, 116 | nocolor=self.options.color_scheme.context_line.1).unwrap(); 117 | } 118 | } 119 | 120 | pub fn print_heading_filename(&mut self, filename: &std::path::Path) { 121 | assert!(self.last_printed_lineno == 0); 122 | if self.options.print_filename == PrintFilename::PerFile { 123 | self.output.write(b"\n").unwrap(); 124 | write!(&mut self.output, "{}", self.options.color_scheme.filename.0).unwrap(); 125 | self.output.write(filename.as_os_str().as_bytes()).unwrap(); 126 | write!(&mut self.output, "{}", self.options.color_scheme.filename.1).unwrap(); 127 | self.output.write(b"\n\n").unwrap(); 128 | } else if self.options.breaks { 129 | self.output.write(b"\n").unwrap(); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use super::options::*; 3 | 4 | use regex::Regex; 5 | use std::path::Path; 6 | 7 | use std::fmt::Write as FmtWrite; 8 | 9 | /// Returns default options for tests. 10 | /// Note that this is not the same as options used by CLI by default. 11 | /// CLI by default enabled some features which are useful for most users, 12 | /// like handling proprocessor lines, while default options for tests 13 | /// enables only minimal possible set of features. 14 | /// 15 | /// Use 16 | /// ``` 17 | /// Options { print_filename: true, ..default_options() } 18 | /// ``` 19 | /// to alter options. 20 | fn default_options() -> Options { 21 | Options { 22 | pattern: String::new(), 23 | regex: false, 24 | case_insensitive: false, 25 | whole_word: false, 26 | use_colors: UseColors::Never, 27 | color_scheme: ColorSchemeSpec::Grey, 28 | use_pager: false, 29 | use_git_grep: false, 30 | breaks: false, 31 | ellipsis: false, 32 | print_filename: PrintFilename::No, 33 | smart_branches: false, 34 | preprocessor: Preprocessor::Preserve, 35 | context_lines_before: 0, 36 | context_lines_after: 0, 37 | children: false, 38 | } 39 | } 40 | 41 | /// Tests which lines are dipslayed in ogrep result. 42 | /// 43 | /// `pattern` is fixed string to search for, 44 | /// `specification` should be written in special format which is used to 45 | /// prepare both input text and expected result. Each line must start with 46 | /// one of: 47 | /// * line starting with ". " means that line must be ommitted from result, 48 | /// * line starting with "o " means that line must be printed, 49 | /// * "~ …" means that ellipsis should be printed 50 | /// * "~" means that break should be printed 51 | fn test(options: &Options, pattern: &str, specification: &str) { 52 | let mut input = String::with_capacity(specification.len()); 53 | let mut expected_output = String::with_capacity(specification.len()); 54 | 55 | fn rest_of_line(line: &str) -> &str { 56 | assert!(!line.is_empty()); 57 | if line.len() == 1 { 58 | return ""; 59 | } else { 60 | assert_eq!(line.chars().skip(1).next(), Some(' ')); 61 | return &line[2..]; 62 | } 63 | } 64 | 65 | let mut line_number = 0usize; 66 | for line in specification.lines() { 67 | let line = line.trim_start(); 68 | 69 | if line.is_empty() { continue } 70 | let (to_input, to_expected) = match line { 71 | l if l.starts_with(".") => (Some(rest_of_line(line)), None), 72 | l if l.starts_with("o") => (Some(rest_of_line(line)), 73 | Some((rest_of_line(line), true))), 74 | "~ …" => (None, Some((" …", false))), 75 | "~" => (None, Some(("", false))), 76 | _ => panic!("unexpected specification line: {}", line), 77 | }; 78 | 79 | if let Some(to_input) = to_input { 80 | line_number += 1; 81 | write!(input, "{}\n", to_input).unwrap(); 82 | } 83 | if let Some((expected, with_line_number)) = to_expected { 84 | if with_line_number { 85 | write!(expected_output, "{:4}: {}\n", line_number, expected).unwrap(); 86 | } else { 87 | write!(expected_output, "{}\n", expected).unwrap(); 88 | } 89 | } 90 | } 91 | 92 | let regex = Regex::new(pattern).expect("invalid regexp"); 93 | let mut result = std::io::Cursor::new(Vec::new()); 94 | { 95 | let mut printer = Printer::new( 96 | &mut result, 97 | AppearanceOptions { 98 | color_scheme: ColorScheme { 99 | filename: ("".to_string(), "".to_string()), 100 | matched_part: ("".to_string(), "".to_string()), 101 | context_line: ("".to_string(), "".to_string()), 102 | }, 103 | breaks: options.breaks, 104 | ellipsis: options.ellipsis, 105 | print_filename: options.print_filename, 106 | }); 107 | let mut input = std::io::BufReader::new(std::io::Cursor::new(input)); 108 | process_input(&mut input, ®ex, &options, None, &mut printer).expect("i/o error"); 109 | } 110 | 111 | let mut result = result.into_inner(); 112 | 113 | // Hack to fix assert_diff! output which incorrectly indents first line. 114 | result.insert(0, b'\n'); 115 | expected_output.insert(0, '\n'); 116 | 117 | assert_diff!(&expected_output, 118 | std::str::from_utf8(result.as_slice()).expect("output is not valid utf-8"), 119 | "\n", 0); 120 | } 121 | 122 | /// Tests most basic ogrep function: showing context based on indentation. 123 | #[test] 124 | fn test_simple_context() { 125 | test(&default_options(), "bla", 126 | "o foo 127 | . bar 128 | . baz 129 | o qux 130 | o bla"); 131 | } 132 | 133 | /// Tests that all corresponding if-else branches are preserved if match 134 | /// is found in one of branches. 135 | #[test] 136 | fn test_smart_branches() { 137 | test(&Options { smart_branches: true, ..default_options() }, 138 | "bla", 139 | 140 | "o if a > 0 { 141 | . bar 142 | . baz 143 | o } else { 144 | o qux 145 | o bla 146 | . }"); 147 | } 148 | 149 | /// Tests that smart-branches feature handles Python code. 150 | #[test] 151 | fn test_smart_branches_python() { 152 | test(&Options { smart_branches: true, ..default_options() }, 153 | "bla", 154 | 155 | "o if a > 0: 156 | . bar 157 | . baz 158 | o else: 159 | o qux 160 | o bla"); 161 | } 162 | 163 | /// Tests that branches are recognized even when there are no 164 | /// space after 'if'. 165 | #[test] 166 | fn test_smart_branches_no_space() { 167 | test(&Options { smart_branches: true, ..default_options() }, 168 | "bla", 169 | 170 | "o if(a > 0) { 171 | . bar 172 | . baz 173 | o } else { 174 | o qux 175 | o bla 176 | . }"); 177 | } 178 | 179 | /// Tests that branches are NOT recognized when first words 180 | /// has 'if' prefix. 181 | #[test] 182 | fn test_smart_branches_if_prefix() { 183 | test(&Options { smart_branches: true, ..default_options() }, 184 | "bla", 185 | 186 | ". ifere(a > 0) { 187 | . bar 188 | . baz 189 | o } else { 190 | o qux 191 | o bla 192 | . }"); 193 | } 194 | 195 | 196 | /// Tests 'preserve' mode of handling preprocessor instructions, 197 | /// they must be treated just like usual lines. 198 | #[test] 199 | fn test_preprocessor_preserve() { 200 | test(&Options { preprocessor: Preprocessor::Preserve, ..default_options() }, 201 | "bla", 202 | 203 | ". foo 204 | . #if defined(yup) 205 | . bar 206 | . baz 207 | o #else 208 | o qux 209 | o bla 210 | . #endif"); 211 | } 212 | 213 | /// Tests 'ignore' mode of handling preprocessor instructions, 214 | /// they must be completely ignored. 215 | #[test] 216 | fn test_preprocessor_ignore() { 217 | test(&Options { preprocessor: Preprocessor::Ignore, ..default_options() }, 218 | "bla", 219 | 220 | "o foo 221 | . #if defined(yup) 222 | . bar 223 | . baz 224 | . #else 225 | o qux 226 | o bla 227 | . #endif"); 228 | } 229 | 230 | /// Tests 'context' mode of handling preprocessor instructions, 231 | /// they must form parallel context. 232 | #[test] 233 | fn test_preprocessor_context() { 234 | test(&Options { preprocessor: Preprocessor::Context, ..default_options() }, 235 | "bla", 236 | 237 | "o foo 238 | o #if defined(yup) 239 | . bar 240 | . baz 241 | o #else 242 | o qux 243 | o bla 244 | . #endif"); 245 | } 246 | 247 | /// Tests that matched preprocessor lines are printed. 248 | #[test] 249 | fn test_matched_preprocessor_lines_are_printed() { 250 | test(&Options { preprocessor: Preprocessor::Context, ..default_options() }, 251 | "bla", 252 | 253 | ". foo 254 | o #if defined(bla) 255 | . bar 256 | . baz 257 | . #endif"); 258 | } 259 | 260 | /// Tests 'context' mode of handling preprocessor instructions, 261 | /// they must form parallel context. 262 | #[test] 263 | fn test_jinja_preprocessor() { 264 | test(&Options { preprocessor: Preprocessor::Context, ..default_options() }, 265 | "bla", 266 | 267 | "o foo 268 | o {% if defined(yup) %} 269 | . bar 270 | . baz 271 | o {%- else %} 272 | o qux 273 | o bla 274 | . {% endif %}"); 275 | } 276 | 277 | /// Tests printing textual context. 278 | #[test] 279 | fn test_context_before() { 280 | test(&Options { context_lines_before: 1, ..default_options() }, 281 | "bla", 282 | 283 | "o foo 284 | . bar 285 | . baz 286 | o qux 287 | . bat 288 | o boo 289 | o bla 290 | . pug"); 291 | } 292 | 293 | /// Tests printing textual context. 294 | #[test] 295 | fn test_context_after() { 296 | test(&Options { context_lines_after: 1, ..default_options() }, 297 | "bla", 298 | 299 | "o foo 300 | . bar 301 | . baz 302 | o qux 303 | . bat 304 | . boo 305 | o bla 306 | o pug"); 307 | } 308 | 309 | /// Tests that ellipsis may be printed when lines are skipped. 310 | #[test] 311 | fn test_ellipsis() { 312 | test(&Options { ellipsis: true, ..default_options() }, 313 | "bla", 314 | 315 | "o foo 316 | . bar 317 | . baz 318 | ~ … 319 | o qux 320 | . boo 321 | ~ … 322 | o bla"); 323 | } 324 | 325 | /// Tests that breaks are printed instead of ellipsis when there 326 | /// was empty line in source text. 327 | #[test] 328 | fn test_breaks() { 329 | test(&Options { breaks: true, ..default_options() }, 330 | "bla", 331 | 332 | "o foo 333 | . bar 334 | . baz 335 | . 336 | o qux 337 | o bla 338 | . 339 | ~ 340 | o bla"); 341 | } 342 | 343 | /// Tests that breaks are printed instead of ellipsis when there 344 | /// was empty line in source text. 345 | #[test] 346 | fn test_breaks_incorrect() { 347 | test(&Options { breaks: true, ..default_options() }, 348 | "bla", 349 | 350 | "o foo 351 | . bar 352 | . baz 353 | . 354 | o qux 355 | o bla 356 | ~ 357 | o fux 358 | . 359 | o bla"); 360 | } 361 | 362 | /// Tests printing all children of matched line. 363 | #[test] 364 | fn test_children() { 365 | test(&Options { children: true, ..default_options() }, 366 | "foo", 367 | 368 | "o foo 369 | o bar 370 | o baz"); 371 | } 372 | 373 | /// Tests printing all children of matched line when there is 374 | /// another match inside children. 375 | #[test] 376 | fn test_nested_children() { 377 | test(&Options { children: true, ..default_options() }, 378 | "foo", 379 | 380 | "o foo 381 | o bar 382 | o foo 383 | o baz"); 384 | } 385 | 386 | /// Tests printing breaks together with children context. 387 | /// It doesn't work right now and current workaround is to disable breaks 388 | /// when children option is used. 389 | // #[test] 390 | #[allow(dead_code)] 391 | fn test_children_breaks() { 392 | test(&Options { breaks: true, children: true, ..default_options() }, 393 | "foo", 394 | 395 | "o foo 396 | o bar 397 | o 398 | o baz 399 | ~ 400 | o foo 401 | o bar"); 402 | } 403 | 404 | /// Tests that stdin is read when no inputs are given and there is 405 | /// no 'use-git-grep' option. 406 | #[test] 407 | fn test_options_empty_args_is_stdin() { 408 | let args = &["foo"]; 409 | let (input, _opts) = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 410 | assert!(matches!(input, InputSpec::Stdin)); 411 | } 412 | 413 | /// Tests that explicit stdin input ("-") is parsed. 414 | #[test] 415 | fn test_options_explicit_stdin_arg() { 416 | let args = &["foo", "-"]; 417 | let (input, _opts) = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 418 | assert!(matches!(input, InputSpec::Stdin)); 419 | } 420 | 421 | /// Tests that explicit stdin input ("-") must be the only provided input. 422 | #[test] 423 | #[should_panic(expected="must be the only argument")] 424 | fn test_options_explicit_stdin_arg_must_be_alone() { 425 | let args = &["foo", "-", "a"]; 426 | let _ = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 427 | } 428 | 429 | /// Tests that explicit stdin input ("-") must be the only provided input. 430 | #[test] 431 | fn test_options_file_inputs() { 432 | let args = &["foo", "a", "b"]; 433 | let (input, _opts) = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 434 | assert!(matches!(input, 435 | InputSpec::Files(f) if f == &[Path::new("a"), Path::new("b")])); 436 | } 437 | 438 | #[test] 439 | fn test_options_git_grep_input() { 440 | let args = &["--use-git-grep", "foo", "a", "b"]; 441 | let (input, _opts) = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 442 | assert!(matches!(input, 443 | InputSpec::GitGrep(a) if a == &[OsStr::new("a"), OsStr::new("b")])); 444 | } 445 | 446 | #[test] 447 | fn test_options_git_grep_empty_input() { 448 | let args = &["--use-git-grep", "foo"]; 449 | let (input, _opts) = options::parse_arguments(args.iter().map(OsString::from)).unwrap(); 450 | assert!(matches!(input, 451 | InputSpec::GitGrep(a) if a.is_empty())); 452 | } 453 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | 2 | /// Checks whether `text` starts with `prefix` and there is word boundary 3 | /// right after prefix, i.e. either `text` ends there or next character 4 | /// is not alphanumberic. 5 | pub fn starts_with_word(text: &str, prefix: &str) -> bool { 6 | text.starts_with(prefix) && 7 | text[prefix.len()..].chars().next().map(|c| !c.is_ascii_alphanumeric()).unwrap_or(true) 8 | } 9 | --------------------------------------------------------------------------------