├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── dependabot_merge.yml │ ├── periodic.yml │ ├── regression.yml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── benches ├── matcher.rs └── path_finder.rs ├── compare_ambr.sh ├── compare_ambs.sh ├── rustfmt.toml ├── src ├── ambr.rs ├── ambs.rs ├── console.rs ├── ignore.rs ├── lib.rs ├── matcher.rs ├── pipeline.rs ├── pipeline_finder.rs ├── pipeline_matcher.rs ├── pipeline_printer.rs ├── pipeline_replacer.rs ├── pipeline_sorter.rs └── util.rs └── test ├── .bzr └── file ├── .gitignore ├── .hg └── file ├── .svn └── file ├── a.o ├── a.s ├── abc.o ├── abc.s ├── ao ├── d0.t ├── d00.t ├── dir0 └── file ├── dir1 └── file ├── dir11 └── dir12 │ └── file ├── dir2 └── file ├── dir3 ├── dir4 │ └── file └── dir7 │ └── file ├── dir5 └── dir6 │ └── file ├── dir7 └── file ├── dir8 └── file ├── dir9 └── dir10 │ └── file ├── file └── x ├── dir0 └── file ├── dir1 └── file └── file /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: dalance 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "20:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_merge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot auto-merge 2 | on: pull_request_target 3 | 4 | permissions: 5 | pull-requests: write 6 | contents: write 7 | 8 | jobs: 9 | dependabot: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.actor == 'dependabot[bot]' }} 12 | steps: 13 | - name: Dependabot metadata 14 | id: metadata 15 | uses: dependabot/fetch-metadata@v2.2.0 16 | with: 17 | github-token: '${{ secrets.GITHUB_TOKEN }}' 18 | - name: Enable auto-merge for Dependabot PRs 19 | if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' || ( !startsWith( steps.metadata.outputs.new-version, '0.' ) && steps.metadata.outputs.update-type == 'version-update:semver-minor' ) }} 20 | run: gh pr merge --auto --merge "$PR_URL" 21 | env: 22 | PR_URL: ${{github.event.pull_request.html_url}} 23 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 24 | -------------------------------------------------------------------------------- /.github/workflows/periodic.yml: -------------------------------------------------------------------------------- 1 | name: Periodic 2 | 3 | on: 4 | schedule: 5 | - cron: 0 0 * * SUN 6 | 7 | jobs: 8 | build: 9 | 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest] 13 | rust: [stable, beta, nightly] 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - name: Setup Rust 19 | uses: hecrj/setup-rust-action@v1 20 | with: 21 | rust-version: ${{ matrix.rust }} 22 | - name: Checkout 23 | uses: actions/checkout@v1 24 | - name: Run tests 25 | run: cargo test 26 | -------------------------------------------------------------------------------- /.github/workflows/regression.yml: -------------------------------------------------------------------------------- 1 | name: Regression 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macOS-latest, windows-latest] 15 | rust: [stable] 16 | 17 | runs-on: ${{ matrix.os }} 18 | 19 | steps: 20 | - name: Setup Rust 21 | uses: hecrj/setup-rust-action@v1 22 | with: 23 | rust-version: ${{ matrix.rust }} 24 | - name: Checkout 25 | uses: actions/checkout@v1 26 | - name: Run tests 27 | run: cargo test 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | build: 10 | 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macOS-latest, windows-latest] 14 | rust: [stable] 15 | 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - name: Setup Rust 20 | uses: hecrj/setup-rust-action@v1 21 | with: 22 | rust-version: ${{ matrix.rust }} 23 | - name: Checkout 24 | uses: actions/checkout@v1 25 | - name: Setup MUSL 26 | if: matrix.os == 'ubuntu-latest' 27 | run: | 28 | rustup target add x86_64-unknown-linux-musl 29 | sudo apt-get -qq install musl-tools 30 | - name: Build for linux 31 | if: matrix.os == 'ubuntu-latest' 32 | run: make release_lnx 33 | - name: Build for macOS 34 | if: matrix.os == 'macOS-latest' 35 | run: make release_mac 36 | - name: Build for Windows 37 | if: matrix.os == 'windows-latest' 38 | run: make release_win 39 | - name: Release 40 | uses: softprops/action-gh-release@v1 41 | with: 42 | files: '*.zip' 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | /data/ 14 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "amber" 16 | version = "0.6.0" 17 | dependencies = [ 18 | "crossbeam", 19 | "ctrlc", 20 | "directories", 21 | "filetime", 22 | "getch", 23 | "glob", 24 | "ignore", 25 | "lazy_static", 26 | "memmap", 27 | "num_cpus", 28 | "rand", 29 | "regex", 30 | "rlibc", 31 | "serde", 32 | "structopt", 33 | "tempfile", 34 | "term", 35 | "termios", 36 | "time", 37 | "toml", 38 | "unicode-width 0.2.0", 39 | ] 40 | 41 | [[package]] 42 | name = "ansi_term" 43 | version = "0.12.1" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 46 | dependencies = [ 47 | "winapi", 48 | ] 49 | 50 | [[package]] 51 | name = "atty" 52 | version = "0.2.14" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 55 | dependencies = [ 56 | "hermit-abi 0.1.19", 57 | "libc", 58 | "winapi", 59 | ] 60 | 61 | [[package]] 62 | name = "autocfg" 63 | version = "1.1.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 66 | 67 | [[package]] 68 | name = "bitflags" 69 | version = "1.3.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 72 | 73 | [[package]] 74 | name = "bitflags" 75 | version = "2.4.1" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 78 | 79 | [[package]] 80 | name = "bstr" 81 | version = "1.8.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" 84 | dependencies = [ 85 | "memchr", 86 | "serde", 87 | ] 88 | 89 | [[package]] 90 | name = "cfg-if" 91 | version = "1.0.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 94 | 95 | [[package]] 96 | name = "cfg_aliases" 97 | version = "0.2.1" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 100 | 101 | [[package]] 102 | name = "clap" 103 | version = "2.34.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 106 | dependencies = [ 107 | "ansi_term", 108 | "atty", 109 | "bitflags 1.3.2", 110 | "strsim", 111 | "textwrap", 112 | "unicode-width 0.1.14", 113 | "vec_map", 114 | ] 115 | 116 | [[package]] 117 | name = "crossbeam" 118 | version = "0.8.4" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" 121 | dependencies = [ 122 | "crossbeam-channel", 123 | "crossbeam-deque", 124 | "crossbeam-epoch", 125 | "crossbeam-queue", 126 | "crossbeam-utils", 127 | ] 128 | 129 | [[package]] 130 | name = "crossbeam-channel" 131 | version = "0.5.10" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "82a9b73a36529d9c47029b9fb3a6f0ea3cc916a261195352ba19e770fc1748b2" 134 | dependencies = [ 135 | "cfg-if", 136 | "crossbeam-utils", 137 | ] 138 | 139 | [[package]] 140 | name = "crossbeam-deque" 141 | version = "0.8.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "fca89a0e215bab21874660c67903c5f143333cab1da83d041c7ded6053774751" 144 | dependencies = [ 145 | "cfg-if", 146 | "crossbeam-epoch", 147 | "crossbeam-utils", 148 | ] 149 | 150 | [[package]] 151 | name = "crossbeam-epoch" 152 | version = "0.9.17" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "0e3681d554572a651dda4186cd47240627c3d0114d45a95f6ad27f2f22e7548d" 155 | dependencies = [ 156 | "autocfg", 157 | "cfg-if", 158 | "crossbeam-utils", 159 | ] 160 | 161 | [[package]] 162 | name = "crossbeam-queue" 163 | version = "0.3.10" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "adc6598521bb5a83d491e8c1fe51db7296019d2ca3cb93cc6c2a20369a4d78a2" 166 | dependencies = [ 167 | "cfg-if", 168 | "crossbeam-utils", 169 | ] 170 | 171 | [[package]] 172 | name = "crossbeam-utils" 173 | version = "0.8.18" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "c3a430a770ebd84726f584a90ee7f020d28db52c6d02138900f22341f866d39c" 176 | dependencies = [ 177 | "cfg-if", 178 | ] 179 | 180 | [[package]] 181 | name = "ctrlc" 182 | version = "3.4.7" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" 185 | dependencies = [ 186 | "nix", 187 | "windows-sys 0.59.0", 188 | ] 189 | 190 | [[package]] 191 | name = "deranged" 192 | version = "0.4.0" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 195 | dependencies = [ 196 | "powerfmt", 197 | ] 198 | 199 | [[package]] 200 | name = "directories" 201 | version = "6.0.0" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" 204 | dependencies = [ 205 | "dirs-sys", 206 | ] 207 | 208 | [[package]] 209 | name = "dirs-sys" 210 | version = "0.5.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 213 | dependencies = [ 214 | "libc", 215 | "option-ext", 216 | "redox_users", 217 | "windows-sys 0.59.0", 218 | ] 219 | 220 | [[package]] 221 | name = "equivalent" 222 | version = "1.0.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 225 | 226 | [[package]] 227 | name = "errno" 228 | version = "0.3.10" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 231 | dependencies = [ 232 | "libc", 233 | "windows-sys 0.59.0", 234 | ] 235 | 236 | [[package]] 237 | name = "fastrand" 238 | version = "2.1.1" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" 241 | 242 | [[package]] 243 | name = "filetime" 244 | version = "0.2.25" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 247 | dependencies = [ 248 | "cfg-if", 249 | "libc", 250 | "libredox", 251 | "windows-sys 0.59.0", 252 | ] 253 | 254 | [[package]] 255 | name = "getch" 256 | version = "0.3.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "13990e2d5b29e1770ddf7fc000afead4acb9bd8f8a9602de63bf189e261b1ba8" 259 | dependencies = [ 260 | "libc", 261 | "termios", 262 | ] 263 | 264 | [[package]] 265 | name = "getrandom" 266 | version = "0.2.11" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 269 | dependencies = [ 270 | "cfg-if", 271 | "libc", 272 | "wasi 0.11.0+wasi-snapshot-preview1", 273 | ] 274 | 275 | [[package]] 276 | name = "getrandom" 277 | version = "0.3.0" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "71393ecc86efbf00e4ca13953979ba8b94cfe549a4b74cc26d8b62f4d8feac2b" 280 | dependencies = [ 281 | "cfg-if", 282 | "libc", 283 | "wasi 0.13.3+wasi-0.2.2", 284 | "windows-targets", 285 | ] 286 | 287 | [[package]] 288 | name = "glob" 289 | version = "0.3.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 292 | 293 | [[package]] 294 | name = "globset" 295 | version = "0.4.15" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" 298 | dependencies = [ 299 | "aho-corasick", 300 | "bstr", 301 | "log", 302 | "regex-automata", 303 | "regex-syntax", 304 | ] 305 | 306 | [[package]] 307 | name = "hashbrown" 308 | version = "0.15.2" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 311 | 312 | [[package]] 313 | name = "heck" 314 | version = "0.3.3" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 317 | dependencies = [ 318 | "unicode-segmentation", 319 | ] 320 | 321 | [[package]] 322 | name = "hermit-abi" 323 | version = "0.1.19" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 326 | dependencies = [ 327 | "libc", 328 | ] 329 | 330 | [[package]] 331 | name = "hermit-abi" 332 | version = "0.5.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" 335 | 336 | [[package]] 337 | name = "home" 338 | version = "0.5.9" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 341 | dependencies = [ 342 | "windows-sys 0.52.0", 343 | ] 344 | 345 | [[package]] 346 | name = "ignore" 347 | version = "0.4.23" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" 350 | dependencies = [ 351 | "crossbeam-deque", 352 | "globset", 353 | "log", 354 | "memchr", 355 | "regex-automata", 356 | "same-file", 357 | "walkdir", 358 | "winapi-util", 359 | ] 360 | 361 | [[package]] 362 | name = "indexmap" 363 | version = "2.7.1" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 366 | dependencies = [ 367 | "equivalent", 368 | "hashbrown", 369 | ] 370 | 371 | [[package]] 372 | name = "lazy_static" 373 | version = "1.5.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 376 | 377 | [[package]] 378 | name = "libc" 379 | version = "0.2.172" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 382 | 383 | [[package]] 384 | name = "libredox" 385 | version = "0.1.3" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 388 | dependencies = [ 389 | "bitflags 2.4.1", 390 | "libc", 391 | "redox_syscall", 392 | ] 393 | 394 | [[package]] 395 | name = "linux-raw-sys" 396 | version = "0.9.2" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9" 399 | 400 | [[package]] 401 | name = "log" 402 | version = "0.4.21" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 405 | 406 | [[package]] 407 | name = "memchr" 408 | version = "2.6.4" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 411 | 412 | [[package]] 413 | name = "memmap" 414 | version = "0.7.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" 417 | dependencies = [ 418 | "libc", 419 | "winapi", 420 | ] 421 | 422 | [[package]] 423 | name = "nix" 424 | version = "0.30.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 427 | dependencies = [ 428 | "bitflags 2.4.1", 429 | "cfg-if", 430 | "cfg_aliases", 431 | "libc", 432 | ] 433 | 434 | [[package]] 435 | name = "num-conv" 436 | version = "0.1.0" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 439 | 440 | [[package]] 441 | name = "num_cpus" 442 | version = "1.17.0" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 445 | dependencies = [ 446 | "hermit-abi 0.5.1", 447 | "libc", 448 | ] 449 | 450 | [[package]] 451 | name = "once_cell" 452 | version = "1.19.0" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 455 | 456 | [[package]] 457 | name = "option-ext" 458 | version = "0.2.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 461 | 462 | [[package]] 463 | name = "powerfmt" 464 | version = "0.2.0" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 467 | 468 | [[package]] 469 | name = "ppv-lite86" 470 | version = "0.2.17" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 473 | 474 | [[package]] 475 | name = "proc-macro-error" 476 | version = "1.0.4" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 479 | dependencies = [ 480 | "proc-macro-error-attr", 481 | "proc-macro2", 482 | "quote", 483 | "syn 1.0.109", 484 | "version_check", 485 | ] 486 | 487 | [[package]] 488 | name = "proc-macro-error-attr" 489 | version = "1.0.4" 490 | source = "registry+https://github.com/rust-lang/crates.io-index" 491 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 492 | dependencies = [ 493 | "proc-macro2", 494 | "quote", 495 | "version_check", 496 | ] 497 | 498 | [[package]] 499 | name = "proc-macro2" 500 | version = "1.0.93" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 503 | dependencies = [ 504 | "unicode-ident", 505 | ] 506 | 507 | [[package]] 508 | name = "quote" 509 | version = "1.0.35" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 512 | dependencies = [ 513 | "proc-macro2", 514 | ] 515 | 516 | [[package]] 517 | name = "rand" 518 | version = "0.9.1" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" 521 | dependencies = [ 522 | "rand_chacha", 523 | "rand_core", 524 | ] 525 | 526 | [[package]] 527 | name = "rand_chacha" 528 | version = "0.9.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 531 | dependencies = [ 532 | "ppv-lite86", 533 | "rand_core", 534 | ] 535 | 536 | [[package]] 537 | name = "rand_core" 538 | version = "0.9.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" 541 | dependencies = [ 542 | "getrandom 0.3.0", 543 | "zerocopy", 544 | ] 545 | 546 | [[package]] 547 | name = "redox_syscall" 548 | version = "0.5.3" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 551 | dependencies = [ 552 | "bitflags 2.4.1", 553 | ] 554 | 555 | [[package]] 556 | name = "redox_users" 557 | version = "0.5.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 560 | dependencies = [ 561 | "getrandom 0.2.11", 562 | "libredox", 563 | "thiserror", 564 | ] 565 | 566 | [[package]] 567 | name = "regex" 568 | version = "1.11.1" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 571 | dependencies = [ 572 | "aho-corasick", 573 | "memchr", 574 | "regex-automata", 575 | "regex-syntax", 576 | ] 577 | 578 | [[package]] 579 | name = "regex-automata" 580 | version = "0.4.8" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" 583 | dependencies = [ 584 | "aho-corasick", 585 | "memchr", 586 | "regex-syntax", 587 | ] 588 | 589 | [[package]] 590 | name = "regex-syntax" 591 | version = "0.8.5" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 594 | 595 | [[package]] 596 | name = "rlibc" 597 | version = "1.0.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" 600 | 601 | [[package]] 602 | name = "rustix" 603 | version = "1.0.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" 606 | dependencies = [ 607 | "bitflags 2.4.1", 608 | "errno", 609 | "libc", 610 | "linux-raw-sys", 611 | "windows-sys 0.59.0", 612 | ] 613 | 614 | [[package]] 615 | name = "same-file" 616 | version = "1.0.6" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 619 | dependencies = [ 620 | "winapi-util", 621 | ] 622 | 623 | [[package]] 624 | name = "serde" 625 | version = "1.0.219" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 628 | dependencies = [ 629 | "serde_derive", 630 | ] 631 | 632 | [[package]] 633 | name = "serde_derive" 634 | version = "1.0.219" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 637 | dependencies = [ 638 | "proc-macro2", 639 | "quote", 640 | "syn 2.0.96", 641 | ] 642 | 643 | [[package]] 644 | name = "serde_spanned" 645 | version = "0.6.8" 646 | source = "registry+https://github.com/rust-lang/crates.io-index" 647 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 648 | dependencies = [ 649 | "serde", 650 | ] 651 | 652 | [[package]] 653 | name = "strsim" 654 | version = "0.8.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 657 | 658 | [[package]] 659 | name = "structopt" 660 | version = "0.3.26" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 663 | dependencies = [ 664 | "clap", 665 | "lazy_static", 666 | "structopt-derive", 667 | ] 668 | 669 | [[package]] 670 | name = "structopt-derive" 671 | version = "0.4.18" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 674 | dependencies = [ 675 | "heck", 676 | "proc-macro-error", 677 | "proc-macro2", 678 | "quote", 679 | "syn 1.0.109", 680 | ] 681 | 682 | [[package]] 683 | name = "syn" 684 | version = "1.0.109" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 687 | dependencies = [ 688 | "proc-macro2", 689 | "quote", 690 | "unicode-ident", 691 | ] 692 | 693 | [[package]] 694 | name = "syn" 695 | version = "2.0.96" 696 | source = "registry+https://github.com/rust-lang/crates.io-index" 697 | checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" 698 | dependencies = [ 699 | "proc-macro2", 700 | "quote", 701 | "unicode-ident", 702 | ] 703 | 704 | [[package]] 705 | name = "tempfile" 706 | version = "3.20.0" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 709 | dependencies = [ 710 | "fastrand", 711 | "getrandom 0.3.0", 712 | "once_cell", 713 | "rustix", 714 | "windows-sys 0.59.0", 715 | ] 716 | 717 | [[package]] 718 | name = "term" 719 | version = "1.0.2" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "8a984c8d058c627faaf5e8e2ed493fa3c51771889196de1016cf9c1c6e90d750" 722 | dependencies = [ 723 | "home", 724 | "windows-sys 0.59.0", 725 | ] 726 | 727 | [[package]] 728 | name = "termios" 729 | version = "0.3.3" 730 | source = "registry+https://github.com/rust-lang/crates.io-index" 731 | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" 732 | dependencies = [ 733 | "libc", 734 | ] 735 | 736 | [[package]] 737 | name = "textwrap" 738 | version = "0.11.0" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 741 | dependencies = [ 742 | "unicode-width 0.1.14", 743 | ] 744 | 745 | [[package]] 746 | name = "thiserror" 747 | version = "2.0.11" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 750 | dependencies = [ 751 | "thiserror-impl", 752 | ] 753 | 754 | [[package]] 755 | name = "thiserror-impl" 756 | version = "2.0.11" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 759 | dependencies = [ 760 | "proc-macro2", 761 | "quote", 762 | "syn 2.0.96", 763 | ] 764 | 765 | [[package]] 766 | name = "time" 767 | version = "0.3.41" 768 | source = "registry+https://github.com/rust-lang/crates.io-index" 769 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 770 | dependencies = [ 771 | "deranged", 772 | "num-conv", 773 | "powerfmt", 774 | "serde", 775 | "time-core", 776 | ] 777 | 778 | [[package]] 779 | name = "time-core" 780 | version = "0.1.4" 781 | source = "registry+https://github.com/rust-lang/crates.io-index" 782 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 783 | 784 | [[package]] 785 | name = "toml" 786 | version = "0.8.22" 787 | source = "registry+https://github.com/rust-lang/crates.io-index" 788 | checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" 789 | dependencies = [ 790 | "serde", 791 | "serde_spanned", 792 | "toml_datetime", 793 | "toml_edit", 794 | ] 795 | 796 | [[package]] 797 | name = "toml_datetime" 798 | version = "0.6.9" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 801 | dependencies = [ 802 | "serde", 803 | ] 804 | 805 | [[package]] 806 | name = "toml_edit" 807 | version = "0.22.26" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" 810 | dependencies = [ 811 | "indexmap", 812 | "serde", 813 | "serde_spanned", 814 | "toml_datetime", 815 | "toml_write", 816 | "winnow", 817 | ] 818 | 819 | [[package]] 820 | name = "toml_write" 821 | version = "0.1.1" 822 | source = "registry+https://github.com/rust-lang/crates.io-index" 823 | checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" 824 | 825 | [[package]] 826 | name = "unicode-ident" 827 | version = "1.0.12" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 830 | 831 | [[package]] 832 | name = "unicode-segmentation" 833 | version = "1.10.1" 834 | source = "registry+https://github.com/rust-lang/crates.io-index" 835 | checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" 836 | 837 | [[package]] 838 | name = "unicode-width" 839 | version = "0.1.14" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 842 | 843 | [[package]] 844 | name = "unicode-width" 845 | version = "0.2.0" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 848 | 849 | [[package]] 850 | name = "vec_map" 851 | version = "0.8.2" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 854 | 855 | [[package]] 856 | name = "version_check" 857 | version = "0.9.4" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 860 | 861 | [[package]] 862 | name = "walkdir" 863 | version = "2.5.0" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 866 | dependencies = [ 867 | "same-file", 868 | "winapi-util", 869 | ] 870 | 871 | [[package]] 872 | name = "wasi" 873 | version = "0.11.0+wasi-snapshot-preview1" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 876 | 877 | [[package]] 878 | name = "wasi" 879 | version = "0.13.3+wasi-0.2.2" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 882 | dependencies = [ 883 | "wit-bindgen-rt", 884 | ] 885 | 886 | [[package]] 887 | name = "winapi" 888 | version = "0.3.9" 889 | source = "registry+https://github.com/rust-lang/crates.io-index" 890 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 891 | dependencies = [ 892 | "winapi-i686-pc-windows-gnu", 893 | "winapi-x86_64-pc-windows-gnu", 894 | ] 895 | 896 | [[package]] 897 | name = "winapi-i686-pc-windows-gnu" 898 | version = "0.4.0" 899 | source = "registry+https://github.com/rust-lang/crates.io-index" 900 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 901 | 902 | [[package]] 903 | name = "winapi-util" 904 | version = "0.1.8" 905 | source = "registry+https://github.com/rust-lang/crates.io-index" 906 | checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" 907 | dependencies = [ 908 | "windows-sys 0.52.0", 909 | ] 910 | 911 | [[package]] 912 | name = "winapi-x86_64-pc-windows-gnu" 913 | version = "0.4.0" 914 | source = "registry+https://github.com/rust-lang/crates.io-index" 915 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 916 | 917 | [[package]] 918 | name = "windows-sys" 919 | version = "0.52.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 922 | dependencies = [ 923 | "windows-targets", 924 | ] 925 | 926 | [[package]] 927 | name = "windows-sys" 928 | version = "0.59.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 931 | dependencies = [ 932 | "windows-targets", 933 | ] 934 | 935 | [[package]] 936 | name = "windows-targets" 937 | version = "0.52.6" 938 | source = "registry+https://github.com/rust-lang/crates.io-index" 939 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 940 | dependencies = [ 941 | "windows_aarch64_gnullvm", 942 | "windows_aarch64_msvc", 943 | "windows_i686_gnu", 944 | "windows_i686_gnullvm", 945 | "windows_i686_msvc", 946 | "windows_x86_64_gnu", 947 | "windows_x86_64_gnullvm", 948 | "windows_x86_64_msvc", 949 | ] 950 | 951 | [[package]] 952 | name = "windows_aarch64_gnullvm" 953 | version = "0.52.6" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 956 | 957 | [[package]] 958 | name = "windows_aarch64_msvc" 959 | version = "0.52.6" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 962 | 963 | [[package]] 964 | name = "windows_i686_gnu" 965 | version = "0.52.6" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 968 | 969 | [[package]] 970 | name = "windows_i686_gnullvm" 971 | version = "0.52.6" 972 | source = "registry+https://github.com/rust-lang/crates.io-index" 973 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 974 | 975 | [[package]] 976 | name = "windows_i686_msvc" 977 | version = "0.52.6" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 980 | 981 | [[package]] 982 | name = "windows_x86_64_gnu" 983 | version = "0.52.6" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 986 | 987 | [[package]] 988 | name = "windows_x86_64_gnullvm" 989 | version = "0.52.6" 990 | source = "registry+https://github.com/rust-lang/crates.io-index" 991 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 992 | 993 | [[package]] 994 | name = "windows_x86_64_msvc" 995 | version = "0.52.6" 996 | source = "registry+https://github.com/rust-lang/crates.io-index" 997 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 998 | 999 | [[package]] 1000 | name = "winnow" 1001 | version = "0.7.7" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "6cb8234a863ea0e8cd7284fcdd4f145233eb00fee02bbdd9861aec44e6477bc5" 1004 | dependencies = [ 1005 | "memchr", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "wit-bindgen-rt" 1010 | version = "0.33.0" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 1013 | dependencies = [ 1014 | "bitflags 2.4.1", 1015 | ] 1016 | 1017 | [[package]] 1018 | name = "zerocopy" 1019 | version = "0.8.14" 1020 | source = "registry+https://github.com/rust-lang/crates.io-index" 1021 | checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" 1022 | dependencies = [ 1023 | "zerocopy-derive", 1024 | ] 1025 | 1026 | [[package]] 1027 | name = "zerocopy-derive" 1028 | version = "0.8.14" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" 1031 | dependencies = [ 1032 | "proc-macro2", 1033 | "quote", 1034 | "syn 2.0.96", 1035 | ] 1036 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "amber" 3 | version = "0.6.0" 4 | authors = ["dalance@gmail.com"] 5 | repository = "https://github.com/dalance/amber" 6 | keywords = ["search", "replace"] 7 | categories = ["command-line-utilities", "development-tools"] 8 | license = "MIT" 9 | readme = "README.md" 10 | description = "A code search and replace tool" 11 | edition = "2021" 12 | 13 | [badges] 14 | travis-ci = { repository = "dalance/amber" } 15 | appveyor = { repository = "dalance/amber", branch = "master", service = "github" } 16 | codecov = { repository = "dalance/amber", branch = "master", service = "github" } 17 | 18 | [[bin]] 19 | name = "ambr" 20 | path = "src/ambr.rs" 21 | 22 | [[bin]] 23 | name = "ambs" 24 | path = "src/ambs.rs" 25 | 26 | [features] 27 | sse = [] 28 | statistics = [] 29 | 30 | [dependencies] 31 | crossbeam = "0.8" 32 | ctrlc = "3" 33 | directories = "6.0.0" 34 | filetime = "0.2" 35 | getch = "0.3" 36 | glob = "0.3" 37 | ignore = "0.4" 38 | lazy_static = "1" 39 | memmap = "0.7" 40 | num_cpus = "1" 41 | regex = "1" 42 | rand = "0.9" 43 | rlibc = "1" 44 | serde = {version = "1.0", features = ["derive"]} 45 | structopt = "0.3" 46 | tempfile = "3" 47 | term = "1.0" 48 | time = "0.3" 49 | toml = "0.8" 50 | unicode-width = "0.2" 51 | 52 | [target.'cfg(not(target_os = "windows"))'.dependencies] 53 | termios = "0.3" 54 | 55 | [profile.dev] 56 | opt-level = 0 57 | debug = true 58 | rpath = false 59 | lto = false 60 | debug-assertions = true 61 | codegen-units = 1 62 | 63 | [profile.release] 64 | opt-level = 3 65 | debug = false 66 | rpath = false 67 | lto = true 68 | debug-assertions = false 69 | codegen-units = 1 70 | 71 | [profile.test] 72 | opt-level = 3 73 | debug = false 74 | rpath = false 75 | lto = false 76 | debug-assertions = false 77 | codegen-units = 1 78 | 79 | [package.metadata.release] 80 | pre-release-commit-message = "Prepare to v{{version}}" 81 | post-release-commit-message = "Start next development iteration v{{version}}" 82 | tag-message = "Bump version to {{version}}" 83 | tag-prefix = "" 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 dalance 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = $(patsubst "%",%, $(word 3, $(shell grep version Cargo.toml))) 2 | BUILD_TIME = $(shell date +"%Y/%m/%d %H:%M:%S") 3 | GIT_REVISION = $(shell git log -1 --format="%h") 4 | RUST_VERSION = $(word 2, $(shell rustc -V)) 5 | LONG_VERSION = "$(VERSION) ( rev: $(GIT_REVISION), rustc: $(RUST_VERSION), build at: $(BUILD_TIME) )" 6 | BIN_NAME = amber 7 | 8 | export LONG_VERSION 9 | 10 | .PHONY: all test bench bench_sse clean release_lnx64 release_win64 release_osx64 11 | 12 | all: test 13 | 14 | test: 15 | cargo test 16 | 17 | bench: 18 | cargo bench 19 | 20 | bench_sse: 21 | cargo bench --features 'sse' 22 | 23 | build_statistics: 24 | cargo build --release --features 'statistics' 25 | 26 | clean: 27 | cargo clean 28 | 29 | release_lnx: 30 | cargo build --release --target=x86_64-unknown-linux-musl 31 | zip -j ${BIN_NAME}-v${VERSION}-x86_64-lnx.zip target/x86_64-unknown-linux-musl/release/amb? 32 | 33 | release_win: 34 | cargo build --release --target=x86_64-pc-windows-msvc 35 | 7z a ${BIN_NAME}-v${VERSION}-x86_64-win.zip target/x86_64-pc-windows-msvc/release/amb?.exe 36 | 37 | release_mac: 38 | cargo build --release --target=x86_64-apple-darwin 39 | zip -j ${BIN_NAME}-v${VERSION}-x86_64-mac.zip target/x86_64-apple-darwin/release/amb? 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # amber 2 | 3 | [![Actions Status](https://github.com/dalance/amber/workflows/Regression/badge.svg)](https://github.com/dalance/amber/actions) 4 | [![Crates.io](https://img.shields.io/crates/v/amber.svg)](https://crates.io/crates/amber) 5 | 6 | **amber** is a code search and replace tool written by [Rust](https://www.rust-lang.org/). 7 | This tool is inspired by [ack](http://beyondgrep.com/), 8 | [ag](https://github.com/ggreer/the_silver_searcher), and other grep-like tools. 9 | 10 | ## Features 11 | 12 | ### Useful default settings 13 | - Recursively search from the current directory 14 | - Ignore VCS directories (.git, .hg, .svn, .bzr) 15 | - Ignore binary files 16 | - Output by the colored format 17 | 18 | ### Multi-threaded searching 19 | Large files ( > 1MB by default) are divided and searched in parallel. 20 | 21 | ### Interactive replacing 22 | **amber** can replace a keyword over directories (traditionally by `find ... | xargs sed -i '...'`) . 23 | You can decide to do replacing or not interactively. 24 | 25 | ## Installation 26 | 27 | ### Arch Linux 28 | Install the `amber-search-git` package from AUR. 29 | 30 | ``` 31 | yay -S amber-search-git 32 | ``` 33 | 34 | ### Cargo 35 | 36 | You can install with [cargo](https://crates.io/crates/amber). 37 | 38 | ``` 39 | cargo install amber 40 | ``` 41 | 42 | ### Manual 43 | Download from [release page](https://github.com/dalance/amber/releases/latest), and extract to the directory in PATH. 44 | 45 | ## Usage 46 | Two commands (`ambs`/`ambr`) are provided. `ambs` means "amber search", and `ambr` means "amber replace". 47 | The search keyword is not regular expression by default. If you want to use regular expression, add `--regex`. 48 | 49 | ``` 50 | ambs keyword // recursively search 'keyword' from the current directory. 51 | ambs keyword path // recursively search 'keyword' from 'path'. 52 | ambr keyword replacement // recursively search 'keyword' from the current directory, and replace to 'replacement' interactively. 53 | ambr keyword replacement path // recursively search 'keyword' from 'path', and replace to 'replacement' interactively. 54 | ``` 55 | 56 | **amber** replace interactively by default. If the keyword is found, the following prompt is shown, and wait. 57 | If you input 'y', 'Y', 'Yes', the keyword is replaced. 'a', 'A', 'All' means replacing all keywords non-interactively. 58 | 59 | ``` 60 | Replace keyword? ( Yes[Y], No[N], All[A], Quit[Q] ): 61 | ``` 62 | 63 | If `--regex` option is enabled, regex captures can be used in `replacement` of `ambr`. 64 | 65 | ``` 66 | $ cat text.txt 67 | aaa bbb 68 | $ ambr --no-interactive --regex '(aaa) (?bbb)' '$1 $pat ${1} ${pat}' test.txt 69 | $ cat text.txt 70 | aaa bbb aaa bbb 71 | ``` 72 | 73 | ## Configuration 74 | 75 | ### Configuration path 76 | 77 | You can change configuration by writing a configuration file. 78 | The locations of the configuration file is OS-specific: 79 | 80 | * Linux: `~/.config/amber/ambs.toml`, `/etc/amber/ambs.toml` 81 | * macOS: `~/Library/Preferences/com.github.dalance.amber/ambs.toml`, `/etc/amber/ambs.toml` 82 | * Windows: `~/AppData/Roaming/dalance/amber/config/ambs.toml` 83 | 84 | For compatibility, if `~/.ambs.toml` exists, it will be preferred to 85 | the OS-specific locations. 86 | 87 | The above paths are examples for the configuration of `ambs` command. 88 | `ambr.toml` in the same directory is used for `ambr` command. 89 | 90 | ### Configurable value 91 | 92 | Available entries and default values are below: 93 | 94 | ```toml 95 | regex = false 96 | column = false 97 | row = false 98 | binary = false 99 | statistics = false 100 | skipped = false 101 | interactive = true 102 | recursive = true 103 | symlink = true 104 | color = true 105 | file = true 106 | skip_vcs = true 107 | skip_gitignore = true 108 | fixed_order = true 109 | parent_ignore = true 110 | line_by_match = false 111 | ``` 112 | 113 | You can choose some entries to override like below: 114 | 115 | ```toml 116 | column = true 117 | ``` 118 | 119 | ## Benchmark 120 | 121 | ### Environment 122 | 123 | - CPU: Intel(R) Xeon(R) Gold 6134 CPU @ 3.20GHz 124 | - MEM: 1.5TB 125 | - OS : CentOS 7.5 126 | 127 | ### Target Data 128 | 129 | - source1: https://github.com/torvalds/linux ( 52998files, 2.2GB ) 130 | - source2: https://dumps.wikimedia.org/jawiki/latest/jawiki-latest-pages-articles.xml.bz2 ( 1file, 8.5GB ) 131 | 132 | ### Pattern 133 | 134 | - pattern1( many files with many matches ) : 'EXPORT_SYMBOL_GPL' in source1 135 | - pattern2( many files with few matches ) : 'irq_bypass_register_producer' in source1 136 | - pattern3( a large file with many matches ) : '検索結果' in source2 137 | - pattern4( a large file with few matches ) : '"Quick Search"' in source2 138 | 139 | ### Comparison Tools 140 | 141 | - amber (v0.5.1) 142 | - [ripgrep](https://github.com/BurntSushi/ripgrep) (v0.10.0) 143 | - [grep](https://www.gnu.org/software/grep/) (v2.20) 144 | - [fastmod](https://github.com/facebookincubator/fastmod) (v0.2.0) 145 | - [find](https://www.gnu.org/software/findutils/)/[sed](https://www.gnu.org/software/sed/) (v4.5.11/v4.2.2) 146 | 147 | ### Benchmarking Tool 148 | 149 | [hyperfine](https://github.com/sharkdp/hyperfine) with the following options. 150 | 151 | - `--warmup 3`: to load all data on memory. 152 | 153 | ### Result 154 | 155 | - search ( `compare_ambs.sh` ) 156 | 157 | | pattern | amber | ripgrep | grep | 158 | | ------- | ---------------- | ---------------- | ---------------- | 159 | | 1 | 212.8ms ( 139% ) | 154.1ms ( 100% ) | 685.2ms ( 448% ) | 160 | | 2 | 199.7ms ( 132% ) | 151.6ms ( 100% ) | 678.7ms ( 448% ) | 161 | | 3 | 1.068s ( 100% ) | 4.642s ( 434% ) | 3.869s ( 362% ) | 162 | | 4 | 1.027s ( 100% ) | 4.409s ( 429% ) | 3.118s ( 304% ) | 163 | 164 | - replace ( `compare_ambr.sh` ) 165 | 166 | | pattern | amber | fastmod | find/sed | 167 | | ------- | ---------------- | ---------------- | ------------------- | 168 | | 1 | 792.2ms ( 100% ) | 1231ms ( 155% ) | 155724ms ( 19657% ) | 169 | | 2 | 418.1ms ( 119% ) | 352.4ms ( 100% ) | 157396ms ( 44663% ) | 170 | | 3 | 18.390s ( 100% ) | 74.282s ( 404% ) | 639.740s ( 3479% ) | 171 | | 4 | 17.777s ( 100% ) | 74.204s ( 417% ) | 625.756s ( 3520% ) | 172 | -------------------------------------------------------------------------------- /benches/matcher.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate amber; 4 | extern crate rand; 5 | extern crate test; 6 | 7 | use amber::matcher::{BruteForceMatcher, FjsMatcher, Matcher, QuickSearchMatcher, TbmMatcher}; 8 | use rand::rngs::StdRng; 9 | use rand::{Rng, SeedableRng}; 10 | use test::Bencher; 11 | 12 | // --------------------------------------------------------------------------------------------------------------------- 13 | // Utility 14 | // --------------------------------------------------------------------------------------------------------------------- 15 | 16 | fn make_src() -> Box<[u8]> { 17 | let seed: [u8; 32] = [1; 32]; 18 | let mut rng: StdRng = SeedableRng::from_seed(seed); 19 | 20 | const SRC_LEN: usize = 1024 * 1024 * 4; 21 | let mut src = Box::new([0u8; SRC_LEN]); 22 | for i in 0..SRC_LEN { 23 | src[i] = rng.gen(); 24 | } 25 | src 26 | } 27 | 28 | fn make_pat(src: &[u8]) -> Box<[u8]> { 29 | let seed: [u8; 32] = [1; 32]; 30 | let mut rng: StdRng = SeedableRng::from_seed(seed); 31 | 32 | const PAT_LEN: usize = 16; 33 | let src_len = src.len(); 34 | let mut pat = Box::new([0u8; PAT_LEN]); 35 | let pos = rng.gen::() % (src_len - PAT_LEN - 1); 36 | for i in 0..PAT_LEN { 37 | pat[i] = src[i + pos]; 38 | } 39 | pat 40 | } 41 | 42 | fn bench(b: &mut Bencher, m: &dyn Matcher) { 43 | let src = make_src(); 44 | let pat = make_pat(&src); 45 | 46 | b.iter(|| { 47 | let ret = m.search(&*src, &*pat); 48 | assert!(ret.len() > 0); 49 | }); 50 | } 51 | 52 | // --------------------------------------------------------------------------------------------------------------------- 53 | // Normal 54 | // --------------------------------------------------------------------------------------------------------------------- 55 | 56 | #[bench] 57 | fn normal_brute_force(b: &mut Bencher) { 58 | let m = BruteForceMatcher::new(); 59 | bench(b, &m); 60 | } 61 | 62 | #[bench] 63 | fn normal_quick_search(b: &mut Bencher) { 64 | let mut m = QuickSearchMatcher::new(); 65 | m.max_threads = 1; 66 | bench(b, &m); 67 | } 68 | 69 | #[bench] 70 | fn normal_tbm(b: &mut Bencher) { 71 | let mut m = TbmMatcher::new(); 72 | m.max_threads = 1; 73 | bench(b, &m); 74 | } 75 | 76 | #[bench] 77 | fn normal_fjs(b: &mut Bencher) { 78 | let mut m = FjsMatcher::new(); 79 | m.max_threads = 1; 80 | bench(b, &m); 81 | } 82 | 83 | // --------------------------------------------------------------------------------------------------------------------- 84 | // Multithread 85 | // --------------------------------------------------------------------------------------------------------------------- 86 | 87 | #[bench] 88 | fn thread2_quick_search(b: &mut Bencher) { 89 | let mut m = QuickSearchMatcher::new(); 90 | m.max_threads = 2; 91 | bench(b, &m); 92 | } 93 | 94 | #[bench] 95 | fn thread2_tbm(b: &mut Bencher) { 96 | let mut m = TbmMatcher::new(); 97 | m.max_threads = 2; 98 | bench(b, &m); 99 | } 100 | 101 | #[bench] 102 | fn thread2_fjs(b: &mut Bencher) { 103 | let mut m = FjsMatcher::new(); 104 | m.max_threads = 2; 105 | bench(b, &m); 106 | } 107 | 108 | #[bench] 109 | fn thread4_quick_search(b: &mut Bencher) { 110 | let mut m = QuickSearchMatcher::new(); 111 | m.max_threads = 4; 112 | bench(b, &m); 113 | } 114 | 115 | #[bench] 116 | fn thread4_tbm(b: &mut Bencher) { 117 | let mut m = TbmMatcher::new(); 118 | m.max_threads = 4; 119 | bench(b, &m); 120 | } 121 | 122 | #[bench] 123 | fn thread4_fjs(b: &mut Bencher) { 124 | let mut m = FjsMatcher::new(); 125 | m.max_threads = 4; 126 | bench(b, &m); 127 | } 128 | 129 | #[bench] 130 | fn thread8_quick_search(b: &mut Bencher) { 131 | let mut m = QuickSearchMatcher::new(); 132 | m.max_threads = 8; 133 | bench(b, &m); 134 | } 135 | 136 | #[bench] 137 | fn thread8_tbm(b: &mut Bencher) { 138 | let mut m = TbmMatcher::new(); 139 | m.max_threads = 8; 140 | bench(b, &m); 141 | } 142 | 143 | #[bench] 144 | fn thread8_fjs(b: &mut Bencher) { 145 | let mut m = FjsMatcher::new(); 146 | m.max_threads = 8; 147 | bench(b, &m); 148 | } 149 | 150 | // --------------------------------------------------------------------------------------------------------------------- 151 | // SSE 152 | // --------------------------------------------------------------------------------------------------------------------- 153 | 154 | #[cfg(feature = "sse")] 155 | #[bench] 156 | fn sse_thread1_quick_search(b: &mut Bencher) { 157 | let mut m = QuickSearchMatcher::new(); 158 | m.max_threads = 1; 159 | m.use_sse = true; 160 | bench(b, &m); 161 | } 162 | 163 | #[cfg(feature = "sse")] 164 | #[bench] 165 | fn sse_thread1_tbm(b: &mut Bencher) { 166 | let mut m = TbmMatcher::new(); 167 | m.max_threads = 1; 168 | m.use_sse = true; 169 | bench(b, &m); 170 | } 171 | 172 | #[cfg(feature = "sse")] 173 | #[bench] 174 | fn sse_thread2_quick_search(b: &mut Bencher) { 175 | let mut m = QuickSearchMatcher::new(); 176 | m.max_threads = 2; 177 | m.use_sse = true; 178 | bench(b, &m); 179 | } 180 | 181 | #[cfg(feature = "sse")] 182 | #[bench] 183 | fn sse_thread2_tbm(b: &mut Bencher) { 184 | let mut m = TbmMatcher::new(); 185 | m.max_threads = 2; 186 | m.use_sse = true; 187 | bench(b, &m); 188 | } 189 | 190 | #[cfg(feature = "sse")] 191 | #[bench] 192 | fn sse_thread4_quick_search(b: &mut Bencher) { 193 | let mut m = QuickSearchMatcher::new(); 194 | m.max_threads = 4; 195 | m.use_sse = true; 196 | bench(b, &m); 197 | } 198 | 199 | #[cfg(feature = "sse")] 200 | #[bench] 201 | fn sse_thread4_tbm(b: &mut Bencher) { 202 | let mut m = TbmMatcher::new(); 203 | m.max_threads = 4; 204 | m.use_sse = true; 205 | bench(b, &m); 206 | } 207 | 208 | #[cfg(feature = "sse")] 209 | #[bench] 210 | fn sse_thread8_quick_search(b: &mut Bencher) { 211 | let mut m = QuickSearchMatcher::new(); 212 | m.max_threads = 8; 213 | m.use_sse = true; 214 | bench(b, &m); 215 | } 216 | 217 | #[cfg(feature = "sse")] 218 | #[bench] 219 | fn sse_thread8_tbm(b: &mut Bencher) { 220 | let mut m = TbmMatcher::new(); 221 | m.max_threads = 8; 222 | m.use_sse = true; 223 | bench(b, &m); 224 | } 225 | -------------------------------------------------------------------------------- /benches/path_finder.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate amber; 4 | extern crate test; 5 | 6 | //use amber::path_finder::{PathFinder, SimplePathFinder}; 7 | use std::path::PathBuf; 8 | use test::Bencher; 9 | 10 | // --------------------------------------------------------------------------------------------------------------------- 11 | // Benchmark 12 | // --------------------------------------------------------------------------------------------------------------------- 13 | 14 | //#[bench] 15 | //fn bench_simple_path_finder( b: &mut Bencher ) { 16 | // b.iter( || { 17 | // let mut finder = SimplePathFinder::new(); 18 | // finder.find( vec![PathBuf::from( "/usr/share" )] ); 19 | // } ); 20 | //} 21 | -------------------------------------------------------------------------------- /compare_ambr.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dev="./target/release/ambr --no-interactive" 4 | ambr="ambr --no-interactive" 5 | fastmod="fastmod --accept-all" 6 | 7 | hyperfine --warmup 3 "$dev EXPORT_SYMBOL_GPL EXPORT_SYMBOL_GPL2 ./data/linux; $dev EXPORT_SYMBOL_GPL2 EXPORT_SYMBOL_GPL ./data/linux" \ 8 | "$ambr EXPORT_SYMBOL_GPL EXPORT_SYMBOL_GPL2 ./data/linux; $ambr EXPORT_SYMBOL_GPL2 EXPORT_SYMBOL_GPL ./data/linux" \ 9 | "$fastmod EXPORT_SYMBOL_GPL EXPORT_SYMBOL_GPL2 ./data/linux; $fastmod EXPORT_SYMBOL_GPL2 EXPORT_SYMBOL_GPL ./data/linux" \ 10 | "find ./data/linux -type f | xargs sed -i 's/EXPORT_SYMBOL_GPL/EXPORT_SYMBOL_GPL2/g'; find ./data/linux -type f | xargs sed -i 's/EXPORT_SYMBOL_GPL2/EXPORT_SYMBOL_GPL/g'" 11 | hyperfine --warmup 3 "$dev irq_bypass_register_producer irq_bypass_register_producer2 ./data/linux; $dev irq_bypass_register_producer2 irq_bypass_register_producer ./data/linux" \ 12 | "$ambr irq_bypass_register_producer irq_bypass_register_producer2 ./data/linux; $ambr irq_bypass_register_producer2 irq_bypass_register_producer ./data/linux" \ 13 | "$fastmod irq_bypass_register_producer irq_bypass_register_producer2 ./data/linux; $fastmod irq_bypass_register_producer2 irq_bypass_register_producer ./data/linux" \ 14 | "find ./data/linux -type f | xargs sed -i 's/irq_bypass_register_producer/irq_bypass_register_producer2/g'; find ./data/linux -type f | xargs sed -i 's/irq_bypass_register_producer2/irq_bypass_register_producer/g'" 15 | hyperfine --warmup 3 "$dev 検索結果 検索結果2 ./data/jawiki-latest-pages-articles.xml; $dev 検索結果2 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 16 | "$ambr 検索結果 検索結果2 ./data/jawiki-latest-pages-articles.xml; $ambr 検索結果2 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 17 | "$fastmod 検索結果 検索結果2 ./data/jawiki-latest-pages-articles.xml; $fastmod 検索結果2 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 18 | "find ./data/jawiki-latest-pages-articles.xml -type f | xargs sed -i 's/検索結果/検索結果2/g'; find ./data/jawiki-latest-pages-articles.xml -type f | xargs sed -i 's/検索結果2/検索結果/g'" 19 | hyperfine --warmup 3 "$dev \"Quick Search\" \"Quick Search2\" ./data/jawiki-latest-pages-articles.xml; $dev \"Quick Search2\" \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 20 | "$ambr \"Quick Search\" \"Quick Search2\" ./data/jawiki-latest-pages-articles.xml; $ambr \"Quick Search2\" \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 21 | "$fastmod \"Quick Search\" \"Quick Search2\" ./data/jawiki-latest-pages-articles.xml; $fastmod \"Quick Search2\" \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 22 | "find ./data/jawiki-latest-pages-articles.xml -type f | xargs sed -i 's/\"Quick Search\"/\"Quick Search2\"/g'; find ./data/jawiki-latest-pages-articles.xml -type f | xargs sed -i 's/\"Quick Search2\"/\"Quick Search\"/g'" 23 | -------------------------------------------------------------------------------- /compare_ambs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | dev="./target/release/ambs --no-parent-ignore" 4 | grep="grep --binary-files=without-match --color=auto -r" 5 | ambs="ambs --no-parent-ignore" 6 | rg="rg --no-heading --no-line-number" 7 | 8 | hyperfine --warmup 3 "$dev EXPORT_SYMBOL_GPL ./data/linux" \ 9 | "$ambs EXPORT_SYMBOL_GPL ./data/linux" \ 10 | "$rg EXPORT_SYMBOL_GPL ./data/linux" \ 11 | "$grep EXPORT_SYMBOL_GPL ./data/linux" 12 | hyperfine --warmup 3 "$dev irq_bypass_register_producer ./data/linux" \ 13 | "$ambs irq_bypass_register_producer ./data/linux" \ 14 | "$rg irq_bypass_register_producer ./data/linux" \ 15 | "$grep irq_bypass_register_producer ./data/linux" 16 | hyperfine --warmup 3 "$dev 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 17 | "$ambs 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 18 | "$rg 検索結果 ./data/jawiki-latest-pages-articles.xml" \ 19 | "$grep 検索結果 ./data/jawiki-latest-pages-articles.xml" 20 | hyperfine --warmup 3 "$dev \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 21 | "$ambs \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 22 | "$rg \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" \ 23 | "$grep \"Quick Search\" ./data/jawiki-latest-pages-articles.xml" 24 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /src/ambr.rs: -------------------------------------------------------------------------------- 1 | use amber::console::{Console, ConsoleTextKind}; 2 | use amber::matcher::{QuickSearchMatcher, RegexMatcher, TbmMatcher}; 3 | use amber::pipeline::{Pipeline, PipelineFork, PipelineInfo, PipelineJoin}; 4 | use amber::pipeline_finder::PipelineFinder; 5 | use amber::pipeline_matcher::PipelineMatcher; 6 | use amber::pipeline_replacer::PipelineReplacer; 7 | use amber::pipeline_sorter::PipelineSorter; 8 | use amber::util::{as_secsf64, decode_error, exit, get_config, handle_escape, read_from_file}; 9 | use crossbeam::channel::unbounded; 10 | use lazy_static::lazy_static; 11 | use serde::Deserialize; 12 | use std::cmp; 13 | use std::fs; 14 | use std::io::Read; 15 | use std::path::PathBuf; 16 | use std::thread; 17 | use std::time::Duration; 18 | use structopt::{clap, StructOpt}; 19 | 20 | // --------------------------------------------------------------------------------------------------------------------- 21 | // Opt 22 | // --------------------------------------------------------------------------------------------------------------------- 23 | 24 | #[derive(Debug, StructOpt)] 25 | #[structopt(name = "ambr")] 26 | #[structopt(long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))))] 27 | #[structopt(setting(clap::AppSettings::ColoredHelp))] 28 | #[structopt(setting(clap::AppSettings::DeriveDisplayOrder))] 29 | pub struct Opt { 30 | /// Keyword for search 31 | #[structopt(name = "KEYWORD")] 32 | pub keyword: String, 33 | 34 | /// Keyword for replace 35 | #[structopt(name = "REPLACEMENT")] 36 | pub replacement: String, 37 | 38 | /// Use file contents of KEYWORD as keyword for search 39 | #[structopt(long = "key-from-file")] 40 | pub key_from_file: bool, 41 | 42 | /// Use file contents of REPLACEMENT as keyword for replacement 43 | #[structopt(long = "rep-from-file")] 44 | pub rep_from_file: bool, 45 | 46 | /// Search paths 47 | #[structopt(name = "PATHS")] 48 | pub paths: Vec, 49 | 50 | /// Number of max threads 51 | #[structopt(long = "max-threads", default_value = &MAX_THREADS, value_name = "NUM")] 52 | pub max_threads: usize, 53 | 54 | /// File size per one thread 55 | #[structopt(long = "size-per-thread", default_value = "1048576", value_name = "BYTES")] 56 | pub size_per_thread: usize, 57 | 58 | /// Read size for checking binary 59 | #[structopt(long = "bin-check-bytes", default_value = "256", value_name = "BYTES")] 60 | pub bin_check_bytes: usize, 61 | 62 | /// [Experimental] Minimum size for using mmap 63 | #[structopt(long = "mmap-bytes", default_value = "1048576", value_name = "BYTES")] 64 | pub mmap_bytes: u64, 65 | 66 | /// Verbose message 67 | #[structopt(long = "verbose")] 68 | pub verbose: bool, 69 | 70 | /// Enable regular expression search 71 | #[structopt(short = "r", long = "regex", hidden = DEFAULT_FLAGS.regex)] 72 | pub regex: bool, 73 | 74 | /// Enable column output 75 | #[structopt(long = "column", hidden = DEFAULT_FLAGS.column)] 76 | pub column: bool, 77 | 78 | /// Enable row output 79 | #[structopt(long = "row", hidden = DEFAULT_FLAGS.row)] 80 | pub row: bool, 81 | 82 | /// Enable binary file search 83 | #[structopt(long = "binary", hidden = DEFAULT_FLAGS.binary)] 84 | pub binary: bool, 85 | 86 | /// Enable statistics output 87 | #[structopt(long = "statistics", hidden = DEFAULT_FLAGS.statistics)] 88 | pub statistics: bool, 89 | 90 | /// Enable skipped file output 91 | #[structopt(long = "skipped", hidden = DEFAULT_FLAGS.skipped)] 92 | pub skipped: bool, 93 | 94 | /// Enable interactive replace 95 | #[structopt(long = "interactive", hidden = DEFAULT_FLAGS.interactive)] 96 | pub interactive: bool, 97 | 98 | /// Enable recursive directory search 99 | #[structopt(long = "recursive", hidden = DEFAULT_FLAGS.recursive)] 100 | pub recursive: bool, 101 | 102 | /// Enable symbolic link follow 103 | #[structopt(long = "symlink", hidden = DEFAULT_FLAGS.symlink)] 104 | pub symlink: bool, 105 | 106 | /// Enable colored output 107 | #[structopt(long = "color", hidden = DEFAULT_FLAGS.color)] 108 | pub color: bool, 109 | 110 | /// Enable filename output 111 | #[structopt(long = "file", hidden = DEFAULT_FLAGS.file)] 112 | pub file: bool, 113 | 114 | /// Enable vcs directory ( .hg/.git/.svn ) skip 115 | #[structopt(long = "skip-vcs", hidden = DEFAULT_FLAGS.skip_vcs)] 116 | pub skip_vcs: bool, 117 | 118 | /// Enable .gitignore skip 119 | #[structopt(long = "skip-gitignore", hidden = DEFAULT_FLAGS.skip_gitignore)] 120 | pub skip_gitignore: bool, 121 | 122 | /// Enable output order guarantee 123 | #[structopt(long = "fixed-order", hidden = DEFAULT_FLAGS.fixed_order)] 124 | pub fixed_order: bool, 125 | 126 | /// Enable .*ignore file search at parent directories 127 | #[structopt(long = "parent-ignore", hidden = DEFAULT_FLAGS.parent_ignore)] 128 | pub parent_ignore: bool, 129 | 130 | /// Enable timestamp preserve 131 | #[structopt(long = "preserve-time", hidden = DEFAULT_FLAGS.preserve_time)] 132 | pub preserve_time: bool, 133 | 134 | /// Disable regular expression search 135 | #[structopt(long = "no-regex", hidden = !DEFAULT_FLAGS.regex)] 136 | pub no_regex: bool, 137 | 138 | /// Disable column output 139 | #[structopt(long = "no-column", hidden = !DEFAULT_FLAGS.column)] 140 | pub no_column: bool, 141 | 142 | /// Disable row output 143 | #[structopt(long = "no-row", hidden = !DEFAULT_FLAGS.row)] 144 | pub no_row: bool, 145 | 146 | /// Disable binary file search 147 | #[structopt(long = "no-binary", hidden = !DEFAULT_FLAGS.binary)] 148 | pub no_binary: bool, 149 | 150 | /// Disable statistics output 151 | #[structopt(long = "no-statistics", hidden = !DEFAULT_FLAGS.statistics)] 152 | pub no_statistics: bool, 153 | 154 | /// Disable skipped file output 155 | #[structopt(long = "no-skipped", hidden = !DEFAULT_FLAGS.skipped)] 156 | pub no_skipped: bool, 157 | 158 | /// Disable interactive replace 159 | #[structopt(long = "no-interactive", hidden = !DEFAULT_FLAGS.interactive)] 160 | pub no_interactive: bool, 161 | 162 | /// Disable recursive directory search 163 | #[structopt(long = "no-recursive", hidden = !DEFAULT_FLAGS.recursive)] 164 | pub no_recursive: bool, 165 | 166 | /// Disable symbolic link follow 167 | #[structopt(long = "no-symlink", hidden = !DEFAULT_FLAGS.symlink)] 168 | pub no_symlink: bool, 169 | 170 | /// Disable colored output 171 | #[structopt(long = "no-color", hidden = !DEFAULT_FLAGS.color)] 172 | pub no_color: bool, 173 | 174 | /// Disable filename output 175 | #[structopt(long = "no-file", hidden = !DEFAULT_FLAGS.file)] 176 | pub no_file: bool, 177 | 178 | /// Disable vcs directory ( .hg/.git/.svn ) skip 179 | #[structopt(long = "no-skip-vcs", hidden = !DEFAULT_FLAGS.skip_vcs)] 180 | pub no_skip_vcs: bool, 181 | 182 | /// Disable .gitignore skip 183 | #[structopt(long = "no-skip-gitignore", hidden = !DEFAULT_FLAGS.skip_gitignore)] 184 | pub no_skip_gitignore: bool, 185 | 186 | /// Disable output order guarantee 187 | #[structopt(long = "no-fixed-order", hidden = !DEFAULT_FLAGS.fixed_order)] 188 | pub no_fixed_order: bool, 189 | 190 | /// Disable .*ignore file search at parent directories 191 | #[structopt(long = "no-parent-ignore", hidden = !DEFAULT_FLAGS.parent_ignore)] 192 | pub no_parent_ignore: bool, 193 | 194 | /// Disable timestamp preserve 195 | #[structopt(long = "no-preserve-time", hidden = !DEFAULT_FLAGS.preserve_time)] 196 | pub no_preserve_time: bool, 197 | 198 | /// [Experimental] Enable TBM matcher 199 | #[structopt(long = "tbm")] 200 | pub tbm: bool, 201 | 202 | /// [Experimental] Enable SSE 4.2 203 | #[structopt(long = "sse")] 204 | pub sse: bool, 205 | } 206 | 207 | #[derive(Debug, Deserialize)] 208 | struct DefaultFlags { 209 | #[serde(default = "flag_false")] 210 | regex: bool, 211 | #[serde(default = "flag_false")] 212 | column: bool, 213 | #[serde(default = "flag_false")] 214 | row: bool, 215 | #[serde(default = "flag_false")] 216 | binary: bool, 217 | #[serde(default = "flag_false")] 218 | statistics: bool, 219 | #[serde(default = "flag_false")] 220 | skipped: bool, 221 | #[serde(default = "flag_true")] 222 | interactive: bool, 223 | #[serde(default = "flag_true")] 224 | recursive: bool, 225 | #[serde(default = "flag_true")] 226 | symlink: bool, 227 | #[serde(default = "flag_true")] 228 | color: bool, 229 | #[serde(default = "flag_true")] 230 | file: bool, 231 | #[serde(default = "flag_true")] 232 | skip_vcs: bool, 233 | #[serde(default = "flag_true")] 234 | skip_gitignore: bool, 235 | #[serde(default = "flag_true")] 236 | fixed_order: bool, 237 | #[serde(default = "flag_true")] 238 | parent_ignore: bool, 239 | #[serde(default = "flag_false")] 240 | preserve_time: bool, 241 | } 242 | 243 | impl DefaultFlags { 244 | fn new() -> DefaultFlags { 245 | toml::from_str("").unwrap() 246 | } 247 | 248 | fn load() -> DefaultFlags { 249 | if let Some(path) = get_config("ambr.toml") { 250 | match fs::File::open(&path) { 251 | Ok(mut f) => { 252 | let mut s = String::new(); 253 | let _ = f.read_to_string(&mut s); 254 | match toml::from_str(&s) { 255 | Ok(x) => x, 256 | Err(_) => DefaultFlags::new(), 257 | } 258 | } 259 | Err(_) => DefaultFlags::new(), 260 | } 261 | } else { 262 | DefaultFlags::new() 263 | } 264 | } 265 | 266 | fn merge(&self, mut opt: Opt) -> Opt { 267 | opt.regex = if self.regex { !opt.no_regex } else { opt.regex }; 268 | opt.column = if self.column { !opt.no_column } else { opt.column }; 269 | opt.row = if self.row { !opt.no_row } else { opt.row }; 270 | opt.binary = if self.binary { !opt.no_binary } else { opt.binary }; 271 | opt.statistics = if self.statistics { 272 | !opt.no_statistics 273 | } else { 274 | opt.statistics 275 | }; 276 | opt.skipped = if self.skipped { !opt.no_skipped } else { opt.skipped }; 277 | opt.interactive = if self.interactive { 278 | !opt.no_interactive 279 | } else { 280 | opt.interactive 281 | }; 282 | opt.recursive = if self.recursive { 283 | !opt.no_recursive 284 | } else { 285 | opt.recursive 286 | }; 287 | opt.symlink = if self.symlink { !opt.no_symlink } else { opt.symlink }; 288 | opt.color = if self.color { !opt.no_color } else { opt.color }; 289 | opt.file = if self.file { !opt.no_file } else { opt.file }; 290 | opt.skip_vcs = if self.skip_vcs { !opt.no_skip_vcs } else { opt.skip_vcs }; 291 | opt.skip_gitignore = if self.skip_gitignore { 292 | !opt.no_skip_gitignore 293 | } else { 294 | opt.skip_gitignore 295 | }; 296 | opt.fixed_order = if self.fixed_order { 297 | !opt.no_fixed_order 298 | } else { 299 | opt.fixed_order 300 | }; 301 | opt.parent_ignore = if self.parent_ignore { 302 | !opt.no_parent_ignore 303 | } else { 304 | opt.parent_ignore 305 | }; 306 | opt.preserve_time = if self.preserve_time { 307 | !opt.no_preserve_time 308 | } else { 309 | opt.preserve_time 310 | }; 311 | opt 312 | } 313 | } 314 | 315 | fn flag_true() -> bool { 316 | true 317 | } 318 | fn flag_false() -> bool { 319 | false 320 | } 321 | 322 | lazy_static! { 323 | static ref MAX_THREADS: String = format!("{}", num_cpus::get()); 324 | static ref DEFAULT_FLAGS: DefaultFlags = DefaultFlags::load(); 325 | } 326 | 327 | // --------------------------------------------------------------------------------------------------------------------- 328 | // Main 329 | // --------------------------------------------------------------------------------------------------------------------- 330 | 331 | fn main() { 332 | // --------------------------------------------------------------------------------------------- 333 | // Parse Arguments 334 | // --------------------------------------------------------------------------------------------- 335 | 336 | // - Create opt ------------------------------------------------------------ 337 | 338 | let opt = Opt::from_args(); 339 | let opt = DEFAULT_FLAGS.merge(opt); 340 | 341 | let mut console = Console::new(); 342 | console.is_color = opt.color; 343 | 344 | // - Set base path, keyword and replacement -------------------------------- 345 | let mut base_paths: Vec = Vec::new(); 346 | if opt.paths.is_empty() { 347 | base_paths.push(PathBuf::from("./")); 348 | } else { 349 | for p in &opt.paths { 350 | base_paths.push(PathBuf::from(p)); 351 | } 352 | } 353 | 354 | let keyword = if opt.key_from_file { 355 | match read_from_file(&opt.keyword) { 356 | Ok(x) => { 357 | if !x.is_empty() { 358 | x 359 | } else { 360 | console.write( 361 | ConsoleTextKind::Error, 362 | &format!("Error: file is empty @ {:?}\n", opt.keyword), 363 | ); 364 | exit(1, &mut console); 365 | } 366 | } 367 | Err(e) => { 368 | console.write( 369 | ConsoleTextKind::Error, 370 | &format!("Error: {} @ {:?}\n", decode_error(e.kind()), opt.keyword), 371 | ); 372 | exit(1, &mut console); 373 | } 374 | } 375 | } else { 376 | handle_escape(&opt.keyword).into_bytes() 377 | }; 378 | 379 | let replacement = if opt.rep_from_file { 380 | match read_from_file(&opt.replacement) { 381 | Ok(x) => x, 382 | Err(e) => { 383 | console.write( 384 | ConsoleTextKind::Error, 385 | &format!("Error: {} @ {:?}\n", decode_error(e.kind()), opt.replacement), 386 | ); 387 | exit(1, &mut console); 388 | } 389 | } 390 | } else { 391 | handle_escape(&opt.replacement).into_bytes() 392 | }; 393 | 394 | // --------------------------------------------------------------------------------------------- 395 | // Pipeline Construct 396 | // --------------------------------------------------------------------------------------------- 397 | 398 | let id_finder = 0; 399 | let id_sorter = 1; 400 | let id_replacer = 2; 401 | let id_matcher = 3; 402 | 403 | let matcher_num = cmp::min(8, opt.max_threads); 404 | 405 | let (tx_finder, rx_finder) = unbounded(); 406 | let (tx_replacer, rx_replacer) = unbounded(); 407 | let (tx_main, rx_main) = unbounded(); 408 | 409 | let mut tx_matcher = Vec::new(); 410 | let mut rx_sorter = Vec::new(); 411 | 412 | let mut finder = PipelineFinder::new(); 413 | let mut sorter = PipelineSorter::new(matcher_num); 414 | let mut replacer = PipelineReplacer::new(&keyword, &replacement, opt.regex); 415 | 416 | finder.is_recursive = opt.recursive; 417 | finder.follow_symlink = opt.symlink; 418 | finder.skip_vcs = opt.skip_vcs; 419 | finder.skip_gitignore = opt.skip_gitignore; 420 | finder.print_skipped = opt.skipped | opt.verbose; 421 | finder.find_parent_ignore = opt.parent_ignore; 422 | sorter.through = !opt.fixed_order; 423 | replacer.is_color = opt.color; 424 | replacer.is_interactive = opt.interactive; 425 | replacer.preserve_time = opt.preserve_time; 426 | replacer.print_file = opt.file; 427 | replacer.print_column = opt.column; 428 | replacer.print_row = opt.row; 429 | 430 | let use_regex = opt.regex; 431 | let use_tbm = opt.tbm; 432 | let skip_binary = !opt.binary; 433 | let print_skipped = opt.skipped | opt.verbose; 434 | let print_search = opt.verbose; 435 | let binary_check_bytes = opt.bin_check_bytes; 436 | let mmap_bytes = opt.mmap_bytes; 437 | let max_threads = opt.max_threads; 438 | let size_per_thread = opt.size_per_thread; 439 | 440 | for i in 0..matcher_num { 441 | let keyword = keyword.clone(); 442 | let (tx_in, rx_in) = unbounded(); 443 | let (tx_out, rx_out) = unbounded(); 444 | tx_matcher.push(tx_in); 445 | rx_sorter.push(rx_out); 446 | 447 | let _ = thread::Builder::new().name("matcher".to_string()).spawn(move || { 448 | if use_regex { 449 | let m = RegexMatcher::new(); 450 | let mut matcher = PipelineMatcher::new(m, &keyword); 451 | matcher.skip_binary = skip_binary; 452 | matcher.print_skipped = print_skipped; 453 | matcher.print_search = print_search; 454 | matcher.binary_check_bytes = binary_check_bytes; 455 | matcher.mmap_bytes = mmap_bytes; 456 | matcher.setup(id_matcher + i, rx_in, tx_out); 457 | } else if use_tbm { 458 | let mut m = TbmMatcher::new(); 459 | m.max_threads = max_threads; 460 | m.size_per_thread = size_per_thread; 461 | let mut matcher = PipelineMatcher::new(m, &keyword); 462 | matcher.skip_binary = skip_binary; 463 | matcher.print_skipped = print_skipped; 464 | matcher.print_search = print_search; 465 | matcher.binary_check_bytes = binary_check_bytes; 466 | matcher.mmap_bytes = mmap_bytes; 467 | matcher.setup(id_matcher + i, rx_in, tx_out); 468 | } else { 469 | let mut m = QuickSearchMatcher::new(); 470 | m.max_threads = max_threads; 471 | m.size_per_thread = size_per_thread; 472 | let mut matcher = PipelineMatcher::new(m, &keyword); 473 | matcher.skip_binary = skip_binary; 474 | matcher.print_skipped = print_skipped; 475 | matcher.print_search = print_search; 476 | matcher.binary_check_bytes = binary_check_bytes; 477 | matcher.mmap_bytes = mmap_bytes; 478 | matcher.setup(id_matcher + i, rx_in, tx_out); 479 | }; 480 | }); 481 | } 482 | 483 | let _ = thread::Builder::new().name("finder".to_string()).spawn(move || { 484 | finder.setup(id_finder, rx_finder, tx_matcher); 485 | }); 486 | 487 | let _ = thread::Builder::new().name("sorter".to_string()).spawn(move || { 488 | sorter.setup(id_sorter, rx_sorter, tx_replacer); 489 | }); 490 | 491 | let _ = thread::Builder::new().name("replacer".to_string()).spawn(move || { 492 | replacer.setup(id_replacer, rx_replacer, tx_main); 493 | }); 494 | 495 | // --------------------------------------------------------------------------------------------- 496 | // Pipeline Flow 497 | // --------------------------------------------------------------------------------------------- 498 | 499 | let mut seq_no = 0; 500 | let _ = tx_finder.send(PipelineInfo::SeqBeg(seq_no)); 501 | for p in base_paths { 502 | let _ = tx_finder.send(PipelineInfo::SeqDat(seq_no, p)); 503 | seq_no += 1; 504 | } 505 | let _ = tx_finder.send(PipelineInfo::SeqEnd(seq_no)); 506 | 507 | let mut time_finder_bsy = Duration::new(0, 0); 508 | let mut time_finder_all = Duration::new(0, 0); 509 | let mut time_sorter_bsy = Duration::new(0, 0); 510 | let mut time_sorter_all = Duration::new(0, 0); 511 | let mut time_replacer_bsy = Duration::new(0, 0); 512 | let mut time_replacer_all = Duration::new(0, 0); 513 | 514 | let mut time_matcher_bsy = Vec::new(); 515 | let mut time_matcher_all = Vec::new(); 516 | for _ in 0..matcher_num { 517 | time_matcher_bsy.push(Duration::new(0, 0)); 518 | time_matcher_all.push(Duration::new(0, 0)); 519 | } 520 | 521 | loop { 522 | match rx_main.try_recv() { 523 | Ok(PipelineInfo::SeqEnd(_)) => break, 524 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_finder => { 525 | time_finder_bsy = t0; 526 | time_finder_all = t1; 527 | } 528 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_sorter => { 529 | time_sorter_bsy = t0; 530 | time_sorter_all = t1; 531 | } 532 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_replacer => { 533 | time_replacer_bsy = t0; 534 | time_replacer_all = t1; 535 | } 536 | Ok(PipelineInfo::MsgTime(id, t0, t1)) => { 537 | time_matcher_bsy[id - id_matcher] = t0; 538 | time_matcher_all[id - id_matcher] = t1; 539 | } 540 | Ok(PipelineInfo::MsgInfo(_id, s)) => console.write(ConsoleTextKind::Info, &format!("{}\n", s)), 541 | Ok(PipelineInfo::MsgErr(_id, s)) => console.write(ConsoleTextKind::Error, &format!("{}\n", s)), 542 | Ok(_) => (), 543 | Err(_) => (), 544 | } 545 | } 546 | 547 | // --------------------------------------------------------------------------------------------- 548 | // Pipeline Flow 549 | // --------------------------------------------------------------------------------------------- 550 | 551 | let sec_finder_bsy = as_secsf64(time_finder_bsy); 552 | let sec_finder_all = as_secsf64(time_finder_all); 553 | let sec_sorter_bsy = as_secsf64(time_sorter_bsy); 554 | let sec_sorter_all = as_secsf64(time_sorter_all); 555 | let sec_replacer_bsy = as_secsf64(time_replacer_bsy); 556 | let sec_replacer_all = as_secsf64(time_replacer_all); 557 | 558 | let mut sec_matcher_bsy = Vec::new(); 559 | let mut sec_matcher_all = Vec::new(); 560 | for i in 0..matcher_num { 561 | sec_matcher_bsy.push(as_secsf64(time_matcher_bsy[i])); 562 | sec_matcher_all.push(as_secsf64(time_matcher_all[i])); 563 | } 564 | 565 | if opt.statistics { 566 | console.write(ConsoleTextKind::Info, "\nStatistics\n"); 567 | console.write( 568 | ConsoleTextKind::Info, 569 | &format!(" Max threads: {}\n\n", opt.max_threads), 570 | ); 571 | console.write(ConsoleTextKind::Info, " Consumed time ( busy / total )\n"); 572 | console.write( 573 | ConsoleTextKind::Info, 574 | &format!(" Find : {}s / {}s\n", sec_finder_bsy, sec_finder_all), 575 | ); 576 | for i in 0..matcher_num { 577 | console.write( 578 | ConsoleTextKind::Info, 579 | &format!( 580 | " Match{:02} : {}s / {}s\n", 581 | i, sec_matcher_bsy[i], sec_matcher_all[i] 582 | ), 583 | ); 584 | } 585 | console.write( 586 | ConsoleTextKind::Info, 587 | &format!(" Sort : {}s / {}s\n", sec_sorter_bsy, sec_sorter_all), 588 | ); 589 | console.write( 590 | ConsoleTextKind::Info, 591 | &format!(" Replace : {}s / {}s\n\n", sec_replacer_bsy, sec_replacer_all), 592 | ); 593 | } 594 | 595 | exit(0, &mut console); 596 | } 597 | -------------------------------------------------------------------------------- /src/ambs.rs: -------------------------------------------------------------------------------- 1 | use amber::console::{Console, ConsoleTextKind}; 2 | use amber::matcher::{QuickSearchMatcher, RegexMatcher, TbmMatcher}; 3 | use amber::pipeline::{Pipeline, PipelineFork, PipelineInfo, PipelineJoin}; 4 | use amber::pipeline_finder::PipelineFinder; 5 | use amber::pipeline_matcher::PipelineMatcher; 6 | use amber::pipeline_printer::PipelinePrinter; 7 | use amber::pipeline_sorter::PipelineSorter; 8 | use amber::util::{as_secsf64, decode_error, exit, get_config, handle_escape, read_from_file}; 9 | use crossbeam::channel::unbounded; 10 | use lazy_static::lazy_static; 11 | use serde::Deserialize; 12 | use std::cmp; 13 | use std::fs; 14 | use std::io::Read; 15 | use std::path::PathBuf; 16 | use std::thread; 17 | use std::time::Duration; 18 | use structopt::{clap, StructOpt}; 19 | 20 | // --------------------------------------------------------------------------------------------------------------------- 21 | // Opt 22 | // --------------------------------------------------------------------------------------------------------------------- 23 | 24 | #[derive(Debug, StructOpt)] 25 | #[structopt(name = "ambs")] 26 | #[structopt(long_version(option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"))))] 27 | #[structopt(setting(clap::AppSettings::ColoredHelp))] 28 | #[structopt(setting(clap::AppSettings::DeriveDisplayOrder))] 29 | pub struct Opt { 30 | /// Keyword for search 31 | #[structopt(name = "KEYWORD")] 32 | pub keyword: String, 33 | 34 | /// Use file contents of KEYWORD as keyword for search 35 | #[structopt(long = "key-from-file")] 36 | pub key_from_file: bool, 37 | 38 | /// Search paths 39 | #[structopt(name = "PATHS")] 40 | pub paths: Vec, 41 | 42 | /// Number of max threads 43 | #[structopt(long = "max-threads", default_value = &MAX_THREADS, value_name = "NUM")] 44 | pub max_threads: usize, 45 | 46 | /// File size per one thread 47 | #[structopt(long = "size-per-thread", default_value = "1048576", value_name = "BYTES")] 48 | pub size_per_thread: usize, 49 | 50 | /// Read size for checking binary 51 | #[structopt(long = "bin-check-bytes", default_value = "256", value_name = "BYTES")] 52 | pub bin_check_bytes: usize, 53 | 54 | /// [Experimental] Minimum size for using mmap 55 | #[structopt(long = "mmap-bytes", default_value = "1048576", value_name = "BYTES")] 56 | pub mmap_bytes: u64, 57 | 58 | /// Verbose message 59 | #[structopt(long = "verbose")] 60 | pub verbose: bool, 61 | 62 | /// Enable regular expression search 63 | #[structopt(short = "r", long = "regex", hidden = DEFAULT_FLAGS.regex)] 64 | pub regex: bool, 65 | 66 | /// Enable column output 67 | #[structopt(long = "column", hidden = DEFAULT_FLAGS.column)] 68 | pub column: bool, 69 | 70 | /// Enable row output 71 | #[structopt(long = "row", hidden = DEFAULT_FLAGS.row)] 72 | pub row: bool, 73 | 74 | /// Enable binary file search 75 | #[structopt(long = "binary", hidden = DEFAULT_FLAGS.binary)] 76 | pub binary: bool, 77 | 78 | /// Enable statistics output 79 | #[structopt(long = "statistics", hidden = DEFAULT_FLAGS.statistics)] 80 | pub statistics: bool, 81 | 82 | /// Enable skipped file output 83 | #[structopt(long = "skipped", hidden = DEFAULT_FLAGS.skipped)] 84 | pub skipped: bool, 85 | 86 | /// Enable recursive directory search 87 | #[structopt(long = "recursive", hidden = DEFAULT_FLAGS.recursive)] 88 | pub recursive: bool, 89 | 90 | /// Enable symbolic link follow 91 | #[structopt(long = "symlink", hidden = DEFAULT_FLAGS.symlink)] 92 | pub symlink: bool, 93 | 94 | /// Enable colored output 95 | #[structopt(long = "color", hidden = DEFAULT_FLAGS.color)] 96 | pub color: bool, 97 | 98 | /// Enable filename output 99 | #[structopt(long = "file", hidden = DEFAULT_FLAGS.file)] 100 | pub file: bool, 101 | 102 | /// Enable vcs directory ( .hg/.git/.svn ) skip 103 | #[structopt(long = "skip-vcs", hidden = DEFAULT_FLAGS.skip_vcs)] 104 | pub skip_vcs: bool, 105 | 106 | /// Enable .gitignore skip 107 | #[structopt(long = "skip-gitignore", hidden = DEFAULT_FLAGS.skip_gitignore)] 108 | pub skip_gitignore: bool, 109 | 110 | /// Enable output order guarantee 111 | #[structopt(long = "fixed-order", hidden = DEFAULT_FLAGS.fixed_order)] 112 | pub fixed_order: bool, 113 | 114 | /// Enable .*ignore file search at parent directories 115 | #[structopt(long = "parent-ignore", hidden = DEFAULT_FLAGS.parent_ignore)] 116 | pub parent_ignore: bool, 117 | 118 | /// Enable to show the line by each match 119 | #[structopt(long = "line-by-match", hidden = DEFAULT_FLAGS.line_by_match)] 120 | pub line_by_match: bool, 121 | 122 | /// Disable regular expression search 123 | #[structopt(long = "no-regex", hidden = !DEFAULT_FLAGS.regex)] 124 | pub no_regex: bool, 125 | 126 | /// Disable column output 127 | #[structopt(long = "no-column", hidden = !DEFAULT_FLAGS.column)] 128 | pub no_column: bool, 129 | 130 | /// Disable row output 131 | #[structopt(long = "no-row", hidden = !DEFAULT_FLAGS.row)] 132 | pub no_row: bool, 133 | 134 | /// Disable binary file search 135 | #[structopt(long = "no-binary", hidden = !DEFAULT_FLAGS.binary)] 136 | pub no_binary: bool, 137 | 138 | /// Disable statistics output 139 | #[structopt(long = "no-statistics", hidden = !DEFAULT_FLAGS.statistics)] 140 | pub no_statistics: bool, 141 | 142 | /// Disable skipped file output 143 | #[structopt(long = "no-skipped", hidden = !DEFAULT_FLAGS.skipped)] 144 | pub no_skipped: bool, 145 | 146 | /// Disable recursive directory search 147 | #[structopt(long = "no-recursive", hidden = !DEFAULT_FLAGS.recursive)] 148 | pub no_recursive: bool, 149 | 150 | /// Disable symbolic link follow 151 | #[structopt(long = "no-symlink", hidden = !DEFAULT_FLAGS.symlink)] 152 | pub no_symlink: bool, 153 | 154 | /// Disable colored output 155 | #[structopt(long = "no-color", hidden = !DEFAULT_FLAGS.color)] 156 | pub no_color: bool, 157 | 158 | /// Disable filename output 159 | #[structopt(long = "no-file", hidden = !DEFAULT_FLAGS.file)] 160 | pub no_file: bool, 161 | 162 | /// Disable vcs directory ( .hg/.git/.svn ) skip 163 | #[structopt(long = "no-skip-vcs", hidden = !DEFAULT_FLAGS.skip_vcs)] 164 | pub no_skip_vcs: bool, 165 | 166 | /// Disable .gitignore skip 167 | #[structopt(long = "no-skip-gitignore", hidden = !DEFAULT_FLAGS.skip_gitignore)] 168 | pub no_skip_gitignore: bool, 169 | 170 | /// Disable output order guarantee 171 | #[structopt(long = "no-fixed-order", hidden = !DEFAULT_FLAGS.fixed_order)] 172 | pub no_fixed_order: bool, 173 | 174 | /// Disable .*ignore file search at parent directories 175 | #[structopt(long = "no-parent-ignore", hidden = !DEFAULT_FLAGS.parent_ignore)] 176 | pub no_parent_ignore: bool, 177 | 178 | /// Disable to show the line by each match 179 | #[structopt(long = "no-line-by-match", hidden = !DEFAULT_FLAGS.line_by_match)] 180 | pub no_line_by_match: bool, 181 | 182 | /// [Experimental] Enable TBM matcher 183 | #[structopt(long = "tbm")] 184 | pub tbm: bool, 185 | 186 | /// [Experimental] Enable SSE 4.2 187 | #[structopt(long = "sse")] 188 | pub sse: bool, 189 | } 190 | 191 | #[derive(Debug, Deserialize)] 192 | struct DefaultFlags { 193 | #[serde(default = "flag_false")] 194 | regex: bool, 195 | #[serde(default = "flag_false")] 196 | column: bool, 197 | #[serde(default = "flag_false")] 198 | row: bool, 199 | #[serde(default = "flag_false")] 200 | binary: bool, 201 | #[serde(default = "flag_false")] 202 | statistics: bool, 203 | #[serde(default = "flag_false")] 204 | skipped: bool, 205 | #[serde(default = "flag_true")] 206 | recursive: bool, 207 | #[serde(default = "flag_true")] 208 | symlink: bool, 209 | #[serde(default = "flag_true")] 210 | color: bool, 211 | #[serde(default = "flag_true")] 212 | file: bool, 213 | #[serde(default = "flag_true")] 214 | skip_vcs: bool, 215 | #[serde(default = "flag_true")] 216 | skip_gitignore: bool, 217 | #[serde(default = "flag_true")] 218 | fixed_order: bool, 219 | #[serde(default = "flag_true")] 220 | parent_ignore: bool, 221 | #[serde(default = "flag_false")] 222 | line_by_match: bool, 223 | } 224 | 225 | impl DefaultFlags { 226 | fn new() -> DefaultFlags { 227 | toml::from_str("").unwrap() 228 | } 229 | 230 | fn load() -> DefaultFlags { 231 | if let Some(path) = get_config("ambs.toml") { 232 | match fs::File::open(&path) { 233 | Ok(mut f) => { 234 | let mut s = String::new(); 235 | let _ = f.read_to_string(&mut s); 236 | match toml::from_str(&s) { 237 | Ok(x) => x, 238 | Err(_) => DefaultFlags::new(), 239 | } 240 | } 241 | Err(_) => DefaultFlags::new(), 242 | } 243 | } else { 244 | DefaultFlags::new() 245 | } 246 | } 247 | 248 | fn merge(&self, mut opt: Opt) -> Opt { 249 | opt.regex = if self.regex { !opt.no_regex } else { opt.regex }; 250 | opt.column = if self.column { !opt.no_column } else { opt.column }; 251 | opt.row = if self.row { !opt.no_row } else { opt.row }; 252 | opt.binary = if self.binary { !opt.no_binary } else { opt.binary }; 253 | opt.statistics = if self.statistics { 254 | !opt.no_statistics 255 | } else { 256 | opt.statistics 257 | }; 258 | opt.skipped = if self.skipped { !opt.no_skipped } else { opt.skipped }; 259 | opt.recursive = if self.recursive { 260 | !opt.no_recursive 261 | } else { 262 | opt.recursive 263 | }; 264 | opt.symlink = if self.symlink { !opt.no_symlink } else { opt.symlink }; 265 | opt.color = if self.color { !opt.no_color } else { opt.color }; 266 | opt.file = if self.file { !opt.no_file } else { opt.file }; 267 | opt.skip_vcs = if self.skip_vcs { !opt.no_skip_vcs } else { opt.skip_vcs }; 268 | opt.skip_gitignore = if self.skip_gitignore { 269 | !opt.no_skip_gitignore 270 | } else { 271 | opt.skip_gitignore 272 | }; 273 | opt.fixed_order = if self.fixed_order { 274 | !opt.no_fixed_order 275 | } else { 276 | opt.fixed_order 277 | }; 278 | opt.parent_ignore = if self.parent_ignore { 279 | !opt.no_parent_ignore 280 | } else { 281 | opt.parent_ignore 282 | }; 283 | opt.line_by_match = if self.line_by_match { 284 | !opt.no_line_by_match 285 | } else { 286 | opt.line_by_match 287 | }; 288 | opt 289 | } 290 | } 291 | 292 | fn flag_true() -> bool { 293 | true 294 | } 295 | fn flag_false() -> bool { 296 | false 297 | } 298 | 299 | lazy_static! { 300 | static ref MAX_THREADS: String = format!("{}", num_cpus::get()); 301 | static ref DEFAULT_FLAGS: DefaultFlags = DefaultFlags::load(); 302 | } 303 | 304 | // --------------------------------------------------------------------------------------------------------------------- 305 | // Main 306 | // --------------------------------------------------------------------------------------------------------------------- 307 | 308 | #[allow(dead_code)] 309 | fn main() { 310 | // --------------------------------------------------------------------------------------------- 311 | // Parse Arguments 312 | // --------------------------------------------------------------------------------------------- 313 | 314 | // - Create opt ------------------------------------------------------------ 315 | 316 | let opt = Opt::from_args(); 317 | let opt = DEFAULT_FLAGS.merge(opt); 318 | 319 | let mut console = Console::new(); 320 | console.is_color = opt.color; 321 | 322 | // - Set base path, keyword and replacement -------------------------------- 323 | let mut base_paths: Vec = Vec::new(); 324 | if opt.paths.is_empty() { 325 | base_paths.push(PathBuf::from("./")); 326 | } else { 327 | for p in &opt.paths { 328 | base_paths.push(PathBuf::from(p)); 329 | } 330 | } 331 | 332 | let keyword = if opt.key_from_file { 333 | match read_from_file(&opt.keyword) { 334 | Ok(x) => { 335 | if !x.is_empty() { 336 | x 337 | } else { 338 | console.write( 339 | ConsoleTextKind::Error, 340 | &format!("Error: file is empty @ {:?}\n", opt.keyword), 341 | ); 342 | exit(1, &mut console); 343 | } 344 | } 345 | Err(e) => { 346 | console.write( 347 | ConsoleTextKind::Error, 348 | &format!("Error: {} @ {:?}\n", decode_error(e.kind()), opt.keyword), 349 | ); 350 | exit(1, &mut console); 351 | } 352 | } 353 | } else { 354 | handle_escape(&opt.keyword).into_bytes() 355 | }; 356 | 357 | // --------------------------------------------------------------------------------------------- 358 | // Pipeline Construct 359 | // --------------------------------------------------------------------------------------------- 360 | 361 | let id_finder = 0; 362 | let id_sorter = 1; 363 | let id_printer = 2; 364 | let id_matcher = 3; 365 | 366 | let matcher_num = cmp::min(8, opt.max_threads); 367 | 368 | let (tx_finder, rx_finder) = unbounded(); 369 | let (tx_printer, rx_printer) = unbounded(); 370 | let (tx_main, rx_main) = unbounded(); 371 | 372 | let mut tx_matcher = Vec::new(); 373 | let mut rx_sorter = Vec::new(); 374 | 375 | let mut finder = PipelineFinder::new(); 376 | let mut sorter = PipelineSorter::new(matcher_num); 377 | let mut printer = PipelinePrinter::new(); 378 | 379 | finder.is_recursive = opt.recursive; 380 | finder.follow_symlink = opt.symlink; 381 | finder.skip_vcs = opt.skip_vcs; 382 | finder.skip_gitignore = opt.skip_gitignore; 383 | finder.print_skipped = opt.skipped | opt.verbose; 384 | finder.find_parent_ignore = opt.parent_ignore; 385 | sorter.through = !opt.fixed_order; 386 | printer.is_color = opt.color; 387 | printer.print_file = opt.file; 388 | printer.print_column = opt.column; 389 | printer.print_row = opt.row; 390 | printer.print_line_by_match = opt.line_by_match; 391 | 392 | let use_regex = opt.regex; 393 | let use_tbm = opt.tbm; 394 | let skip_binary = !opt.binary; 395 | let print_skipped = opt.skipped | opt.verbose; 396 | let print_search = opt.verbose; 397 | let binary_check_bytes = opt.bin_check_bytes; 398 | let mmap_bytes = opt.mmap_bytes; 399 | let max_threads = opt.max_threads; 400 | let size_per_thread = opt.size_per_thread; 401 | 402 | for i in 0..matcher_num { 403 | let keyword = keyword.clone(); 404 | let (tx_in, rx_in) = unbounded(); 405 | let (tx_out, rx_out) = unbounded(); 406 | tx_matcher.push(tx_in); 407 | rx_sorter.push(rx_out); 408 | 409 | let _ = thread::Builder::new().name("matcher".to_string()).spawn(move || { 410 | if use_regex { 411 | let m = RegexMatcher::new(); 412 | let mut matcher = PipelineMatcher::new(m, &keyword); 413 | matcher.skip_binary = skip_binary; 414 | matcher.print_skipped = print_skipped; 415 | matcher.print_search = print_search; 416 | matcher.binary_check_bytes = binary_check_bytes; 417 | matcher.mmap_bytes = mmap_bytes; 418 | matcher.setup(id_matcher + i, rx_in, tx_out); 419 | } else if use_tbm { 420 | let mut m = TbmMatcher::new(); 421 | m.max_threads = max_threads; 422 | m.size_per_thread = size_per_thread; 423 | let mut matcher = PipelineMatcher::new(m, &keyword); 424 | matcher.skip_binary = skip_binary; 425 | matcher.print_skipped = print_skipped; 426 | matcher.print_search = print_search; 427 | matcher.binary_check_bytes = binary_check_bytes; 428 | matcher.mmap_bytes = mmap_bytes; 429 | matcher.setup(id_matcher + i, rx_in, tx_out); 430 | } else { 431 | let mut m = QuickSearchMatcher::new(); 432 | m.max_threads = max_threads; 433 | m.size_per_thread = size_per_thread; 434 | let mut matcher = PipelineMatcher::new(m, &keyword); 435 | matcher.skip_binary = skip_binary; 436 | matcher.print_skipped = print_skipped; 437 | matcher.print_search = print_search; 438 | matcher.binary_check_bytes = binary_check_bytes; 439 | matcher.mmap_bytes = mmap_bytes; 440 | matcher.setup(id_matcher + i, rx_in, tx_out); 441 | }; 442 | }); 443 | } 444 | 445 | let _ = thread::Builder::new().name("finder".to_string()).spawn(move || { 446 | finder.setup(id_finder, rx_finder, tx_matcher); 447 | }); 448 | 449 | let _ = thread::Builder::new().name("sorter".to_string()).spawn(move || { 450 | sorter.setup(id_sorter, rx_sorter, tx_printer); 451 | }); 452 | 453 | let _ = thread::Builder::new().name("printer".to_string()).spawn(move || { 454 | printer.setup(id_printer, rx_printer, tx_main); 455 | }); 456 | 457 | // --------------------------------------------------------------------------------------------- 458 | // Pipeline Flow 459 | // --------------------------------------------------------------------------------------------- 460 | 461 | let mut seq_no = 0; 462 | let _ = tx_finder.send(PipelineInfo::SeqBeg(seq_no)); 463 | for p in base_paths { 464 | let _ = tx_finder.send(PipelineInfo::SeqDat(seq_no, p)); 465 | seq_no += 1; 466 | } 467 | let _ = tx_finder.send(PipelineInfo::SeqEnd(seq_no)); 468 | 469 | let mut time_finder_bsy = Duration::new(0, 0); 470 | let mut time_finder_all = Duration::new(0, 0); 471 | let mut time_sorter_bsy = Duration::new(0, 0); 472 | let mut time_sorter_all = Duration::new(0, 0); 473 | let mut time_printer_bsy = Duration::new(0, 0); 474 | let mut time_printer_all = Duration::new(0, 0); 475 | 476 | let mut time_matcher_bsy = Vec::new(); 477 | let mut time_matcher_all = Vec::new(); 478 | for _ in 0..matcher_num { 479 | time_matcher_bsy.push(Duration::new(0, 0)); 480 | time_matcher_all.push(Duration::new(0, 0)); 481 | } 482 | 483 | loop { 484 | match rx_main.try_recv() { 485 | Ok(PipelineInfo::SeqEnd(_)) => break, 486 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_finder => { 487 | time_finder_bsy = t0; 488 | time_finder_all = t1; 489 | } 490 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_sorter => { 491 | time_sorter_bsy = t0; 492 | time_sorter_all = t1; 493 | } 494 | Ok(PipelineInfo::MsgTime(id, t0, t1)) if id == id_printer => { 495 | time_printer_bsy = t0; 496 | time_printer_all = t1; 497 | } 498 | Ok(PipelineInfo::MsgTime(id, t0, t1)) => { 499 | time_matcher_bsy[id - id_matcher] = t0; 500 | time_matcher_all[id - id_matcher] = t1; 501 | } 502 | Ok(PipelineInfo::MsgInfo(_id, s)) => console.write(ConsoleTextKind::Info, &format!("{}\n", s)), 503 | Ok(PipelineInfo::MsgErr(_id, s)) => console.write(ConsoleTextKind::Error, &format!("{}\n", s)), 504 | Ok(_) => (), 505 | Err(_) => (), 506 | } 507 | } 508 | 509 | // --------------------------------------------------------------------------------------------- 510 | // Pipeline Flow 511 | // --------------------------------------------------------------------------------------------- 512 | 513 | let sec_finder_bsy = as_secsf64(time_finder_bsy); 514 | let sec_finder_all = as_secsf64(time_finder_all); 515 | let sec_sorter_bsy = as_secsf64(time_sorter_bsy); 516 | let sec_sorter_all = as_secsf64(time_sorter_all); 517 | let sec_printer_bsy = as_secsf64(time_printer_bsy); 518 | let sec_printer_all = as_secsf64(time_printer_all); 519 | 520 | let sec_matcher_bsy = time_matcher_bsy.into_iter().map(as_secsf64).collect::>(); 521 | let sec_matcher_all = time_matcher_all.into_iter().map(as_secsf64).collect::>(); 522 | 523 | if opt.statistics { 524 | console.write(ConsoleTextKind::Info, "\nStatistics\n"); 525 | console.write( 526 | ConsoleTextKind::Info, 527 | &format!(" Max threads: {}\n\n", opt.max_threads), 528 | ); 529 | console.write(ConsoleTextKind::Info, " Consumed time ( busy / total )\n"); 530 | console.write( 531 | ConsoleTextKind::Info, 532 | &format!(" Find : {}s / {}s\n", sec_finder_bsy, sec_finder_all), 533 | ); 534 | for i in 0..matcher_num { 535 | console.write( 536 | ConsoleTextKind::Info, 537 | &format!( 538 | " Match{:02} : {}s / {}s\n", 539 | i, sec_matcher_bsy[i], sec_matcher_all[i] 540 | ), 541 | ); 542 | } 543 | console.write( 544 | ConsoleTextKind::Info, 545 | &format!(" Sort : {}s / {}s\n", sec_sorter_bsy, sec_sorter_all), 546 | ); 547 | console.write( 548 | ConsoleTextKind::Info, 549 | &format!(" Display : {}s / {}s\n\n", sec_printer_bsy, sec_printer_all), 550 | ); 551 | } 552 | 553 | exit(0, &mut console); 554 | } 555 | -------------------------------------------------------------------------------- /src/console.rs: -------------------------------------------------------------------------------- 1 | extern crate term; 2 | 3 | use crate::matcher::Match; 4 | use std::io; 5 | use std::io::Write; 6 | use std::process; 7 | use term::color::Color; 8 | use term::{StderrTerminal, StdoutTerminal}; 9 | 10 | // --------------------------------------------------------------------------------------------------------------------- 11 | // Console 12 | // --------------------------------------------------------------------------------------------------------------------- 13 | 14 | pub enum ConsoleTextKind { 15 | Filename, 16 | Text, 17 | MatchText, 18 | Other, 19 | Info, 20 | Error, 21 | } 22 | 23 | pub struct Console { 24 | pub is_color: bool, 25 | term_stdout: Box, 26 | term_stderr: Box, 27 | last_color: Color, 28 | } 29 | 30 | const CR: u8 = 0x0d; 31 | const LF: u8 = 0x0a; 32 | 33 | impl Default for Console { 34 | fn default() -> Self { 35 | Self::new() 36 | } 37 | } 38 | 39 | impl Console { 40 | pub fn new() -> Self { 41 | Console { 42 | term_stdout: term::stdout().unwrap_or_else(|| { 43 | process::exit(1); 44 | }), 45 | term_stderr: term::stderr().unwrap_or_else(|| { 46 | process::exit(1); 47 | }), 48 | is_color: true, 49 | last_color: term::color::BLACK, 50 | } 51 | } 52 | 53 | pub fn carriage_return(&mut self) { 54 | let _ = self.term_stdout.carriage_return(); 55 | } 56 | 57 | pub fn cursor_up(&mut self) { 58 | let _ = self.term_stdout.cursor_up(); 59 | } 60 | 61 | pub fn delete_line(&mut self) { 62 | let _ = self.term_stdout.delete_line(); 63 | } 64 | 65 | pub fn write_with_clear(&mut self, kind: ConsoleTextKind, val: &str) { 66 | self.carriage_return(); 67 | self.delete_line(); 68 | self.write(kind, val); 69 | } 70 | 71 | pub fn write(&mut self, kind: ConsoleTextKind, val: &str) { 72 | let color = match kind { 73 | ConsoleTextKind::Filename => term::color::BRIGHT_GREEN, 74 | ConsoleTextKind::Text => term::color::WHITE, 75 | ConsoleTextKind::MatchText => term::color::BRIGHT_YELLOW, 76 | ConsoleTextKind::Other => term::color::BRIGHT_CYAN, 77 | ConsoleTextKind::Info => term::color::BRIGHT_CYAN, 78 | ConsoleTextKind::Error => term::color::BRIGHT_RED, 79 | }; 80 | 81 | match kind { 82 | ConsoleTextKind::Error => self.write_stderr(val, color), 83 | ConsoleTextKind::Info => self.write_stderr(val, color), 84 | _ => self.write_stdout(val, color), 85 | } 86 | } 87 | 88 | pub fn flush(&mut self) { 89 | let _ = io::stdout().flush(); 90 | let _ = io::stderr().flush(); 91 | } 92 | 93 | pub fn reset(&mut self) { 94 | self.term_stdout.reset().unwrap_or_else(|_| { 95 | process::exit(1); 96 | }); 97 | self.term_stderr.reset().unwrap_or_else(|_| { 98 | process::exit(1); 99 | }); 100 | } 101 | 102 | pub fn get_line_beg(src: &[u8], beg: usize) -> usize { 103 | let mut ret = beg; 104 | while ret > 0 { 105 | if src[ret] == CR || src[ret] == LF { 106 | ret += 1; 107 | break; 108 | } 109 | ret -= 1; 110 | } 111 | ret 112 | } 113 | 114 | pub fn get_line_end(src: &[u8], end: usize) -> usize { 115 | let mut ret = end; 116 | while src.len() > ret { 117 | if src[ret] == CR || src[ret] == LF { 118 | ret -= 1; 119 | break; 120 | } 121 | ret += 1; 122 | } 123 | if src.len() <= ret { 124 | ret = src.len() 125 | } else { 126 | ret += 1 127 | }; 128 | ret 129 | } 130 | 131 | pub fn write_to_linebreak(&mut self, src: &[u8], beg: usize, end: usize) { 132 | if beg < end { 133 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[beg..end])); 134 | } 135 | self.write(ConsoleTextKind::Text, "\n"); 136 | } 137 | 138 | pub fn write_match_part(&mut self, src: &[u8], m: &Match, beg: usize) { 139 | if beg < m.beg { 140 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[beg..m.beg])); 141 | } 142 | self.write(ConsoleTextKind::MatchText, &String::from_utf8_lossy(&src[m.beg..m.end])); 143 | } 144 | 145 | pub fn write_match_line(&mut self, src: &[u8], m: &Match) { 146 | let beg = Console::get_line_beg(src, m.beg); 147 | let end = Console::get_line_end(src, m.end); 148 | 149 | if beg < m.beg { 150 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[beg..m.beg])); 151 | } 152 | self.write(ConsoleTextKind::MatchText, &String::from_utf8_lossy(&src[m.beg..m.end])); 153 | if m.end < end { 154 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[m.end..end])); 155 | } 156 | self.write(ConsoleTextKind::Text, "\n"); 157 | } 158 | 159 | pub fn write_replace_line(&mut self, src: &[u8], m: &Match, rep: &[u8]) { 160 | let beg = Console::get_line_beg(src, m.beg); 161 | let end = Console::get_line_end(src, m.end); 162 | 163 | if beg < m.beg { 164 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[beg..m.beg])); 165 | } 166 | self.write(ConsoleTextKind::MatchText, &String::from_utf8_lossy(rep)); 167 | if m.end < end { 168 | self.write(ConsoleTextKind::Text, &String::from_utf8_lossy(&src[m.end..end])); 169 | } 170 | self.write(ConsoleTextKind::Text, "\n"); 171 | } 172 | 173 | fn write_stdout(&mut self, val: &str, color: Color) { 174 | if self.is_color && self.last_color != color { 175 | self.term_stdout.fg(color).unwrap_or_else(|_| { 176 | process::exit(1); 177 | }); 178 | self.last_color = color; 179 | } 180 | 181 | write!(self.term_stdout, "{}", val).unwrap_or_else(|_| { 182 | process::exit(1); 183 | }); 184 | } 185 | 186 | fn write_stderr(&mut self, val: &str, color: Color) { 187 | if self.is_color && self.last_color != color { 188 | self.term_stderr.fg(color).unwrap_or_else(|_| { 189 | process::exit(1); 190 | }); 191 | self.last_color = color; 192 | } 193 | 194 | write!(self.term_stderr, "{}", val).unwrap_or_else(|_| { 195 | process::exit(1); 196 | }); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/ignore.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | pub use ignore::gitignore::Gitignore; 4 | 5 | // --------------------------------------------------------------------------------------------------------------------- 6 | // Ignore 7 | // --------------------------------------------------------------------------------------------------------------------- 8 | 9 | pub trait Ignore { 10 | fn is_ignore(&self, path: &Path, is_dir: bool) -> bool; 11 | } 12 | 13 | // --------------------------------------------------------------------------------------------------------------------- 14 | // IgnoreVcs 15 | // --------------------------------------------------------------------------------------------------------------------- 16 | 17 | pub struct IgnoreVcs { 18 | vcs_dirs: Vec, 19 | } 20 | 21 | impl Default for IgnoreVcs { 22 | fn default() -> Self { 23 | Self::new() 24 | } 25 | } 26 | 27 | impl IgnoreVcs { 28 | pub fn new() -> Self { 29 | IgnoreVcs { 30 | vcs_dirs: vec![ 31 | ".svn".to_string(), 32 | ".hg".to_string(), 33 | ".git".to_string(), 34 | ".bzr".to_string(), 35 | ], 36 | } 37 | } 38 | } 39 | 40 | impl Ignore for IgnoreVcs { 41 | fn is_ignore(&self, path: &Path, is_dir: bool) -> bool { 42 | if is_dir { 43 | for d in &self.vcs_dirs { 44 | if path.ends_with(d) { 45 | return true; 46 | } 47 | } 48 | } 49 | false 50 | } 51 | } 52 | 53 | // --------------------------------------------------------------------------------------------------------------------- 54 | // IgnoreGit 55 | // --------------------------------------------------------------------------------------------------------------------- 56 | 57 | impl Ignore for Gitignore { 58 | fn is_ignore(&self, path: &Path, is_dir: bool) -> bool { 59 | match self.matched(path, is_dir) { 60 | ignore::Match::None | ignore::Match::Whitelist(_) => false, 61 | ignore::Match::Ignore(_) => true, 62 | } 63 | } 64 | } 65 | 66 | // --------------------------------------------------------------------------------------------------------------------- 67 | // Test 68 | // --------------------------------------------------------------------------------------------------------------------- 69 | 70 | #[cfg(test)] 71 | mod tests { 72 | use super::*; 73 | use std::path::PathBuf; 74 | 75 | #[test] 76 | fn ignore_git() { 77 | let ignore = Gitignore::new(PathBuf::from("./test/.gitignore")).0; 78 | 79 | assert!(!ignore.is_ignore(&PathBuf::from("./test/ao"), false)); 80 | assert!(ignore.is_ignore(&PathBuf::from("./test/a.o"), false)); 81 | assert!(ignore.is_ignore(&PathBuf::from("./test/abc.o"), false)); 82 | assert!(ignore.is_ignore(&PathBuf::from("./test/a.s"), false)); 83 | assert!(!ignore.is_ignore(&PathBuf::from("./test/abc.s"), false)); 84 | assert!(ignore.is_ignore(&PathBuf::from("./test/d0.t"), false)); 85 | assert!(!ignore.is_ignore(&PathBuf::from("./test/d00.t"), false)); 86 | assert!(ignore.is_ignore(&PathBuf::from("./test/file"), false)); 87 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir0/file"), false)); 88 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir1/file"), false)); 89 | assert!(!ignore.is_ignore(&PathBuf::from("./test/x/file"), false)); 90 | assert!(!ignore.is_ignore(&PathBuf::from("./test/x/dir0/file"), false)); 91 | assert!(!ignore.is_ignore(&PathBuf::from("./test/x/dir1/file"), false)); 92 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir2"), true)); 93 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir3/dir4"), true)); 94 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir5/dir6"), true)); 95 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir7"), true)); 96 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir3/dir7"), true)); 97 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir8"), true)); 98 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir9/dir10"), true)); 99 | assert!(ignore.is_ignore(&PathBuf::from("./test/dir11/dir12"), true)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod util; 3 | pub mod console; 4 | pub mod ignore; 5 | pub mod matcher; 6 | pub mod pipeline; 7 | pub mod pipeline_finder; 8 | pub mod pipeline_matcher; 9 | pub mod pipeline_printer; 10 | pub mod pipeline_replacer; 11 | pub mod pipeline_sorter; 12 | -------------------------------------------------------------------------------- /src/matcher.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::unbounded; 2 | use regex::RegexBuilder; 3 | use rlibc::memcmp; 4 | use std::cmp; 5 | use std::collections::HashMap; 6 | use std::str; 7 | use std::thread; 8 | 9 | // --------------------------------------------------------------------------------------------------------------------- 10 | // Matcher 11 | // --------------------------------------------------------------------------------------------------------------------- 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct Match { 15 | pub beg: usize, 16 | pub end: usize, 17 | pub sub_match: Vec, 18 | } 19 | 20 | pub trait Matcher { 21 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec; 22 | } 23 | 24 | // --------------------------------------------------------------------------------------------------------------------- 25 | // macro 26 | // --------------------------------------------------------------------------------------------------------------------- 27 | 28 | #[cfg(feature = "sse")] 29 | macro_rules! cmp_pat_sse ( 30 | ( $src:expr, $pat:expr, $pat_len:expr, $pat_len_by_dq:expr, $do_mismatch:block ) => ( 31 | { 32 | let mut src_ptr = &( $src ) as *const u8 as usize; 33 | let mut pat_ptr = &( $pat ) as *const u8 as usize; 34 | let mut pat_rest = $pat_len; 35 | for _ in 0 .. $pat_len_by_dq { 36 | unsafe { 37 | let ret_cmp: u64; 38 | asm!( 39 | "movdqu ($1), %xmm0 \n\ 40 | pcmpestri $$0b0011000, ($2), %xmm0 \n\ 41 | " 42 | : "={rcx}"( ret_cmp ) 43 | : "r"( pat_ptr ), "r"( src_ptr ), "{rdx}"( pat_rest ), "{rax}"( pat_rest ) 44 | : "{xmm0}" 45 | : 46 | ); 47 | if ret_cmp != 16 { 48 | $do_mismatch 49 | } 50 | src_ptr += 16; 51 | pat_ptr += 16; 52 | pat_rest -= 16; 53 | } 54 | } 55 | } 56 | ); 57 | ); 58 | 59 | // --------------------------------------------------------------------------------------------------------------------- 60 | // BruteForceMatcher 61 | // --------------------------------------------------------------------------------------------------------------------- 62 | 63 | pub struct BruteForceMatcher; 64 | 65 | impl BruteForceMatcher { 66 | pub fn new() -> Self { 67 | BruteForceMatcher 68 | } 69 | } 70 | 71 | impl Default for BruteForceMatcher { 72 | fn default() -> Self { 73 | Self::new() 74 | } 75 | } 76 | 77 | impl Matcher for BruteForceMatcher { 78 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec { 79 | let src_len = src.len(); 80 | let pat_len = pat.len(); 81 | let mut ret = Vec::new(); 82 | 83 | let mut i = 0; 84 | while i < src_len - pat_len + 1 { 85 | if src[i] == pat[0] { 86 | let mut success = true; 87 | for j in 1..pat_len { 88 | if src[i + j] != pat[j] { 89 | success = false; 90 | break; 91 | } 92 | } 93 | 94 | if success { 95 | ret.push(Match { 96 | beg: i, 97 | end: i + pat_len, 98 | sub_match: Vec::new(), 99 | }); 100 | i = i + pat_len - 1; 101 | } 102 | } 103 | 104 | i += 1; 105 | } 106 | 107 | ret 108 | } 109 | } 110 | 111 | // --------------------------------------------------------------------------------------------------------------------- 112 | // QuickSearchMatcher 113 | // --------------------------------------------------------------------------------------------------------------------- 114 | 115 | pub struct QuickSearchMatcher { 116 | pub max_threads: usize, 117 | pub size_per_thread: usize, 118 | } 119 | 120 | impl Default for QuickSearchMatcher { 121 | fn default() -> Self { 122 | Self::new() 123 | } 124 | } 125 | 126 | impl QuickSearchMatcher { 127 | pub fn new() -> Self { 128 | QuickSearchMatcher { 129 | max_threads: 4, 130 | size_per_thread: 1024 * 1024, 131 | } 132 | } 133 | 134 | fn search_sub(&self, src: &[u8], pat: &[u8], qs_table: &[usize; 256], beg: usize, end: usize) -> Vec { 135 | let src_len = src.len(); 136 | let pat_len = pat.len(); 137 | let mut ret = Vec::new(); 138 | 139 | let src_ptr = src.as_ptr(); 140 | let pat_ptr = pat.as_ptr(); 141 | let qs_ptr = qs_table.as_ptr(); 142 | 143 | let mut i = beg; 144 | while i < end { 145 | if src_len < i + pat_len { 146 | break; 147 | } 148 | 149 | let success; 150 | unsafe { 151 | let ret = memcmp(src_ptr.add(i), pat_ptr, pat_len); 152 | success = ret == 0; 153 | } 154 | 155 | if success && MatcherUtil::check_char_boundary(src, i) { 156 | ret.push(Match { 157 | beg: i, 158 | end: i + pat_len, 159 | sub_match: Vec::new(), 160 | }); 161 | i += pat_len; 162 | continue; 163 | } 164 | 165 | if src_len <= i + pat_len { 166 | break; 167 | } 168 | unsafe { 169 | let t = *src_ptr.add(i + pat_len) as isize; 170 | i += *qs_ptr.offset(t); 171 | } 172 | } 173 | 174 | ret 175 | } 176 | } 177 | 178 | impl Matcher for QuickSearchMatcher { 179 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec { 180 | let src_len = src.len(); 181 | let pat_len = pat.len(); 182 | 183 | let mut qs_table: [usize; 256] = [pat_len + 1; 256]; 184 | let mut i = 0; 185 | while i < pat_len { 186 | qs_table[pat[i] as usize] = pat_len - i; 187 | i += 1; 188 | } 189 | 190 | let thread_num = cmp::min(src_len / self.size_per_thread + 1, self.max_threads); 191 | 192 | if thread_num == 1 { 193 | self.search_sub(src, pat, &qs_table, 0, src_len) 194 | } else { 195 | let (tx, rx) = unbounded(); 196 | thread::scope(|s| { 197 | for i in 0..thread_num { 198 | let tx = tx.clone(); 199 | let beg = src_len * i / thread_num; 200 | let end = src_len * (i + 1) / thread_num; 201 | s.spawn(move || { 202 | let tmp = self.search_sub(src, pat, &qs_table, beg, end); 203 | let _ = tx.send((i, tmp)); 204 | }); 205 | } 206 | }); 207 | 208 | let mut rets = HashMap::new(); 209 | for _ in 0..thread_num { 210 | let (i, tmp) = rx.recv().unwrap(); 211 | rets.insert(i, tmp); 212 | } 213 | 214 | let mut ret = Vec::new(); 215 | for i in 0..thread_num { 216 | let tmp = rets.get(&i).unwrap(); 217 | for t in tmp { 218 | ret.push(t.clone()); 219 | } 220 | } 221 | ret 222 | } 223 | } 224 | } 225 | 226 | // --------------------------------------------------------------------------------------------------------------------- 227 | // TbmMatcher 228 | // --------------------------------------------------------------------------------------------------------------------- 229 | 230 | pub struct TbmMatcher { 231 | pub max_threads: usize, 232 | pub size_per_thread: usize, 233 | } 234 | 235 | impl Default for TbmMatcher { 236 | fn default() -> Self { 237 | Self::new() 238 | } 239 | } 240 | 241 | impl TbmMatcher { 242 | pub fn new() -> Self { 243 | TbmMatcher { 244 | max_threads: 4, 245 | size_per_thread: 1024 * 1024, 246 | } 247 | } 248 | 249 | fn search_sub( 250 | &self, 251 | src: &[u8], 252 | pat: &[u8], 253 | qs_table: &[usize; 256], 254 | md2: usize, 255 | beg: usize, 256 | end: usize, 257 | ) -> Vec { 258 | let src_len = src.len(); 259 | let pat_len = pat.len(); 260 | let mut ret = Vec::new(); 261 | 262 | let src_ptr = src.as_ptr(); 263 | let pat_ptr = pat.as_ptr(); 264 | 265 | let mut i = beg + pat_len - 1; 266 | 'outer: while i < end { 267 | let mut k = qs_table[src[i] as usize]; 268 | while k != 0 { 269 | i += k; 270 | if i >= src_len { 271 | break 'outer; 272 | } 273 | k = qs_table[src[i] as usize]; 274 | } 275 | 276 | if i >= end { 277 | break; 278 | } 279 | 280 | unsafe { 281 | let ret = memcmp(src_ptr.add(i + 1 - pat_len), pat_ptr, pat_len); 282 | if ret != 0 { 283 | i += md2; 284 | continue 'outer; 285 | } 286 | } 287 | 288 | if MatcherUtil::check_char_boundary(src, i + 1 - pat_len) { 289 | ret.push(Match { 290 | beg: i + 1 - pat_len, 291 | end: i + 1, 292 | sub_match: Vec::new(), 293 | }); 294 | i += pat_len; 295 | continue; 296 | } 297 | 298 | i += md2; 299 | } 300 | 301 | ret 302 | } 303 | } 304 | 305 | impl Matcher for TbmMatcher { 306 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec { 307 | let src_len = src.len(); 308 | let pat_len = pat.len(); 309 | 310 | let mut qs_table: [usize; 256] = [pat_len; 256]; 311 | for i in 0..pat_len { 312 | qs_table[pat[i] as usize] = pat_len - 1 - i; 313 | } 314 | 315 | let pe: isize = pat_len as isize - 1; 316 | let mut p: isize = pe - 1; 317 | while p >= 0 { 318 | if pat[p as usize] == pat[pe as usize] { 319 | break; 320 | } 321 | p -= 1; 322 | } 323 | let md2 = (pe - p) as usize; 324 | 325 | let thread_num = cmp::min(src_len / self.size_per_thread + 1, self.max_threads); 326 | 327 | if thread_num == 1 { 328 | self.search_sub(src, pat, &qs_table, md2, 0, src_len) 329 | } else { 330 | let (tx, rx) = unbounded(); 331 | thread::scope(|s| { 332 | for i in 0..thread_num { 333 | let tx = tx.clone(); 334 | let beg = src_len * i / thread_num; 335 | let end = src_len * (i + 1) / thread_num; 336 | s.spawn(move || { 337 | let tmp = self.search_sub(src, pat, &qs_table, md2, beg, end); 338 | let _ = tx.send((i, tmp)); 339 | }); 340 | } 341 | }); 342 | 343 | let mut rets = HashMap::new(); 344 | for _ in 0..thread_num { 345 | let (i, tmp) = rx.recv().unwrap(); 346 | rets.insert(i, tmp); 347 | } 348 | 349 | let mut ret = Vec::new(); 350 | for i in 0..thread_num { 351 | let tmp = rets.get(&i).unwrap(); 352 | for t in tmp { 353 | ret.push(t.clone()); 354 | } 355 | } 356 | ret 357 | } 358 | } 359 | } 360 | 361 | // --------------------------------------------------------------------------------------------------------------------- 362 | // FjsMatcher 363 | // --------------------------------------------------------------------------------------------------------------------- 364 | 365 | pub struct FjsMatcher { 366 | pub max_threads: usize, 367 | pub size_per_thread: usize, 368 | pub use_sse: bool, 369 | } 370 | 371 | impl Default for FjsMatcher { 372 | fn default() -> Self { 373 | Self::new() 374 | } 375 | } 376 | 377 | impl FjsMatcher { 378 | pub fn new() -> Self { 379 | FjsMatcher { 380 | max_threads: 4, 381 | size_per_thread: 1024 * 1024, 382 | use_sse: false, 383 | } 384 | } 385 | 386 | #[allow(unused_variables)] 387 | fn search_sub( 388 | &self, 389 | src: &[u8], 390 | pat: &[u8], 391 | betap: &[isize; 101], 392 | delta: &[usize; 256], 393 | beg: usize, 394 | end: usize, 395 | ) -> Vec { 396 | let src_len = src.len(); 397 | let pat_len = pat.len(); 398 | let mut ret = Vec::new(); 399 | 400 | let mut i = 0; 401 | let mut j = 0; 402 | let mp = pat_len - 1; 403 | let mut ip = mp + beg; 404 | let mut prev: isize = -(pat_len as isize); 405 | 406 | while ip < end { 407 | if j == 0 { 408 | if ip + 1 >= src_len { 409 | return ret; 410 | } 411 | while pat[mp] != src[ip] { 412 | ip += delta[src[ip + 1] as usize]; 413 | if ip >= src_len { 414 | return ret; 415 | } 416 | } 417 | j = 0; 418 | i = ip - mp; 419 | while j < mp && src[i] == pat[j] { 420 | i += 1; 421 | j += 1; 422 | } 423 | if j == mp && MatcherUtil::check_char_boundary(src, i - mp) { 424 | if prev + pat_len as isize <= (i - mp) as isize { 425 | ret.push(Match { 426 | beg: i - mp, 427 | end: i - mp + pat_len, 428 | sub_match: Vec::new(), 429 | }); 430 | prev = (i - mp) as isize; 431 | } 432 | i += 1; 433 | j += 1; 434 | } 435 | if j == 0 { 436 | i += 1; 437 | } else { 438 | j = betap[j] as usize; 439 | } 440 | } else { 441 | while j < pat_len && src[i] == pat[j] { 442 | i += 1; 443 | j += 1; 444 | } 445 | if j == pat_len 446 | && MatcherUtil::check_char_boundary(src, i - pat_len) 447 | && prev + pat_len as isize <= (i - pat_len) as isize 448 | { 449 | ret.push(Match { 450 | beg: i - pat_len, 451 | end: i, 452 | sub_match: Vec::new(), 453 | }); 454 | prev = (i - pat_len) as isize; 455 | } 456 | j = betap[j] as usize; 457 | } 458 | ip = i + mp - j; 459 | } 460 | 461 | ret 462 | } 463 | } 464 | 465 | impl Matcher for FjsMatcher { 466 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec { 467 | let src_len = src.len(); 468 | let pat_len = pat.len(); 469 | 470 | let mut betap: [isize; 101] = [-1; 101]; 471 | let mut delta: [usize; 256] = [pat_len; 256]; 472 | 473 | let mut i = 0; 474 | let mut j = betap[0]; 475 | while i < pat_len { 476 | while j > -1 && pat[i] != pat[j as usize] { 477 | j = betap[j as usize]; 478 | } 479 | i += 1; 480 | j += 1; 481 | if i < pat_len && pat[i] == pat[j as usize] { 482 | betap[i] = betap[j as usize]; 483 | } else { 484 | betap[i] = j; 485 | } 486 | } 487 | 488 | for i in 0..pat_len { 489 | delta[pat[i] as usize] = pat_len - i; 490 | } 491 | 492 | let thread_num = cmp::min(src_len / self.size_per_thread + 1, self.max_threads); 493 | 494 | if thread_num == 1 { 495 | self.search_sub(src, pat, &betap, &delta, 0, src_len) 496 | } else { 497 | let (tx, rx) = unbounded(); 498 | thread::scope(|s| { 499 | for i in 0..thread_num { 500 | let tx = tx.clone(); 501 | let beg = src_len * i / thread_num; 502 | let end = src_len * (i + 1) / thread_num; 503 | s.spawn(move || { 504 | let tmp = self.search_sub(src, pat, &betap, &delta, beg, end); 505 | let _ = tx.send((i, tmp)); 506 | }); 507 | } 508 | }); 509 | 510 | let mut rets = HashMap::new(); 511 | for _ in 0..thread_num { 512 | let (i, tmp) = rx.recv().unwrap(); 513 | rets.insert(i, tmp); 514 | } 515 | 516 | let mut ret = Vec::new(); 517 | for i in 0..thread_num { 518 | let tmp = rets.get(&i).unwrap(); 519 | for t in tmp { 520 | ret.push(t.clone()); 521 | } 522 | } 523 | ret 524 | } 525 | } 526 | } 527 | 528 | // --------------------------------------------------------------------------------------------------------------------- 529 | // RegexMatcher 530 | // --------------------------------------------------------------------------------------------------------------------- 531 | 532 | pub struct RegexMatcher; 533 | 534 | impl RegexMatcher { 535 | pub fn new() -> Self { 536 | RegexMatcher 537 | } 538 | } 539 | 540 | impl Default for RegexMatcher { 541 | fn default() -> Self { 542 | Self::new() 543 | } 544 | } 545 | 546 | impl Matcher for RegexMatcher { 547 | fn search(&self, src: &[u8], pat: &[u8]) -> Vec { 548 | let pat_str = match str::from_utf8(pat) { 549 | Ok(x) => x, 550 | Err(_) => return Vec::new(), 551 | }; 552 | 553 | let src_str = match str::from_utf8(src) { 554 | Ok(x) => x, 555 | Err(_) => return Vec::new(), 556 | }; 557 | 558 | let re = match RegexBuilder::new(pat_str).multi_line(true).build() { 559 | Ok(x) => x, 560 | Err(_) => return Vec::new(), 561 | }; 562 | 563 | let result = re.find_iter(src_str); 564 | 565 | let mut ret = Vec::new(); 566 | for r in result { 567 | ret.push(Match { 568 | beg: r.start(), 569 | end: r.end(), 570 | sub_match: Vec::new(), 571 | }); 572 | } 573 | ret 574 | } 575 | } 576 | 577 | // --------------------------------------------------------------------------------------------------------------------- 578 | // MatcherUtil 579 | // --------------------------------------------------------------------------------------------------------------------- 580 | 581 | struct MatcherUtil; 582 | 583 | impl MatcherUtil { 584 | fn check_char_boundary(src: &[u8], pos: usize) -> bool { 585 | let mut pos_ascii = if pos == 0 { 0 } else { pos - 1 }; 586 | while pos_ascii > 0 { 587 | if src[pos_ascii] <= 0x7f { 588 | break; 589 | } 590 | pos_ascii -= 1; 591 | } 592 | 593 | let mut check_pos = pos_ascii; 594 | while check_pos < pos { 595 | let char_width = MatcherUtil::check_char_width(src, check_pos); 596 | check_pos += char_width; 597 | } 598 | 599 | check_pos == pos 600 | } 601 | 602 | fn check_char_width(src: &[u8], pos: usize) -> usize { 603 | let src_len = src.len(); 604 | let pos0 = pos; 605 | let pos1 = if pos + 1 >= src_len { src_len - 1 } else { pos + 1 }; 606 | let pos2 = if pos + 2 >= src_len { src_len - 1 } else { pos + 2 }; 607 | let pos3 = if pos + 3 >= src_len { src_len - 1 } else { pos + 3 }; 608 | let pos4 = if pos + 4 >= src_len { src_len - 1 } else { pos + 4 }; 609 | let pos5 = if pos + 5 >= src_len { src_len - 1 } else { pos + 5 }; 610 | match (src[pos0], src[pos1], src[pos2], src[pos3], src[pos4], src[pos5]) { 611 | (0x00..=0x7f, _, _, _, _, _) => 1, // ASCII 612 | (0xc2..=0xdf, 0x80..=0xbf, _, _, _, _) => 2, // UTF-8 613 | (0xe0..=0xef, 0x80..=0xbf, 0x80..=0xbf, _, _, _) => 3, // UTF-8 614 | (0xf0..=0xf7, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf, _, _) => 4, // UTF-8 615 | (0xf8..=0xfb, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf, _) => 5, // UTF-8 616 | (0xfc..=0xfd, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf, 0x80..=0xbf) => 6, // UTF-8 617 | (0x8e, 0xa1..=0xdf, _, _, _, _) => 2, // EUC-JP 618 | (0xa1..=0xfe, 0xa1..=0xfe, _, _, _, _) => 2, // EUC-JP 619 | (0xa1..=0xdf, _, _, _, _, _) => 1, // ShiftJIS 620 | (0x81..=0x9f, 0x40..=0x7e, _, _, _, _) => 2, // ShiftJIS 621 | (0x81..=0x9f, 0x80..=0xfc, _, _, _, _) => 2, // ShiftJIS 622 | (0xe0..=0xef, 0x40..=0x7e, _, _, _, _) => 2, // ShiftJIS 623 | (0xe0..=0xef, 0x80..=0xfc, _, _, _, _) => 2, // ShiftJIS 624 | _ => 1, // Unknown 625 | } 626 | } 627 | } 628 | 629 | // --------------------------------------------------------------------------------------------------------------------- 630 | // Test 631 | // --------------------------------------------------------------------------------------------------------------------- 632 | 633 | #[cfg(test)] 634 | mod tests { 635 | use super::*; 636 | 637 | fn test_matcher(m: &T) { 638 | let src = "abcabcaaaaabc".to_string().into_bytes(); 639 | let pat = "a".to_string().into_bytes(); 640 | let ret = m.search(&src, &pat); 641 | assert_eq!(ret.len(), 7); 642 | assert_eq!((0, 1), (ret[0].beg, ret[0].end)); 643 | assert_eq!((3, 4), (ret[1].beg, ret[1].end)); 644 | assert_eq!((6, 7), (ret[2].beg, ret[2].end)); 645 | assert_eq!((7, 8), (ret[3].beg, ret[3].end)); 646 | assert_eq!((8, 9), (ret[4].beg, ret[4].end)); 647 | assert_eq!((9, 10), (ret[5].beg, ret[5].end)); 648 | assert_eq!((10, 11), (ret[6].beg, ret[6].end)); 649 | 650 | let src = "abcabcbbbaabc".to_string().into_bytes(); 651 | let pat = "abc".to_string().into_bytes(); 652 | let ret = m.search(&src, &pat); 653 | assert_eq!(ret.len(), 3); 654 | assert_eq!((0, 3), (ret[0].beg, ret[0].end)); 655 | assert_eq!((3, 6), (ret[1].beg, ret[1].end)); 656 | assert_eq!((10, 13), (ret[2].beg, ret[2].end)); 657 | 658 | let src = "abcabcaaaaabc".to_string().into_bytes(); 659 | let pat = "aaa".to_string().into_bytes(); 660 | let ret = m.search(&src, &pat); 661 | assert_eq!(ret.len(), 1); 662 | assert_eq!((6, 9), (ret[0].beg, ret[0].end)); 663 | 664 | let src = "abcabcaaaaabc".to_string().into_bytes(); 665 | let pat = "abcabcaaaaabc".to_string().into_bytes(); 666 | let ret = m.search(&src, &pat); 667 | assert_eq!(ret.len(), 1); 668 | assert_eq!((0, 13), (ret[0].beg, ret[0].end)); 669 | 670 | let src = "abcabcaaaaabc".to_string().into_bytes(); 671 | let pat = "あ".to_string().into_bytes(); 672 | let ret = m.search(&src, &pat); 673 | assert!(ret.is_empty()); 674 | 675 | let src = "abcabcあいうえおaあああaaaabc".to_string().into_bytes(); 676 | let pat = "あ".to_string().into_bytes(); 677 | let ret = m.search(&src, &pat); 678 | assert_eq!(ret.len(), 4); 679 | assert_eq!((6, 9), (ret[0].beg, ret[0].end)); 680 | assert_eq!((22, 25), (ret[1].beg, ret[1].end)); 681 | assert_eq!((25, 28), (ret[2].beg, ret[2].end)); 682 | assert_eq!((28, 31), (ret[3].beg, ret[3].end)); 683 | } 684 | 685 | #[test] 686 | fn test_brute_force_matcher() { 687 | let matcher = BruteForceMatcher::new(); 688 | test_matcher(&matcher); 689 | } 690 | 691 | #[test] 692 | fn test_quick_search_matcher() { 693 | let matcher = QuickSearchMatcher::new(); 694 | test_matcher(&matcher); 695 | } 696 | 697 | #[test] 698 | fn test_tbm_matcher() { 699 | let matcher = TbmMatcher::new(); 700 | test_matcher(&matcher); 701 | } 702 | 703 | //#[test] 704 | //fn test_fjs_matcher() { 705 | // let matcher = FjsMatcher::new(); 706 | // test_matcher( &matcher ); 707 | //} 708 | 709 | #[test] 710 | fn test_regex_matcher() { 711 | let matcher = RegexMatcher::new(); 712 | test_matcher(&matcher); 713 | } 714 | } 715 | -------------------------------------------------------------------------------- /src/pipeline.rs: -------------------------------------------------------------------------------- 1 | use crossbeam::channel::{Receiver, Sender}; 2 | use std::time::Duration; 3 | 4 | pub enum PipelineInfo { 5 | SeqBeg(usize), 6 | SeqDat(usize, T), 7 | SeqEnd(usize), 8 | MsgInfo(usize, String), 9 | MsgDebug(usize, String), 10 | MsgErr(usize, String), 11 | MsgTime(usize, Duration, Duration), 12 | } 13 | 14 | pub trait Pipeline { 15 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Sender>); 16 | } 17 | 18 | pub trait PipelineFork { 19 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Vec>>); 20 | } 21 | 22 | pub trait PipelineJoin { 23 | fn setup(&mut self, id: usize, rx: Vec>>, tx: Sender>); 24 | } 25 | -------------------------------------------------------------------------------- /src/pipeline_finder.rs: -------------------------------------------------------------------------------- 1 | use crate::ignore::{Gitignore, Ignore, IgnoreVcs}; 2 | use crate::pipeline::{PipelineFork, PipelineInfo}; 3 | use crossbeam::channel::{Receiver, Sender}; 4 | use std::fs; 5 | use std::path::{Path, PathBuf}; 6 | use std::time::{Duration, Instant}; 7 | 8 | // --------------------------------------------------------------------------------------------------------------------- 9 | // PathInfo 10 | // --------------------------------------------------------------------------------------------------------------------- 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct PathInfo { 14 | pub path: PathBuf, 15 | } 16 | 17 | // --------------------------------------------------------------------------------------------------------------------- 18 | // PipelineFinder 19 | // --------------------------------------------------------------------------------------------------------------------- 20 | 21 | pub struct PipelineFinder { 22 | pub is_recursive: bool, 23 | pub follow_symlink: bool, 24 | pub skip_vcs: bool, 25 | pub skip_gitignore: bool, 26 | pub skip_hgignore: bool, 27 | pub skip_ambignore: bool, 28 | pub print_skipped: bool, 29 | pub find_parent_ignore: bool, 30 | pub infos: Vec, 31 | pub errors: Vec, 32 | time_beg: Instant, 33 | time_bsy: Duration, 34 | seq_no: usize, 35 | current_tx: usize, 36 | ignore_vcs: IgnoreVcs, 37 | ignore_git: Vec, 38 | } 39 | 40 | impl Default for PipelineFinder { 41 | fn default() -> Self { 42 | Self::new() 43 | } 44 | } 45 | 46 | impl PipelineFinder { 47 | pub fn new() -> Self { 48 | PipelineFinder { 49 | is_recursive: true, 50 | follow_symlink: true, 51 | skip_vcs: true, 52 | skip_gitignore: true, 53 | skip_hgignore: true, 54 | skip_ambignore: true, 55 | print_skipped: false, 56 | find_parent_ignore: true, 57 | infos: Vec::new(), 58 | errors: Vec::new(), 59 | time_beg: Instant::now(), 60 | time_bsy: Duration::new(0, 0), 61 | seq_no: 0, 62 | current_tx: 0, 63 | ignore_vcs: IgnoreVcs::new(), 64 | ignore_git: Vec::new(), 65 | } 66 | } 67 | 68 | fn find_path(&mut self, base: PathBuf, tx: &Vec>>, is_symlink: bool) { 69 | let attr = match fs::metadata(&base) { 70 | Ok(x) => x, 71 | Err(e) => { 72 | if !is_symlink { 73 | self.errors.push(format!("Error: {} @ {}", e, base.to_str().unwrap())); 74 | } 75 | return; 76 | } 77 | }; 78 | 79 | if attr.is_file() { 80 | if attr.len() != 0 { 81 | self.send_path(base, tx); 82 | } 83 | } else { 84 | let reader = match fs::read_dir(&base) { 85 | Ok(x) => x, 86 | Err(e) => { 87 | self.errors.push(format!("Error: {} @ {}", e, base.to_str().unwrap())); 88 | return; 89 | } 90 | }; 91 | 92 | let gitignore_exist = self.push_gitignore(&base); 93 | 94 | for i in reader { 95 | match i { 96 | Ok(entry) => { 97 | let file_type = match entry.file_type() { 98 | Ok(x) => x, 99 | Err(e) => { 100 | self.errors.push(format!("Error: {}", e)); 101 | continue; 102 | } 103 | }; 104 | if file_type.is_file() { 105 | self.send_path(entry.path(), tx); 106 | } else { 107 | let find_dir = file_type.is_dir() & self.is_recursive; 108 | let find_symlink = file_type.is_symlink() & self.is_recursive & self.follow_symlink; 109 | if (find_dir | find_symlink) & self.check_path(&entry.path(), true) { 110 | self.find_path(entry.path(), tx, find_symlink); 111 | } 112 | } 113 | } 114 | Err(e) => self.errors.push(format!("Error: {}", e)), 115 | }; 116 | } 117 | 118 | self.pop_gitignore(gitignore_exist) 119 | } 120 | } 121 | 122 | fn send_path(&mut self, path: PathBuf, tx: &[Sender>]) { 123 | if self.check_path(&path, false) { 124 | let _ = tx[self.current_tx].send(PipelineInfo::SeqDat(self.seq_no, PathInfo { path })); 125 | self.seq_no += 1; 126 | self.current_tx = if self.current_tx == tx.len() - 1 { 127 | 0 128 | } else { 129 | self.current_tx + 1 130 | }; 131 | } 132 | } 133 | 134 | fn push_gitignore(&mut self, path: &PathBuf) -> bool { 135 | if !self.skip_gitignore { 136 | return false; 137 | } 138 | 139 | if let Ok(reader) = fs::read_dir(path) { 140 | for i in reader { 141 | match i { 142 | Ok(entry) => { 143 | if entry.path().ends_with(".gitignore") { 144 | self.ignore_git.push(Gitignore::new(entry.path()).0); 145 | return true; 146 | } 147 | } 148 | Err(e) => self.errors.push(format!("Error: {}", e)), 149 | } 150 | } 151 | } 152 | false 153 | } 154 | 155 | fn pop_gitignore(&mut self, exist: bool) { 156 | if exist { 157 | let _ = self.ignore_git.pop(); 158 | } 159 | } 160 | 161 | fn check_path(&mut self, path: &PathBuf, is_dir: bool) -> bool { 162 | let ok_vcs = if self.skip_vcs { 163 | !self.ignore_vcs.is_ignore(path, is_dir) 164 | } else { 165 | true 166 | }; 167 | 168 | let ok_git = if self.skip_gitignore && !self.ignore_git.is_empty() { 169 | !self.ignore_git.last().unwrap().is_ignore(path, is_dir) 170 | } else { 171 | true 172 | }; 173 | 174 | if !ok_vcs & self.print_skipped { 175 | self.infos.push(format!("Skip (vcs file) : {:?}", path)); 176 | } 177 | 178 | if !ok_git & self.print_skipped { 179 | self.infos.push(format!("Skip (.gitignore): {:?}", path)); 180 | } 181 | 182 | ok_vcs && ok_git 183 | } 184 | 185 | fn set_default_gitignore(&mut self, base: &Path) -> PathBuf { 186 | if !self.skip_gitignore { 187 | return base.to_path_buf(); 188 | } 189 | if !self.find_parent_ignore { 190 | return base.to_path_buf(); 191 | } 192 | 193 | let base_abs = match base.canonicalize() { 194 | Ok(x) => x, 195 | Err(e) => { 196 | self.errors.push(format!("Error: {} @ {}", e, base.to_str().unwrap())); 197 | return base.to_path_buf(); 198 | } 199 | }; 200 | 201 | let mut parent_abs = base_abs.parent(); 202 | let mut parent = base.to_path_buf(); 203 | if parent.is_dir() { 204 | parent.push(".."); 205 | } else { 206 | parent = parent.parent().unwrap().to_path_buf(); 207 | } 208 | while parent_abs.is_some() { 209 | if self.push_gitignore(&PathBuf::from(&parent)) { 210 | self.infos 211 | .push(format!("Found .gitignore at the parent directory: {:?}\n", parent)); 212 | return base.to_path_buf(); 213 | } 214 | parent_abs = parent_abs.unwrap().parent(); 215 | parent.push(".."); 216 | } 217 | 218 | base.to_path_buf() 219 | } 220 | } 221 | 222 | impl PipelineFork for PipelineFinder { 223 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Vec>>) { 224 | self.infos = Vec::new(); 225 | self.errors = Vec::new(); 226 | let mut seq_beg_arrived = false; 227 | 228 | loop { 229 | match rx.recv() { 230 | Ok(PipelineInfo::SeqDat(_, p)) => { 231 | watch_time!(self.time_bsy, { 232 | let p = self.set_default_gitignore(&p); 233 | self.find_path(p, &tx, false); 234 | }); 235 | } 236 | 237 | Ok(PipelineInfo::SeqBeg(x)) => { 238 | if !seq_beg_arrived { 239 | self.seq_no = x; 240 | self.time_beg = Instant::now(); 241 | 242 | for tx in &tx { 243 | let _ = tx.send(PipelineInfo::SeqBeg(x)); 244 | } 245 | 246 | seq_beg_arrived = true; 247 | } 248 | } 249 | 250 | Ok(PipelineInfo::SeqEnd(_)) => { 251 | for i in &self.infos { 252 | let _ = tx[0].send(PipelineInfo::MsgInfo(id, i.clone())); 253 | } 254 | for e in &self.errors { 255 | let _ = tx[0].send(PipelineInfo::MsgErr(id, e.clone())); 256 | } 257 | 258 | let _ = tx[0].send(PipelineInfo::MsgTime(id, self.time_bsy, self.time_beg.elapsed())); 259 | 260 | for tx in &tx { 261 | let _ = tx.send(PipelineInfo::SeqEnd(self.seq_no)); 262 | } 263 | 264 | break; 265 | } 266 | 267 | Ok(PipelineInfo::MsgDebug(i, e)) => { 268 | let _ = tx[0].send(PipelineInfo::MsgDebug(i, e)); 269 | } 270 | Ok(PipelineInfo::MsgInfo(i, e)) => { 271 | let _ = tx[0].send(PipelineInfo::MsgInfo(i, e)); 272 | } 273 | Ok(PipelineInfo::MsgErr(i, e)) => { 274 | let _ = tx[0].send(PipelineInfo::MsgErr(i, e)); 275 | } 276 | Ok(PipelineInfo::MsgTime(i, t0, t1)) => { 277 | let _ = tx[0].send(PipelineInfo::MsgTime(i, t0, t1)); 278 | } 279 | Err(_) => break, 280 | } 281 | } 282 | } 283 | } 284 | 285 | // --------------------------------------------------------------------------------------------------------------------- 286 | // Test 287 | // --------------------------------------------------------------------------------------------------------------------- 288 | 289 | #[cfg(test)] 290 | mod tests { 291 | use super::*; 292 | use crate::pipeline::{PipelineFork, PipelineInfo}; 293 | use crossbeam::channel::unbounded; 294 | use std::path::{Path, PathBuf}; 295 | use std::thread; 296 | 297 | fn test + Send>(mut finder: T, path: String) -> Vec { 298 | let (in_tx, in_rx) = unbounded(); 299 | let (out_tx, out_rx) = unbounded(); 300 | thread::spawn(move || { 301 | finder.setup(0, in_rx, vec![out_tx]); 302 | }); 303 | let _ = in_tx.send(PipelineInfo::SeqBeg(0)); 304 | let _ = in_tx.send(PipelineInfo::SeqDat(0, PathBuf::from(path))); 305 | let _ = in_tx.send(PipelineInfo::SeqEnd(1)); 306 | 307 | let mut ret = Vec::new(); 308 | loop { 309 | match out_rx.recv().unwrap() { 310 | PipelineInfo::SeqDat(_, x) => ret.push(x), 311 | PipelineInfo::SeqEnd(_) => break, 312 | _ => (), 313 | } 314 | } 315 | 316 | ret 317 | } 318 | 319 | #[test] 320 | fn pipeline_finder_default() { 321 | if !Path::new("./.git/config").exists() { 322 | fs::create_dir_all("./.git").unwrap(); 323 | fs::File::create("./.git/config").unwrap(); 324 | } 325 | 326 | let finder = PipelineFinder::new(); 327 | let ret = test(finder, "./".to_string()); 328 | 329 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./Cargo.toml"))); 330 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/ambr.rs"))); 331 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/ambs.rs"))); 332 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/console.rs"))); 333 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/lib.rs"))); 334 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/matcher.rs"))); 335 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/util.rs"))); 336 | assert!(!ret.iter().any(|x| x.path == PathBuf::from("./.git/config"))); 337 | } 338 | 339 | #[test] 340 | fn pipeline_finder_not_skip_vcs() { 341 | if !Path::new("./.git/config").exists() { 342 | fs::create_dir_all("./.git").unwrap(); 343 | fs::File::create("./.git/config").unwrap(); 344 | } 345 | 346 | let mut finder = PipelineFinder::new(); 347 | finder.skip_vcs = false; 348 | let ret = test(finder, "./".to_string()); 349 | 350 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./Cargo.toml"))); 351 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/ambr.rs"))); 352 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/ambs.rs"))); 353 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/console.rs"))); 354 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/lib.rs"))); 355 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/matcher.rs"))); 356 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./src/util.rs"))); 357 | assert!(ret.iter().any(|x| x.path == PathBuf::from("./.git/config"))); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/pipeline_matcher.rs: -------------------------------------------------------------------------------- 1 | use crate::matcher::{Match, Matcher}; 2 | use crate::pipeline::{Pipeline, PipelineInfo}; 3 | use crate::pipeline_finder::PathInfo; 4 | use crate::util::{catch, decode_error}; 5 | use crossbeam::channel::{Receiver, Sender}; 6 | use memmap::Mmap; 7 | use std::fs::{self, File}; 8 | use std::io::{Error, Read}; 9 | use std::ops::Deref; 10 | use std::path::PathBuf; 11 | use std::time::{Duration, Instant}; 12 | 13 | // --------------------------------------------------------------------------------------------------------------------- 14 | // PathMatch 15 | // --------------------------------------------------------------------------------------------------------------------- 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct PathMatch { 19 | pub path: PathBuf, 20 | pub matches: Vec, 21 | } 22 | 23 | // --------------------------------------------------------------------------------------------------------------------- 24 | // PipelineMatcher 25 | // --------------------------------------------------------------------------------------------------------------------- 26 | 27 | pub struct PipelineMatcher { 28 | pub skip_binary: bool, 29 | pub print_skipped: bool, 30 | pub print_search: bool, 31 | pub binary_check_bytes: usize, 32 | pub mmap_bytes: u64, 33 | pub infos: Vec, 34 | pub errors: Vec, 35 | time_beg: Instant, 36 | time_bsy: Duration, 37 | matcher: T, 38 | keyword: Vec, 39 | } 40 | 41 | impl PipelineMatcher { 42 | pub fn new(matcher: T, keyword: &[u8]) -> Self { 43 | PipelineMatcher { 44 | skip_binary: true, 45 | print_skipped: false, 46 | print_search: false, 47 | binary_check_bytes: 128, 48 | mmap_bytes: 1024 * 1024, 49 | infos: Vec::new(), 50 | errors: Vec::new(), 51 | time_beg: Instant::now(), 52 | time_bsy: Duration::new(0, 0), 53 | matcher, 54 | keyword: Vec::from(keyword), 55 | } 56 | } 57 | 58 | fn search_path(&mut self, info: PathInfo) -> PathMatch { 59 | let path_org = info.path.clone(); 60 | 61 | let result = catch::<_, PathMatch, Error>(|| { 62 | let attr = match fs::metadata(&info.path) { 63 | Ok(x) => x, 64 | Err(e) => { 65 | return Err(e); 66 | } 67 | }; 68 | 69 | let mmap; 70 | let mut buf = Vec::new(); 71 | let src = if attr.len() > self.mmap_bytes { 72 | let file = File::open(&info.path)?; 73 | mmap = unsafe { Mmap::map(&file) }?; 74 | mmap.deref() 75 | } else { 76 | let mut f = File::open(&info.path)?; 77 | f.read_to_end(&mut buf)?; 78 | &buf[..] 79 | }; 80 | 81 | if self.skip_binary { 82 | let mut is_binary = false; 83 | let check_bytes = if self.binary_check_bytes < src.len() { 84 | self.binary_check_bytes 85 | } else { 86 | src.len() 87 | }; 88 | for byte in src.iter().take(check_bytes) { 89 | if byte <= &0x08 { 90 | is_binary = true; 91 | break; 92 | } 93 | } 94 | if is_binary { 95 | if self.print_skipped { 96 | self.infos.push(format!("Skip (binary) : {:?}", info.path)); 97 | } 98 | return Ok(PathMatch { 99 | path: info.path.clone(), 100 | matches: Vec::new(), 101 | }); 102 | } 103 | } 104 | 105 | let ret = self.matcher.search(src, &self.keyword); 106 | 107 | Ok(PathMatch { 108 | path: info.path.clone(), 109 | matches: ret, 110 | }) 111 | }); 112 | 113 | match result { 114 | Ok(x) => x, 115 | Err(e) => { 116 | self.errors 117 | .push(format!("Error: {} @ {:?}\n", decode_error(e.kind()), path_org)); 118 | PathMatch { 119 | path: info.path.clone(), 120 | matches: Vec::new(), 121 | } 122 | } 123 | } 124 | } 125 | } 126 | 127 | impl Pipeline for PipelineMatcher { 128 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Sender>) { 129 | self.infos = Vec::new(); 130 | self.errors = Vec::new(); 131 | let mut seq_beg_arrived = false; 132 | 133 | loop { 134 | match rx.recv() { 135 | Ok(PipelineInfo::SeqDat(x, p)) => { 136 | let mut path = None; 137 | if self.print_search { 138 | path = Some(p.path.clone()); 139 | let _ = tx.send(PipelineInfo::MsgDebug(id, format!("Search Start : {:?}", p.path))); 140 | } 141 | watch_time!(self.time_bsy, { 142 | let ret = self.search_path(p); 143 | let _ = tx.send(PipelineInfo::SeqDat(x, ret)); 144 | }); 145 | if self.print_search { 146 | let _ = tx.send(PipelineInfo::MsgDebug( 147 | id, 148 | format!("Search Finish : {:?}", path.unwrap()), 149 | )); 150 | } 151 | } 152 | 153 | Ok(PipelineInfo::SeqBeg(x)) => { 154 | if !seq_beg_arrived { 155 | self.time_beg = Instant::now(); 156 | let _ = tx.send(PipelineInfo::SeqBeg(x)); 157 | seq_beg_arrived = true; 158 | } 159 | } 160 | 161 | Ok(PipelineInfo::SeqEnd(x)) => { 162 | for i in &self.infos { 163 | let _ = tx.send(PipelineInfo::MsgInfo(id, i.clone())); 164 | } 165 | for e in &self.errors { 166 | let _ = tx.send(PipelineInfo::MsgErr(id, e.clone())); 167 | } 168 | 169 | let _ = tx.send(PipelineInfo::MsgTime(id, self.time_bsy, self.time_beg.elapsed())); 170 | let _ = tx.send(PipelineInfo::SeqEnd(x)); 171 | break; 172 | } 173 | 174 | Ok(PipelineInfo::MsgDebug(i, e)) => { 175 | let _ = tx.send(PipelineInfo::MsgDebug(i, e)); 176 | } 177 | Ok(PipelineInfo::MsgInfo(i, e)) => { 178 | let _ = tx.send(PipelineInfo::MsgInfo(i, e)); 179 | } 180 | Ok(PipelineInfo::MsgErr(i, e)) => { 181 | let _ = tx.send(PipelineInfo::MsgErr(i, e)); 182 | } 183 | Ok(PipelineInfo::MsgTime(i, t0, t1)) => { 184 | let _ = tx.send(PipelineInfo::MsgTime(i, t0, t1)); 185 | } 186 | Err(_) => break, 187 | } 188 | } 189 | } 190 | } 191 | 192 | // --------------------------------------------------------------------------------------------------------------------- 193 | // Test 194 | // --------------------------------------------------------------------------------------------------------------------- 195 | 196 | #[cfg(test)] 197 | mod tests { 198 | use super::*; 199 | use crate::matcher::QuickSearchMatcher; 200 | use crate::pipeline::{Pipeline, PipelineInfo}; 201 | use crate::pipeline_finder::PathInfo; 202 | use crossbeam::channel::unbounded; 203 | use std::path::PathBuf; 204 | use std::thread; 205 | 206 | #[test] 207 | fn pipeline_matcher() { 208 | let qs = QuickSearchMatcher::new(); 209 | let mut matcher = PipelineMatcher::new(qs, &"amber".to_string().into_bytes()); 210 | 211 | let (in_tx, in_rx) = unbounded(); 212 | let (out_tx, out_rx) = unbounded(); 213 | thread::spawn(move || { 214 | matcher.setup(0, in_rx, out_tx); 215 | }); 216 | 217 | let _ = in_tx.send(PipelineInfo::SeqBeg(0)); 218 | let _ = in_tx.send(PipelineInfo::SeqDat( 219 | 0, 220 | PathInfo { 221 | path: PathBuf::from("./src/ambs.rs"), 222 | }, 223 | )); 224 | let _ = in_tx.send(PipelineInfo::SeqDat( 225 | 1, 226 | PathInfo { 227 | path: PathBuf::from("./src/ambr.rs"), 228 | }, 229 | )); 230 | let _ = in_tx.send(PipelineInfo::SeqDat( 231 | 2, 232 | PathInfo { 233 | path: PathBuf::from("./src/console.rs"), 234 | }, 235 | )); 236 | let _ = in_tx.send(PipelineInfo::SeqEnd(3)); 237 | 238 | let mut ret = Vec::new(); 239 | loop { 240 | match out_rx.recv().unwrap() { 241 | PipelineInfo::SeqDat(_, x) => ret.push(x), 242 | PipelineInfo::SeqEnd(_) => break, 243 | _ => (), 244 | } 245 | } 246 | 247 | for r in ret { 248 | if r.path == PathBuf::from("./src/ambs.rs") { 249 | assert!(!r.matches.is_empty()); 250 | } 251 | if r.path == PathBuf::from("./src/ambr.rs") { 252 | assert!(!r.matches.is_empty()); 253 | } 254 | if r.path == PathBuf::from("./src/console.rs") { 255 | assert!(r.matches.is_empty()); 256 | } 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/pipeline_printer.rs: -------------------------------------------------------------------------------- 1 | use crate::console::{Console, ConsoleTextKind}; 2 | use crate::pipeline::{Pipeline, PipelineInfo}; 3 | use crate::pipeline_matcher::PathMatch; 4 | use crate::util::{catch, decode_error}; 5 | use crossbeam::channel::{Receiver, Sender}; 6 | use memmap::Mmap; 7 | use std::fs::File; 8 | use std::io::Error; 9 | use std::ops::Deref; 10 | use std::time::{Duration, Instant}; 11 | 12 | // --------------------------------------------------------------------------------------------------------------------- 13 | // PipelinePrinter 14 | // --------------------------------------------------------------------------------------------------------------------- 15 | 16 | pub struct PipelinePrinter { 17 | pub is_color: bool, 18 | pub print_file: bool, 19 | pub print_column: bool, 20 | pub print_row: bool, 21 | pub print_line_by_match: bool, 22 | pub infos: Vec, 23 | pub errors: Vec, 24 | console: Console, 25 | time_beg: Instant, 26 | time_bsy: Duration, 27 | } 28 | 29 | impl Default for PipelinePrinter { 30 | fn default() -> Self { 31 | Self::new() 32 | } 33 | } 34 | 35 | impl PipelinePrinter { 36 | pub fn new() -> Self { 37 | PipelinePrinter { 38 | is_color: true, 39 | print_file: true, 40 | print_column: false, 41 | print_row: false, 42 | print_line_by_match: false, 43 | infos: Vec::new(), 44 | errors: Vec::new(), 45 | console: Console::new(), 46 | time_beg: Instant::now(), 47 | time_bsy: Duration::new(0, 0), 48 | } 49 | } 50 | 51 | fn print_match(&mut self, pm: PathMatch) { 52 | if pm.matches.is_empty() { 53 | return; 54 | } 55 | self.console.is_color = self.is_color; 56 | 57 | let result = catch::<_, (), Error>(|| { 58 | let file = File::open(&pm.path)?; 59 | let mmap = unsafe { Mmap::map(&file) }?; 60 | let src = mmap.deref(); 61 | 62 | let mut pos = 0; 63 | let mut column = 0; 64 | let mut last_lf = 0; 65 | let mut last_line_beg = usize::MAX; 66 | let mut last_m_end = usize::MAX; 67 | 68 | if self.print_line_by_match { 69 | for m in &pm.matches { 70 | if self.print_file { 71 | self.console.write(ConsoleTextKind::Filename, pm.path.to_str().unwrap()); 72 | self.console.write(ConsoleTextKind::Filename, ":"); 73 | } 74 | if self.print_column | self.print_row { 75 | while pos < m.beg { 76 | if src[pos] == 0x0a { 77 | column += 1; 78 | last_lf = pos; 79 | } 80 | pos += 1; 81 | } 82 | if self.print_column { 83 | self.console.write(ConsoleTextKind::Other, &format!("{}:", column + 1)); 84 | } 85 | if self.print_row { 86 | self.console 87 | .write(ConsoleTextKind::Other, &format!("{}:", m.beg - last_lf)); 88 | } 89 | } 90 | 91 | self.console.write_match_line(src, m); 92 | } 93 | } else { 94 | for m in &pm.matches { 95 | let line_beg = Console::get_line_beg(src, m.beg); 96 | 97 | if last_line_beg != line_beg { 98 | if last_m_end != usize::MAX { 99 | let line_end = Console::get_line_end(src, last_m_end); 100 | self.console.write_to_linebreak(src, last_m_end, line_end); 101 | } 102 | 103 | if self.print_file { 104 | self.console.write(ConsoleTextKind::Filename, pm.path.to_str().unwrap()); 105 | self.console.write(ConsoleTextKind::Filename, ":"); 106 | } 107 | if self.print_column | self.print_row { 108 | while pos < m.beg { 109 | if src[pos] == 0x0a { 110 | column += 1; 111 | last_lf = pos; 112 | } 113 | pos += 1; 114 | } 115 | if self.print_column { 116 | self.console.write(ConsoleTextKind::Other, &format!("{}:", column + 1)); 117 | } 118 | if self.print_row { 119 | self.console 120 | .write(ConsoleTextKind::Other, &format!("{}:", m.beg - last_lf)); 121 | } 122 | } 123 | 124 | self.console.write_match_part(src, m, line_beg); 125 | } else { 126 | self.console.write_match_part(src, m, last_m_end); 127 | } 128 | 129 | last_line_beg = line_beg; 130 | last_m_end = m.end; 131 | } 132 | 133 | if last_m_end != usize::MAX { 134 | let line_end = Console::get_line_end(src, last_m_end); 135 | self.console.write_to_linebreak(src, last_m_end, line_end); 136 | } 137 | } 138 | 139 | Ok(()) 140 | }); 141 | match result { 142 | Ok(_) => (), 143 | Err(e) => self.console.write( 144 | ConsoleTextKind::Error, 145 | &format!("Error: {} @ {:?}\n", decode_error(e.kind()), pm.path), 146 | ), 147 | } 148 | } 149 | } 150 | 151 | impl Pipeline for PipelinePrinter { 152 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Sender>) { 153 | self.infos = Vec::new(); 154 | self.errors = Vec::new(); 155 | let mut seq_beg_arrived = false; 156 | 157 | loop { 158 | match rx.recv() { 159 | Ok(PipelineInfo::SeqDat(x, pm)) => { 160 | watch_time!(self.time_bsy, { 161 | self.print_match(pm); 162 | let _ = tx.send(PipelineInfo::SeqDat(x, ())); 163 | }); 164 | } 165 | 166 | Ok(PipelineInfo::SeqBeg(x)) => { 167 | if !seq_beg_arrived { 168 | self.time_beg = Instant::now(); 169 | let _ = tx.send(PipelineInfo::SeqBeg(x)); 170 | seq_beg_arrived = true; 171 | } 172 | } 173 | 174 | Ok(PipelineInfo::SeqEnd(x)) => { 175 | for i in &self.infos { 176 | let _ = tx.send(PipelineInfo::MsgInfo(id, i.clone())); 177 | } 178 | for e in &self.errors { 179 | let _ = tx.send(PipelineInfo::MsgErr(id, e.clone())); 180 | } 181 | 182 | let _ = tx.send(PipelineInfo::MsgTime(id, self.time_bsy, self.time_beg.elapsed())); 183 | let _ = tx.send(PipelineInfo::SeqEnd(x)); 184 | break; 185 | } 186 | 187 | Ok(PipelineInfo::MsgDebug(_, e)) => { 188 | self.console.write(ConsoleTextKind::Info, &format!("{}\n", e)); 189 | } 190 | Ok(PipelineInfo::MsgInfo(i, e)) => { 191 | let _ = tx.send(PipelineInfo::MsgInfo(i, e)); 192 | } 193 | Ok(PipelineInfo::MsgErr(i, e)) => { 194 | let _ = tx.send(PipelineInfo::MsgErr(i, e)); 195 | } 196 | Ok(PipelineInfo::MsgTime(i, t0, t1)) => { 197 | let _ = tx.send(PipelineInfo::MsgTime(i, t0, t1)); 198 | } 199 | Err(_) => break, 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/pipeline_replacer.rs: -------------------------------------------------------------------------------- 1 | use crate::console::{Console, ConsoleTextKind}; 2 | use crate::pipeline::{Pipeline, PipelineInfo}; 3 | use crate::pipeline_matcher::PathMatch; 4 | use crate::util::{catch, decode_error, exit}; 5 | use crossbeam::channel::{Receiver, Sender}; 6 | use ctrlc; 7 | use filetime::FileTime; 8 | use getch::Getch; 9 | use memmap::Mmap; 10 | use regex::Regex; 11 | use std::collections::HashSet; 12 | use std::fs::{self, File}; 13 | use std::io::{Error, Write}; 14 | use std::ops::Deref; 15 | use std::path::PathBuf; 16 | use std::str; 17 | use std::time::{Duration, Instant}; 18 | use tempfile::NamedTempFile; 19 | use unicode_width::UnicodeWidthStr; 20 | 21 | // --------------------------------------------------------------------------------------------------------------------- 22 | // PipelineReplacer 23 | // --------------------------------------------------------------------------------------------------------------------- 24 | 25 | pub struct PipelineReplacer { 26 | pub is_color: bool, 27 | pub is_interactive: bool, 28 | pub preserve_time: bool, 29 | pub print_file: bool, 30 | pub print_column: bool, 31 | pub print_row: bool, 32 | pub infos: Vec, 33 | pub errors: Vec, 34 | console: Console, 35 | all_replace: bool, 36 | keyword: Vec, 37 | replacement: Vec, 38 | regex: bool, 39 | time_beg: Instant, 40 | time_bsy: Duration, 41 | replaced_paths: HashSet, 42 | } 43 | 44 | impl PipelineReplacer { 45 | pub fn new(keyword: &[u8], replacement: &[u8], regex: bool) -> Self { 46 | PipelineReplacer { 47 | is_color: true, 48 | is_interactive: true, 49 | preserve_time: false, 50 | print_file: true, 51 | print_column: false, 52 | print_row: false, 53 | infos: Vec::new(), 54 | errors: Vec::new(), 55 | console: Console::new(), 56 | all_replace: false, 57 | keyword: Vec::from(keyword), 58 | replacement: Vec::from(replacement), 59 | regex, 60 | time_beg: Instant::now(), 61 | time_bsy: Duration::new(0, 0), 62 | replaced_paths: HashSet::default(), 63 | } 64 | } 65 | 66 | fn replace_match(&mut self, pm: PathMatch) { 67 | if pm.matches.is_empty() { 68 | return; 69 | } 70 | 71 | // Check duplicate paths caused by symlink 72 | if let Ok(path) = pm.path.canonicalize() { 73 | if self.replaced_paths.contains(&path) { 74 | return; 75 | } else { 76 | self.replaced_paths.insert(path); 77 | } 78 | } else { 79 | return; 80 | } 81 | 82 | self.console.is_color = self.is_color; 83 | 84 | let result = catch::<_, (), Error>(|| { 85 | let mut tmpfile = NamedTempFile::new_in(pm.path.parent().unwrap_or(&pm.path))?; 86 | 87 | let tmpfile_path = tmpfile.path().to_path_buf(); 88 | #[cfg(not(windows))] 89 | let c_lflag = crate::util::get_c_lflag(); 90 | let _ = ctrlc::set_handler(move || { 91 | let path = tmpfile_path.clone(); 92 | let mut console = Console::new(); 93 | console.write( 94 | ConsoleTextKind::Info, 95 | &format!("\nCleanup temporary file: {:?}\n", path), 96 | ); 97 | let _ = fs::remove_file(path); 98 | #[cfg(not(windows))] 99 | crate::util::set_c_lflag(c_lflag); 100 | exit(0, &mut console); 101 | }); 102 | 103 | { 104 | let file = File::open(&pm.path)?; 105 | let mmap = unsafe { Mmap::map(&file) }?; 106 | let src = mmap.deref(); 107 | 108 | let mut i = 0; 109 | let mut pos = 0; 110 | let mut column = 0; 111 | let mut last_lf = 0; 112 | for m in &pm.matches { 113 | tmpfile.write_all(&src[i..m.beg])?; 114 | 115 | let replacement = if self.regex { 116 | self.get_regex_replacement(&src[m.beg..m.end]) 117 | } else { 118 | self.replacement.clone() 119 | }; 120 | 121 | let mut do_replace = true; 122 | if self.is_interactive & !self.all_replace { 123 | let mut header_width = 0; 124 | if self.print_file { 125 | let path = pm.path.to_str().unwrap(); 126 | header_width += UnicodeWidthStr::width(path) + 2; 127 | self.console.write(ConsoleTextKind::Filename, path); 128 | self.console.write(ConsoleTextKind::Other, ": "); 129 | } 130 | if self.print_column | self.print_row { 131 | while pos < m.beg { 132 | if src[pos] == 0x0a { 133 | column += 1; 134 | last_lf = pos; 135 | } 136 | pos += 1; 137 | } 138 | if self.print_column { 139 | let column_str = format!("{}:", column + 1); 140 | header_width += column_str.width(); 141 | self.console.write(ConsoleTextKind::Other, &column_str); 142 | } 143 | if self.print_row { 144 | let row_str = format!("{}:", m.beg - last_lf); 145 | header_width += row_str.width(); 146 | self.console.write(ConsoleTextKind::Other, &row_str); 147 | } 148 | } 149 | 150 | if header_width < 4 { 151 | self.console 152 | .write(ConsoleTextKind::Other, &" ".repeat(4 - header_width).to_string()); 153 | header_width = 4; 154 | } 155 | 156 | self.console.write_match_line(src, m); 157 | self.console 158 | .write(ConsoleTextKind::Other, &format!("{} -> ", " ".repeat(header_width - 4))); 159 | self.console.write_replace_line(src, m, &replacement); 160 | 161 | let getch = Getch::new(); 162 | loop { 163 | self.console 164 | .write(ConsoleTextKind::Other, "Replace keyword? [Y]es/[n]o/[a]ll/[q]uit: "); 165 | self.console.flush(); 166 | let key = char::from(getch.getch()?); 167 | if key != '\n' { 168 | self.console.write(ConsoleTextKind::Other, &format!("{}\n", key)); 169 | } else { 170 | self.console.write(ConsoleTextKind::Other, "\n"); 171 | } 172 | match key { 173 | 'Y' | 'y' | ' ' | '\r' | '\n' => do_replace = true, 174 | 'N' | 'n' => do_replace = false, 175 | 'A' | 'a' => self.all_replace = true, 176 | 'Q' | 'q' => { 177 | let _ = tmpfile.close(); 178 | #[cfg(not(windows))] 179 | crate::util::set_c_lflag(c_lflag); 180 | exit(0, &mut self.console); 181 | } 182 | _ => continue, 183 | } 184 | break; 185 | } 186 | } 187 | 188 | if do_replace { 189 | tmpfile.write_all(&replacement)?; 190 | } else { 191 | tmpfile.write_all(&src[m.beg..m.end])?; 192 | } 193 | i = m.end; 194 | } 195 | 196 | if i < src.len() { 197 | tmpfile.write_all(&src[i..src.len()])?; 198 | } 199 | tmpfile.flush()?; 200 | } 201 | 202 | let real_path = fs::canonicalize(&pm.path)?; 203 | 204 | let metadata = fs::metadata(&real_path)?; 205 | 206 | let time = if self.preserve_time { 207 | let mtime = FileTime::from_last_modification_time(&metadata); 208 | let atime = FileTime::from_last_access_time(&metadata); 209 | Some((mtime, atime)) 210 | } else { 211 | None 212 | }; 213 | 214 | fs::set_permissions(tmpfile.path(), metadata.permissions())?; 215 | tmpfile.persist(&real_path)?; 216 | 217 | if let Some((mtime, atime)) = time { 218 | filetime::set_file_times(&real_path, atime, mtime)?; 219 | } 220 | 221 | Ok(()) 222 | }); 223 | match result { 224 | Ok(_) => (), 225 | Err(e) => self.console.write( 226 | ConsoleTextKind::Error, 227 | &format!("Error: {} @ {:?}\n", decode_error(e.kind()), pm.path), 228 | ), 229 | } 230 | } 231 | 232 | fn get_regex_replacement(&self, org: &[u8]) -> Vec { 233 | // All unwrap() is safe because keyword is already matched in pipeline_matcher 234 | let org = str::from_utf8(org).unwrap(); 235 | let keyword = str::from_utf8(&self.keyword).unwrap(); 236 | // `\b` may not be matched with `org` because `\b` is affected by the character before and 237 | // after `org`. 238 | let keyword = keyword.trim_start_matches("\\b").trim_end_matches("\\b"); 239 | let replacement = str::from_utf8(&self.replacement).unwrap(); 240 | let regex = Regex::new(keyword).unwrap(); 241 | let captures = regex.captures(org).unwrap(); 242 | 243 | let mut dst = String::new(); 244 | captures.expand(replacement, &mut dst); 245 | 246 | dst.into_bytes() 247 | } 248 | } 249 | 250 | impl Pipeline for PipelineReplacer { 251 | fn setup(&mut self, id: usize, rx: Receiver>, tx: Sender>) { 252 | self.infos = Vec::new(); 253 | self.errors = Vec::new(); 254 | let mut seq_beg_arrived = false; 255 | 256 | loop { 257 | match rx.recv() { 258 | Ok(PipelineInfo::SeqDat(x, pm)) => { 259 | watch_time!(self.time_bsy, { 260 | self.replace_match(pm); 261 | let _ = tx.send(PipelineInfo::SeqDat(x, ())); 262 | }); 263 | } 264 | 265 | Ok(PipelineInfo::SeqBeg(x)) => { 266 | if !seq_beg_arrived { 267 | self.time_beg = Instant::now(); 268 | let _ = tx.send(PipelineInfo::SeqBeg(x)); 269 | seq_beg_arrived = true; 270 | } 271 | } 272 | 273 | Ok(PipelineInfo::SeqEnd(x)) => { 274 | for i in &self.infos { 275 | let _ = tx.send(PipelineInfo::MsgInfo(id, i.clone())); 276 | } 277 | for e in &self.errors { 278 | let _ = tx.send(PipelineInfo::MsgErr(id, e.clone())); 279 | } 280 | 281 | let _ = tx.send(PipelineInfo::MsgTime(id, self.time_bsy, self.time_beg.elapsed())); 282 | let _ = tx.send(PipelineInfo::SeqEnd(x)); 283 | break; 284 | } 285 | 286 | Ok(PipelineInfo::MsgDebug(i, e)) => { 287 | let _ = tx.send(PipelineInfo::MsgDebug(i, e)); 288 | } 289 | Ok(PipelineInfo::MsgInfo(i, e)) => { 290 | let _ = tx.send(PipelineInfo::MsgInfo(i, e)); 291 | } 292 | Ok(PipelineInfo::MsgErr(i, e)) => { 293 | let _ = tx.send(PipelineInfo::MsgErr(i, e)); 294 | } 295 | Ok(PipelineInfo::MsgTime(i, t0, t1)) => { 296 | let _ = tx.send(PipelineInfo::MsgTime(i, t0, t1)); 297 | } 298 | Err(_) => break, 299 | } 300 | } 301 | } 302 | } 303 | 304 | // --------------------------------------------------------------------------------------------------------------------- 305 | // Test 306 | // --------------------------------------------------------------------------------------------------------------------- 307 | -------------------------------------------------------------------------------- /src/pipeline_sorter.rs: -------------------------------------------------------------------------------- 1 | use crate::pipeline::{PipelineInfo, PipelineJoin}; 2 | use crate::pipeline_matcher::PathMatch; 3 | use crossbeam::channel::{Receiver, Sender}; 4 | use std::collections::HashMap; 5 | use std::time::{Duration, Instant}; 6 | 7 | // --------------------------------------------------------------------------------------------------------------------- 8 | // PipelineSorter 9 | // --------------------------------------------------------------------------------------------------------------------- 10 | 11 | pub struct PipelineSorter { 12 | pub infos: Vec, 13 | pub errors: Vec, 14 | pub through: bool, 15 | map: HashMap, 16 | seq_no: usize, 17 | join_num: usize, 18 | time_beg: Instant, 19 | time_bsy: Duration, 20 | } 21 | 22 | impl PipelineSorter { 23 | pub fn new(num: usize) -> Self { 24 | PipelineSorter { 25 | infos: Vec::new(), 26 | errors: Vec::new(), 27 | through: false, 28 | map: HashMap::new(), 29 | seq_no: 0, 30 | join_num: num, 31 | time_beg: Instant::now(), 32 | time_bsy: Duration::new(0, 0), 33 | } 34 | } 35 | } 36 | 37 | impl PipelineJoin for PipelineSorter { 38 | fn setup(&mut self, id: usize, rx: Vec>>, tx: Sender>) { 39 | self.infos = Vec::new(); 40 | self.errors = Vec::new(); 41 | let mut seq_beg_arrived = false; 42 | let mut end_num = 0; 43 | 44 | loop { 45 | for rx in &rx { 46 | match rx.recv() { 47 | Ok(PipelineInfo::SeqDat(x, p)) => { 48 | watch_time!(self.time_bsy, { 49 | if self.through { 50 | let _ = tx.send(PipelineInfo::SeqDat(x, p)); 51 | } else { 52 | self.map.insert(x, p); 53 | loop { 54 | if !self.map.contains_key(&self.seq_no) { 55 | break; 56 | } 57 | { 58 | let ret = self.map.get(&self.seq_no).unwrap(); 59 | let _ = tx.send(PipelineInfo::SeqDat(self.seq_no, ret.clone())); 60 | } 61 | let _ = self.map.remove(&self.seq_no); 62 | self.seq_no += 1; 63 | } 64 | } 65 | }); 66 | } 67 | 68 | Ok(PipelineInfo::SeqBeg(x)) => { 69 | if !seq_beg_arrived { 70 | self.seq_no = x; 71 | self.time_beg = Instant::now(); 72 | let _ = tx.send(PipelineInfo::SeqBeg(x)); 73 | seq_beg_arrived = true; 74 | } 75 | } 76 | 77 | Ok(PipelineInfo::SeqEnd(x)) => { 78 | end_num += 1; 79 | if end_num != self.join_num { 80 | continue; 81 | } 82 | 83 | for i in &self.infos { 84 | let _ = tx.send(PipelineInfo::MsgInfo(id, i.clone())); 85 | } 86 | for e in &self.errors { 87 | let _ = tx.send(PipelineInfo::MsgErr(id, e.clone())); 88 | } 89 | 90 | let _ = tx.send(PipelineInfo::MsgTime(id, self.time_bsy, self.time_beg.elapsed())); 91 | let _ = tx.send(PipelineInfo::SeqEnd(x)); 92 | break; 93 | } 94 | 95 | Ok(PipelineInfo::MsgDebug(i, e)) => { 96 | let _ = tx.send(PipelineInfo::MsgDebug(i, e)); 97 | } 98 | Ok(PipelineInfo::MsgInfo(i, e)) => { 99 | let _ = tx.send(PipelineInfo::MsgInfo(i, e)); 100 | } 101 | Ok(PipelineInfo::MsgErr(i, e)) => { 102 | let _ = tx.send(PipelineInfo::MsgErr(i, e)); 103 | } 104 | Ok(PipelineInfo::MsgTime(i, t0, t1)) => { 105 | let _ = tx.send(PipelineInfo::MsgTime(i, t0, t1)); 106 | } 107 | Err(_) => break, 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // --------------------------------------------------------------------------------------------------------------------- 115 | // Test 116 | // --------------------------------------------------------------------------------------------------------------------- 117 | 118 | #[cfg(test)] 119 | mod tests { 120 | use super::*; 121 | use crate::pipeline::{PipelineInfo, PipelineJoin}; 122 | use crate::pipeline_matcher::PathMatch; 123 | use crossbeam::channel::unbounded; 124 | use std::path::PathBuf; 125 | use std::thread; 126 | 127 | #[test] 128 | fn pipeline_sorter() { 129 | let mut sorter = PipelineSorter::new(1); 130 | 131 | let (in_tx, in_rx) = unbounded(); 132 | let (out_tx, out_rx) = unbounded(); 133 | thread::spawn(move || { 134 | sorter.setup(0, vec![in_rx], out_tx); 135 | }); 136 | 137 | let _ = in_tx.send(PipelineInfo::SeqBeg(0)); 138 | let _ = in_tx.send(PipelineInfo::SeqDat( 139 | 2, 140 | PathMatch { 141 | path: PathBuf::from("./"), 142 | matches: Vec::new(), 143 | }, 144 | )); 145 | let _ = in_tx.send(PipelineInfo::SeqDat( 146 | 1, 147 | PathMatch { 148 | path: PathBuf::from("./"), 149 | matches: Vec::new(), 150 | }, 151 | )); 152 | let _ = in_tx.send(PipelineInfo::SeqDat( 153 | 0, 154 | PathMatch { 155 | path: PathBuf::from("./"), 156 | matches: Vec::new(), 157 | }, 158 | )); 159 | let _ = in_tx.send(PipelineInfo::SeqEnd(3)); 160 | 161 | let mut ret = Vec::new(); 162 | loop { 163 | match out_rx.recv().unwrap() { 164 | PipelineInfo::SeqDat(x, _) => ret.push(x), 165 | PipelineInfo::SeqEnd(_) => break, 166 | _ => (), 167 | } 168 | } 169 | 170 | assert_eq!(ret[0], 0); 171 | assert_eq!(ret[1], 1); 172 | assert_eq!(ret[2], 2); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::console::Console; 2 | use std::fs::File; 3 | use std::io::{BufReader, Error, ErrorKind, Read}; 4 | use std::path::PathBuf; 5 | use std::process; 6 | use std::time::{Duration, Instant}; 7 | 8 | // --------------------------------------------------------------------------------------------------------------------- 9 | // Utility 10 | // --------------------------------------------------------------------------------------------------------------------- 11 | 12 | #[cfg(feature = "statistics")] 13 | macro_rules! watch_time ( 14 | ( $total:expr, $func:block ) => ( 15 | { 16 | let beg = Instant::now(); 17 | $func; 18 | $total += beg.elapsed(); 19 | } 20 | ); 21 | ); 22 | 23 | #[cfg(not(feature = "statistics"))] 24 | macro_rules! watch_time ( 25 | ( $total:expr, $func:block ) => ( 26 | { 27 | $func; 28 | } 29 | ); 30 | ); 31 | 32 | pub fn watch_time(closure: F) -> Duration 33 | where 34 | F: FnOnce(), 35 | { 36 | let start = Instant::now(); 37 | closure(); 38 | start.elapsed() 39 | } 40 | 41 | pub fn catch(closure: F) -> Result 42 | where 43 | F: FnOnce() -> Result, 44 | { 45 | closure() 46 | } 47 | 48 | pub fn as_secsf64(dur: Duration) -> f64 { 49 | (dur.as_secs() as f64) + (dur.subsec_nanos() as f64 / 1_000_000_000.0) 50 | } 51 | 52 | pub fn read_from_file(path: &str) -> Result, Error> { 53 | let file = match File::open(path) { 54 | Ok(x) => x, 55 | Err(e) => return Err(e), 56 | }; 57 | let mut reader = BufReader::new(file); 58 | let mut ret: String = String::new(); 59 | let _ = reader.read_to_string(&mut ret); 60 | Ok(ret.into_bytes()) 61 | } 62 | 63 | pub fn decode_error(e: ErrorKind) -> &'static str { 64 | match e { 65 | ErrorKind::NotFound => "file not found", 66 | ErrorKind::PermissionDenied => "permission denied", 67 | ErrorKind::ConnectionRefused => "connection refused", 68 | ErrorKind::ConnectionReset => "connection reset", 69 | ErrorKind::ConnectionAborted => "connection aborted", 70 | ErrorKind::NotConnected => "not connected", 71 | ErrorKind::AddrInUse => "address is in use", 72 | ErrorKind::AddrNotAvailable => "address is not available", 73 | ErrorKind::BrokenPipe => "broken pipe", 74 | ErrorKind::AlreadyExists => "file is already exists", 75 | ErrorKind::WouldBlock => "world be blocked", 76 | ErrorKind::InvalidInput => "invalid parameter", 77 | ErrorKind::InvalidData => "invalid data", 78 | ErrorKind::TimedOut => "operation timeout", 79 | ErrorKind::WriteZero => "write size is zero", 80 | ErrorKind::Interrupted => "interrupted", 81 | ErrorKind::Other => "unknown", 82 | _ => "unknown", 83 | } 84 | } 85 | 86 | pub enum PipelineInfo { 87 | Beg(usize), 88 | Ok(T), 89 | Info(String), 90 | Err(String), 91 | Time(Duration, Duration), 92 | End(usize), 93 | } 94 | 95 | pub fn exit(code: i32, console: &mut Console) -> ! { 96 | console.reset(); 97 | console.flush(); 98 | process::exit(code); 99 | } 100 | 101 | pub fn handle_escape(text: &str) -> String { 102 | let text = text.replace("\\n", "\n"); 103 | let text = text.replace("\\r", "\r"); 104 | let text = text.replace("\\t", "\t"); 105 | let text = text.replace("\\\\", "\\"); 106 | text 107 | } 108 | 109 | pub fn get_config(name: &str) -> Option { 110 | let dot_cfg_path = directories::BaseDirs::new() 111 | .map(|base| base.home_dir().join(&format!(".{}", name))) 112 | .filter(|path| path.exists()); 113 | let app_cfg_path = directories::ProjectDirs::from("com.github", "dalance", "amber") 114 | .map(|proj| proj.preference_dir().join(name)) 115 | .filter(|path| path.exists()); 116 | let xdg_cfg_path = directories::BaseDirs::new() 117 | .map(|base| base.home_dir().join(".config").join("amber").join(name)) 118 | .filter(|path| path.exists()); 119 | let etc_path = PathBuf::from(format!("/etc/amber/{}", name)); 120 | let etc_cfg_path = etc_path.exists().then_some(etc_path); 121 | dot_cfg_path.or(app_cfg_path).or(xdg_cfg_path).or(etc_cfg_path) 122 | } 123 | 124 | #[cfg(not(windows))] 125 | pub fn set_c_lflag(c_lflag: Option) { 126 | if let Ok(mut termios) = termios::Termios::from_fd(0) { 127 | if let Some(c_lflag) = c_lflag { 128 | termios.c_lflag = c_lflag; 129 | let _ = termios::tcsetattr(0, termios::TCSADRAIN, &termios); 130 | } 131 | } 132 | } 133 | 134 | #[cfg(not(windows))] 135 | pub fn get_c_lflag() -> Option { 136 | if let Ok(termios) = termios::Termios::from_fd(0) { 137 | Some(termios.c_lflag) 138 | } else { 139 | None 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/.bzr/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | # Comment 2 | # Comment 3 | 4 | 5 | 6 | *.o 7 | ?.s 8 | d[0-9].t 9 | 10 | /file 11 | /dir0/file 12 | dir1/file 13 | 14 | /dir2 15 | /dir3/dir4 16 | dir5/dir6 17 | 18 | dir7/ 19 | 20 | /dir8/ 21 | /dir9/dir10/ 22 | dir11/dir12/ 23 | 24 | /**/file2 25 | -------------------------------------------------------------------------------- /test/.hg/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/.svn/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/a.o: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/a.s: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/abc.o: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/abc.s: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/ao: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/amber/b8c464f68a74116dd2660bc60f8d9e20549417e2/test/ao -------------------------------------------------------------------------------- /test/d0.t: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/d00.t: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/amber/b8c464f68a74116dd2660bc60f8d9e20549417e2/test/d00.t -------------------------------------------------------------------------------- /test/dir0/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir1/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir11/dir12/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir2/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir3/dir4/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir3/dir7/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir5/dir6/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir7/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir8/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/dir9/dir10/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/file: -------------------------------------------------------------------------------- 1 | amber 2 | -------------------------------------------------------------------------------- /test/x/dir0/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/amber/b8c464f68a74116dd2660bc60f8d9e20549417e2/test/x/dir0/file -------------------------------------------------------------------------------- /test/x/dir1/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/amber/b8c464f68a74116dd2660bc60f8d9e20549417e2/test/x/dir1/file -------------------------------------------------------------------------------- /test/x/file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalance/amber/b8c464f68a74116dd2660bc60f8d9e20549417e2/test/x/file --------------------------------------------------------------------------------