mirror of
https://github.com/sharkdp/fd.git
synced 2026-09-01 04:28:07 +08:00
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9be32fa6a7 | |||
| 5ed7514096 | |||
| 3c5d8a1e86 | |||
| ccdbc76f5a | |||
| 355a171557 | |||
| 5f4a1203b4 | |||
| 0f82fe7ec0 | |||
| c18caaa6f6 | |||
| bc88b8f80e | |||
| 18709b1ede | |||
| 1a6c92c475 | |||
| 3104729f29 | |||
| 673392045f | |||
| 26f71dd21a | |||
| c022528f54 | |||
| 46db4a3860 | |||
| b3f1e1844d | |||
| bba5c4f607 | |||
| 614f576c47 | |||
| dc29ef70c4 | |||
| d702d849ea | |||
| 6b232a82ee | |||
| 87ba4e3bb0 | |||
| 9d26b74c2a | |||
| 7b8d9322ba | |||
| 5ad69fb2fb | |||
| 19afb15a98 | |||
| f4ed2ba74d | |||
| e38b7d7bff | |||
| 1ec204caa8 | |||
| dbf1ba8b82 | |||
| 4bf7ab67d0 | |||
| 7ecb623950 | |||
| 5863a5e706 | |||
| d32fe0241e | |||
| e9cf8af911 | |||
| 15a3ca0c80 | |||
| 701b8f209b | |||
| 570ea78d24 | |||
| ce73c086b6 | |||
| 6a01faf6e1 | |||
| 200dfaa20a | |||
| 6dc704a18e | |||
| 715b07599b | |||
| d2d2c31307 | |||
| f7f5192139 | |||
| ddfa0873fb | |||
| b6c95a4800 | |||
| 8d85debc12 | |||
| 37def9bb83 | |||
| e0eab07881 | |||
| 16eed82ca1 | |||
| 2a23905af5 | |||
| 54d9dde0d4 | |||
| e649c8fa79 | |||
| fe5d8aa29e | |||
| ecfd5568cf | |||
| 08aa06a0ad | |||
| ad1b4f3b2b | |||
| 1bc58b2fbb | |||
| 718f723d31 | |||
| 884bd41cae | |||
| fb1cd3a322 | |||
| db5f2fc3ac | |||
| 137fe2e697 | |||
| 4a5a5faf4d | |||
| f2632d5fce | |||
| c38ef0e9b2 | |||
| be2238ddf4 | |||
| 7150c9a3a9 | |||
| 0b04f39398 | |||
| 0677e6331d | |||
| b441528067 | |||
| 8fc3a83d92 | |||
| 97e3110333 | |||
| 049b9ec06b | |||
| e075e1cf7c | |||
| 35e5ecd492 | |||
| 4f2d9e77b3 | |||
| ccb899a511 | |||
| affe41949d | |||
| 68a85c7e5b | |||
| b9b6964f30 | |||
| 0cd9d8c3a6 | |||
| 108fce3396 | |||
| a7f4a0a581 | |||
| b98e4c9931 | |||
| 43b124cf19 | |||
| 20e179c91d | |||
| d8da6ec83c | |||
| 4731dc670c | |||
| a84536a173 | |||
| 080e8a9b6e | |||
| 3b55edf5dd | |||
| 0aae4a3d46 | |||
| 34e4620e26 | |||
| 2403ac350a | |||
| 6f22957cdc | |||
| 00b57b50ae | |||
| 396b9baaa5 | |||
| 083b1cfd9b | |||
| 4ec33d064c | |||
| 7399558a06 | |||
| 4b61beaa75 | |||
| 2256affd8f | |||
| b0fb774c0e | |||
| 32a321aafd | |||
| 83db8673cf | |||
| 8073635177 | |||
| 60df14f0a3 | |||
| 042959a4af | |||
| 89124b317e | |||
| adaf4f6d73 | |||
| d8c588d805 | |||
| df44407b84 | |||
| c1b8d1eae1 | |||
| 2a3dd5b631 | |||
| fbce0d4f39 | |||
| 1397dbcc6f | |||
| 2def50bddd | |||
| 6c69b98b49 | |||
| fe26f71582 | |||
| 3afd216008 | |||
| f1f537b83a | |||
| a2349848c0 | |||
| 916b55fae1 | |||
| f41c7a4344 | |||
| 4884c23283 | |||
| d67b1bba61 | |||
| b34716051a | |||
| fde8f2e8e3 | |||
| a790f9bd62 | |||
| b4c8a8ba56 | |||
| 1268e982f6 | |||
| 31c76989ad | |||
| f92dfb86bb | |||
| c55b255a3e | |||
| 977e0ac694 | |||
| 550e3b3572 | |||
| e06189e654 | |||
| 2d1d24c38c | |||
| 10ea476e31 | |||
| 1586d653ae | |||
| fb5ba2c068 | |||
| f8b02ac5ae | |||
| 1b0c8f349a | |||
| 918e4a014a | |||
| f5261595a7 | |||
| d8d0cf5d6e | |||
| eb728756ed | |||
| 3f69c0fa1a | |||
| dfd7433347 | |||
| d3d7460707 | |||
| 54f33e5227 | |||
| 1b1f644398 | |||
| 24070b0be0 | |||
| 087e709acd | |||
| 1adcf76f1b | |||
| c74c2ca39b | |||
| c8d4ec89a3 | |||
| f7910c125d | |||
| 2ea23c0000 | |||
| 641ca69b49 | |||
| 8c6a9c8e32 | |||
| 4d950ae97c | |||
| 8adb5b9f84 | |||
| db8d0cc342 | |||
| 634016b9a0 | |||
| bf013da8ff | |||
| 0f5ede48d5 | |||
| 002983b15b | |||
| 787e459c90 | |||
| 1430c1d7bf | |||
| ec1f23cf34 | |||
| 7b1a457214 | |||
| 7f49489aa2 | |||
| 961169aff9 | |||
| c6209471a2 | |||
| d29115b4da | |||
| 0757a50c31 | |||
| 10cd964c1d | |||
| ae12e00533 | |||
| b18eded6aa | |||
| c45cf26577 | |||
| f9cb3326c8 | |||
| bb63a4c6d7 |
+144
@@ -1 +1,145 @@
|
||||
language: rust
|
||||
cache: cargo
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
include:
|
||||
# Stable channel.
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=i686-unknown-linux-gnu
|
||||
- os: linux
|
||||
rust: stable
|
||||
env: TARGET=i686-unknown-linux-musl
|
||||
- os: osx
|
||||
rust: stable
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
|
||||
# Beta channel.
|
||||
- os: linux
|
||||
rust: beta
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
# Disabled to reduce total CI time
|
||||
# - os: linux
|
||||
# rust: beta
|
||||
# env: TARGET=x86_64-unknown-linux-musl
|
||||
# - os: linux
|
||||
# rust: beta
|
||||
# env: TARGET=i686-unknown-linux-gnu
|
||||
# - os: linux
|
||||
# rust: beta
|
||||
# env: TARGET=i686-unknown-linux-musl
|
||||
# - os: osx
|
||||
# rust: beta
|
||||
# env: TARGET=x86_64-apple-darwin
|
||||
|
||||
# Nightly channel.
|
||||
- os: linux
|
||||
rust: nightly
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
# Disabled to reduce total CI time
|
||||
# - os: linux
|
||||
# rust: nightly
|
||||
# env: TARGET=x86_64-unknown-linux-musl
|
||||
# - os: linux
|
||||
# rust: nightly
|
||||
# env: TARGET=i686-unknown-linux-gnu
|
||||
# - os: linux
|
||||
# rust: nightly
|
||||
# env: TARGET=i686-unknown-linux-musl
|
||||
# - os: osx
|
||||
# rust: nightly
|
||||
# env: TARGET=x86_64-apple-darwin
|
||||
|
||||
# Minimum Rust supported channel.
|
||||
- os: linux
|
||||
rust: 1.19.0
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- os: linux
|
||||
rust: 1.19.0
|
||||
env: TARGET=x86_64-unknown-linux-musl
|
||||
- os: linux
|
||||
rust: 1.19.0
|
||||
env: TARGET=i686-unknown-linux-gnu
|
||||
- os: linux
|
||||
rust: 1.19.0
|
||||
env: TARGET=i686-unknown-linux-musl
|
||||
- os: osx
|
||||
rust: 1.19.0
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
|
||||
# Code formatting check
|
||||
- os: linux
|
||||
rust: stable
|
||||
# skip the global install step
|
||||
install:
|
||||
- which cargo-install-update || cargo install cargo-update
|
||||
- cargo install-update rustfmt
|
||||
script: cargo fmt -- --write-mode=diff
|
||||
- os: linux
|
||||
rust: nightly
|
||||
# skip the global install step
|
||||
install:
|
||||
- which cargo-install-update || cargo install cargo-update
|
||||
- cargo install-update rustfmt-nightly
|
||||
script: cargo fmt -- --write-mode=diff
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
# needed for i686-unknown-linux-gnu target
|
||||
- gcc-multilib
|
||||
|
||||
env:
|
||||
global:
|
||||
# Default target on travis-ci.
|
||||
# Used as conditional check in the install stage
|
||||
- HOST=x86_64-unknown-linux-gnu
|
||||
# Used on the deployment script
|
||||
- PROJECT_NAME=fd
|
||||
|
||||
install:
|
||||
# prevent target re-add error from rustup
|
||||
- if [[ $TRAVIS_OS_NAME = linux && $HOST != $TARGET ]]; then rustup target add $TARGET; fi
|
||||
|
||||
script:
|
||||
# Incorporate TARGET env var to the build and test process
|
||||
- cargo build --target $TARGET --verbose
|
||||
- cargo test --target $TARGET --verbose
|
||||
|
||||
before_deploy:
|
||||
- bash ci/before_deploy.bash
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
# NOTE updating the `api_key.secure`
|
||||
# - go to: https://github.com/settings/tokens/new
|
||||
# - generate new token using `public_repo` scope
|
||||
# - encrypt it using: `travis encrypt API_KEY_HERE`
|
||||
# - paste the output below
|
||||
api_key:
|
||||
secure: "RyFdh2lpDmaNhPar7ezsb18Xz+6XFM40y7cZCDRML+Sk+eYK1xtDNfEhDRJU5Qo1ReVsByds/QJTSXr2KmZPk3lXwG3SiN7UtrLUxCxFr6qrcM/iujlKTf5UxeRklkzPXxnH95DEyEgxvgbVhWTGVDWoyMnrVQXZKDy6z1iAiYB5h2Zl1rs+MRb/Enlt5q6XIKAlG0ppGtl8CfYudq5ZiqfJaMWTt9SWm2YskC8FeMc0S3IM6/EhTvaNYLdaarFqVWQEVql+6oCuL3ayPzmGyxLdxM37tIMNQ0f97zxqWodacXTG5ULdRD8if1l/SmTujrtjbZ0KWRjsjOq4vBtxBJKGdprcSiB0xH/hToqqtTSO0z5FPXi5cB8UlK6YLDDHcP3kXNer8CYMLI1VPaUDLTF57/0/RPi2DZiiGfZsIAS6PsICbHdTQVzxQckM4lN1vnAGgkhXIMbztml21pv+QrGy98OZJ0ubf5ztgQhpT0WPH4JXT8M6htsoo8dZf8lQ5aLfmW9RKePJDqixQwPqmimPIkrlxRDTDGII0ZAZws7l779eOLmEcM2tH2HbsUKUCZIG/pRHLSlP45Jn2bULGzuXZ2daq70z6zvIbom0CUzSXIvdTXEZI2AM5RBvPYGGaKI8YlxgRdQvJp3h0BzPdFOXI3RAxscCY7PJpa/RdIg="
|
||||
# for uploading multiple files
|
||||
file_glob: true
|
||||
# NOTE explanation on each env variable
|
||||
# - PROJECT_NAME: name of the project, set on the `env.global` above
|
||||
# - TRAVIS_TAG: tag name that the build is being deployed for, usually the version number
|
||||
# - TARGET: target triple of the build
|
||||
file: $PROJECT_NAME-$TRAVIS_TAG-$TARGET.*
|
||||
# don't delete artifacts from previous stage
|
||||
skip_cleanup: true
|
||||
on:
|
||||
# deploy only if we push a tag
|
||||
tags: true
|
||||
# deploy only on stable channel that has TARGET env variable sets
|
||||
condition: $TRAVIS_RUST_VERSION = stable && $TARGET != ""
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
## Contributing to *fd*
|
||||
|
||||
**Thank you very much for considering to contribute to this project!**
|
||||
|
||||
We welcome any form of contribution:
|
||||
|
||||
* New issues (feature requests, bug reports, questions, ideas, ...)
|
||||
* Pull requests (documentation improvements, code improvements, new features, ...)
|
||||
|
||||
**Note**: Before you take the time to open a pull request, please open a ticket first. This will
|
||||
give us the chance to discuss any potential changes first.
|
||||
|
||||
## Important links
|
||||
|
||||
* [Open issues](https://github.com/sharkdp/fd/issues)
|
||||
* [Open pull requests](https://github.com/sharkdp/fd/pulls)
|
||||
* [Development section in the README](https://github.com/sharkdp/fd#development)
|
||||
* [fd on crates.io](https://crates.io/crates/fd-find)
|
||||
* [LICENSE](https://github.com/sharkdp/fd/blob/master/LICENSE)
|
||||
|
||||
Generated
+250
-36
@@ -1,12 +1,21 @@
|
||||
[root]
|
||||
name = "fd"
|
||||
version = "0.3.0"
|
||||
name = "fd-find"
|
||||
version = "5.0.1"
|
||||
dependencies = [
|
||||
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"isatty 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diff 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ignore 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -23,20 +32,96 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "isatty"
|
||||
version = "0.1.3"
|
||||
name = "atty"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conv"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "custom_derive"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"globset 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
@@ -47,33 +132,90 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.22"
|
||||
name = "lazy_static"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "magenta"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "magenta-sys"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
@@ -86,26 +228,64 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread-id"
|
||||
version = "3.0.0"
|
||||
name = "strsim"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term_size"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.3"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unreachable"
|
||||
version = "0.1.1"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
@@ -116,6 +296,16 @@ name = "utf8-ranges"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "void"
|
||||
version = "1.0.2"
|
||||
@@ -144,18 +334,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.6.3 (registry+https://github.com/rust-lang/crates.io-index)" = "500909c4f87a9e52355b26626d890833e9e1d53ac566db76c36faa984b889699"
|
||||
"checksum ansi_term 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "23ac7c30002a5accbf7e8987d0632fa6de155b7c3d39d0067317a391e00a2ef6"
|
||||
"checksum getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9047cfbd08a437050b363d35ef160452c5fe8ea5187ae0a624708c91581d685"
|
||||
"checksum isatty 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "fa500db770a99afe2a0f2229be2a3d09c7ed9d7e4e8440bf71253141994e240f"
|
||||
"checksum atty 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "21e50800ec991574876040fff8ee46b136a53e985286fbe6a3bdfe6421b78860"
|
||||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d"
|
||||
"checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5"
|
||||
"checksum clap 2.26.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3451e409013178663435d6f15fdb212f14ee4424a3d74f979d081d0a66b6f1f2"
|
||||
"checksum conv 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "78ff10625fd0ac447827aa30ea8b861fead473bb60aeb73af6c1c58caf0d1299"
|
||||
"checksum crossbeam 0.2.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0c5ea215664ca264da8a9d9c3be80d2eaf30923c259d03e870388eb927508f97"
|
||||
"checksum custom_derive 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "ef8ae57c4978a2acd8b869ce6b9ca1dfe817bff704c220209fdef2c0b75a01b9"
|
||||
"checksum diff 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "0a515461b6c8c08419850ced27bc29e86166dcdcde8fbe76f8b1f0589bb49472"
|
||||
"checksum fnv 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc484842f1e2884faf56f529f960cc12ad8c71ce96cc7abba0a067c98fee344"
|
||||
"checksum globset 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "feeb1b6840809ef5efcf7a4a990bc4e1b7ee3df8cf9e2379a75aeb2ba42ac9c3"
|
||||
"checksum ignore 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b3fcaf2365eb14b28ec7603c98c06cc531f19de9eb283d89a3dff8417c8c99f5"
|
||||
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
"checksum libc 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)" = "babb8281da88cba992fa1f4ddec7d63ed96280a1a53ec9b919fd37b53d71e502"
|
||||
"checksum lazy_static 0.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c9e5e58fa1a4c3b915a561a78a22ee0cac6ab97dca2504428bc1cb074375f8d5"
|
||||
"checksum libc 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)" = "d1419b2939a0bc44b77feb34661583c7546b532b192feab36249ab584b86856c"
|
||||
"checksum log 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "880f77541efa6e5cc74e76910c9884d9859683118839d6a1dc3b11e63512565b"
|
||||
"checksum magenta 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4bf0336886480e671965f794bc9b6fce88503563013d1bfb7a502c81fe3ac527"
|
||||
"checksum magenta-sys 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "40d014c7011ac470ae28e2f76a02bfea4a8480f73e701353b49ad7a8d75f4699"
|
||||
"checksum memchr 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1dbccc0e46f1ea47b9f17e6d67c5a96bd27030519c519c9c91327e31275a47b4"
|
||||
"checksum regex 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4278c17d0f6d62dfef0ab00028feb45bd7d2102843f80763474eeb1be8a10c01"
|
||||
"checksum regex-syntax 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9191b1f57603095f105d317e375d19b1c9c5c3185ea9633a99a6dcbed04457"
|
||||
"checksum num_cpus 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "514f0d73e64be53ff320680ca671b64fe3fb91da01e1ae2ddc99eb51d453b20d"
|
||||
"checksum rand 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)" = "eb250fd207a4729c976794d03db689c9be1d634ab5a1c9da9492a13d8fecbcdf"
|
||||
"checksum redox_syscall 0.1.31 (registry+https://github.com/rust-lang/crates.io-index)" = "8dde11f18c108289bef24469638a04dce49da56084f2d50618b226e47eb04509"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1731164734096285ec2a5ec7fea5248ae2f5485b3feeb0115af4fda2183b2d1b"
|
||||
"checksum regex-syntax 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ad890a5eef7953f55427c50575c680c42841653abd2b028b68cd223d157f62db"
|
||||
"checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7"
|
||||
"checksum thread-id 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4437c97558c70d129e40629a5b385b3fb1ffac301e63941335e4d354081ec14a"
|
||||
"checksum thread_local 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c85048c6260d17cf486ceae3282d9fb6b90be220bf5b28c400f5485ffc29f0c7"
|
||||
"checksum unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f2ae5ddb18e1c92664717616dd9549dde73f539f01bd7b77c2edb2446bdff91"
|
||||
"checksum strsim 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d15c810519a91cf877e7e36e63fe068815c678181439f2f29e2562147c3694"
|
||||
"checksum tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "87974a6f5c1dfb344d733055601650059a3363de2a6104819293baff662132d6"
|
||||
"checksum term_size 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2b6b55df3198cc93372e85dd2ed817f0e38ce8cc0f22eb32391bfad9c4bf209"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df8e08afc40ae3459e4838f303e465aa50d823df8d7f83ca88108f6d3afe7edd"
|
||||
"checksum thread_local 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1697c4b57aeeb7a536b647165a2825faddffb1d3bad386d507709bd51a90bb14"
|
||||
"checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f"
|
||||
"checksum unreachable 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
|
||||
"checksum utf8-ranges 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "662fab6525a98beff2921d7f61a39e7d59e0b425ebc7d0d9e66d316e55124122"
|
||||
"checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c"
|
||||
"checksum version_check 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6b772017e347561807c1aa192438c5fd74242a670a6cffacc40f2defd1dc069d"
|
||||
"checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||
"checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff"
|
||||
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
|
||||
+47
-5
@@ -1,11 +1,53 @@
|
||||
[package]
|
||||
name = "fd"
|
||||
version = "0.3.0"
|
||||
authors = ["David Peter <mail@david-peter.de>"]
|
||||
build = "build.rs"
|
||||
categories = ["command-line-utilities"]
|
||||
description = "fd is a simple, fast and user-friendly alternative to find."
|
||||
exclude = ["/benchmarks/*"]
|
||||
homepage = "https://github.com/sharkdp/fd"
|
||||
keywords = [
|
||||
"search",
|
||||
"find",
|
||||
"file",
|
||||
"filesystem",
|
||||
"tool",
|
||||
]
|
||||
license = "MIT/Apache-2.0"
|
||||
name = "fd-find"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/sharkdp/fd"
|
||||
version = "6.0.0"
|
||||
|
||||
[[bin]]
|
||||
name = "fd"
|
||||
path = "src/main.rs"
|
||||
[badges.appveyor]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[badges.travis-ci]
|
||||
repository = "sharkdp/fd"
|
||||
|
||||
[build-dependencies]
|
||||
clap = "2.26.0"
|
||||
version_check = "0.1.3"
|
||||
|
||||
[dependencies]
|
||||
ansi_term = "0.9"
|
||||
getopts = "0.2"
|
||||
isatty = "0.1"
|
||||
atty = "0.2"
|
||||
clap = "2.26.0"
|
||||
ignore = "0.2"
|
||||
lazy_static = "0.2.9"
|
||||
num_cpus = "1.6.2"
|
||||
regex = "0.2"
|
||||
walkdir = "1"
|
||||
regex-syntax = "0.4"
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "redox")))'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
kernel32-sys = "0.2"
|
||||
winapi = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
diff = "0.1"
|
||||
tempdir = "0.3"
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 David Peter
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2017 fd developers
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,65 +1,301 @@
|
||||
# fd
|
||||
[](https://travis-ci.org/sharkdp/fd)
|
||||
[](https://ci.appveyor.com/project/sharkdp/fd)
|
||||
[](https://crates.io/crates/fd-find)
|
||||
|
||||
*fd* is a simple, fast and user-friendly alternative to [*find*](https://www.gnu.org/software/findutils/).
|
||||
*fd* is a simple, fast and user-friendly alternative to
|
||||
[*find*](https://www.gnu.org/software/findutils/).
|
||||
|
||||
While it does not seek to mirror all of *find*'s powerful functionality, it provides sensible (opinionated)
|
||||
defaults for [80%](https://en.wikipedia.org/wiki/Pareto_principle) of the use cases.
|
||||
While it does not seek to mirror all of *find*'s powerful functionality, it provides sensible
|
||||
(opinionated) defaults for [80%](https://en.wikipedia.org/wiki/Pareto_principle) of the use cases.
|
||||
|
||||
## Features
|
||||
* Convenient syntax: `fd PATTERN` instead of `find -iname '*PATTERN*'`.
|
||||
* Colorized terminal output (similar to *ls*).
|
||||
* It's *fast* (see [benchmarks](#benchmark) below).
|
||||
* Smart case: the search is case-insensitive by default. It switches to
|
||||
case-sensitive if the pattern contains an uppercase
|
||||
character[\*](http://vimdoc.sourceforge.net/htmldoc/options.html#'smartcase').
|
||||
* Ignores hidden directories and files by default.
|
||||
* Colorized terminal output (similar to *ls*).
|
||||
* Regular expressions by default.
|
||||
* Unicode-aware.
|
||||
* The command name is *50%* shorter[\*](https://github.com/ggreer/the_silver_searcher) than `find` :-).
|
||||
* Ignores hidden directories and files, by default.
|
||||
* Ignores patterns from your `.gitignore`, by default.
|
||||
* Regular expressions.
|
||||
* Unicode-awareness.
|
||||
* The command name is *50%* shorter[\*](https://github.com/ggreer/the_silver_searcher) than
|
||||
`find` :-).
|
||||
* Parallel command execution with a syntax similar to GNU Parallel.
|
||||
|
||||
## Demo
|
||||
|
||||

|
||||

|
||||
|
||||
## Benchmark
|
||||
Let's search my home folder for files that end in `[0-9].jpg`. It contains ~150.000
|
||||
subdirectories and about a million files. For averaging and statistical analysis, I'm using
|
||||
[bench](https://github.com/Gabriel439/bench). All benchmarks are performed for a "warm
|
||||
cache". Results for a cold cache are similar.
|
||||
|
||||
Let's start with `find`:
|
||||
```
|
||||
find ~ -iregex '.*[0-9]\.jpg$'
|
||||
|
||||
time 6.265 s (6.127 s .. NaN s)
|
||||
1.000 R² (1.000 R² .. 1.000 R²)
|
||||
mean 6.162 s (6.140 s .. 6.181 s)
|
||||
std dev 31.73 ms (0.0 s .. 33.48 ms)
|
||||
```
|
||||
|
||||
`find` is much faster if it does not need to perform a regular-expression search:
|
||||
```
|
||||
find ~ -iname '*[0-9].jpg'
|
||||
|
||||
time 2.866 s (2.754 s .. 2.964 s)
|
||||
1.000 R² (0.999 R² .. 1.000 R²)
|
||||
mean 2.860 s (2.834 s .. 2.875 s)
|
||||
std dev 23.11 ms (0.0 s .. 25.09 ms)
|
||||
```
|
||||
|
||||
Now let's try the same for `fd`. Note that `fd` *always* performs a regular expression
|
||||
search. The options `--hidden` and `--no-ignore` are needed for a fair comparison,
|
||||
otherwise `fd` does not have to traverse hidden folders and ignored paths (see below):
|
||||
```
|
||||
fd --hidden --no-ignore '.*[0-9]\.jpg$' ~
|
||||
|
||||
time 892.6 ms (839.0 ms .. 915.4 ms)
|
||||
0.999 R² (0.997 R² .. 1.000 R²)
|
||||
mean 871.2 ms (857.9 ms .. 881.3 ms)
|
||||
std dev 15.50 ms (0.0 s .. 17.49 ms)
|
||||
```
|
||||
For this particular example, `fd` is approximately seven times faster than `find -iregex`
|
||||
and about three times faster than `find -iname`. By the way, both tools found the exact
|
||||
same 14030 files :smile:.
|
||||
|
||||
Finally, let's run `fd` without `--hidden` and `--no-ignore` (this can lead to different
|
||||
search results, of course):
|
||||
```
|
||||
fd '[0-9]\.jpg$' ~
|
||||
|
||||
time 159.5 ms (155.8 ms .. 165.3 ms)
|
||||
0.999 R² (0.996 R² .. 1.000 R²)
|
||||
mean 158.7 ms (156.5 ms .. 161.6 ms)
|
||||
std dev 3.263 ms (2.401 ms .. 4.298 ms)
|
||||
```
|
||||
|
||||
**Note**: This is *one particular* benchmark on *one particular* machine. While I have
|
||||
performed quite a lot of different tests (and found consistent results), things might
|
||||
be different for you! I encourage everyone to try it out on their own.
|
||||
|
||||
Concerning *fd*'s speed, the main credit goes to the `regex` and `ignore` crates that are also used
|
||||
in [ripgrep](https://github.com/BurntSushi/ripgrep) (check it out!).
|
||||
|
||||
## Colorized output
|
||||
*fd* can colorize files by extension, just like *ls*. In order for
|
||||
this to work, you need to set up a `~/.dir_colors` file. The easiest
|
||||
way is to call
|
||||
```
|
||||
dircolors --print-database > ~/.dir_colors
|
||||
```
|
||||
More complete (and more colorful) alternatives can be found
|
||||
`fd` can colorize files by extension, just like `ls`. In order for this to work, the environment
|
||||
variable [`LS_COLORS`](https://linux.die.net/man/5/dir_colors) has to be set. Typically, the value
|
||||
of this variable is set by the `dircolors` command which provides a convenient configuration format
|
||||
to define colors for different file formats.
|
||||
On most distributions, `LS_COLORS` should be set already. If you are looking for alternative, more
|
||||
complete (and more colorful) variants, see
|
||||
[here](https://github.com/seebi/dircolors-solarized) or
|
||||
[here](https://github.com/trapd00r/LS_COLORS).
|
||||
|
||||
## Benchmark
|
||||
A search in my home folder with ~80.000 subdirectories
|
||||
and ~350.000 files. The `--hidden` for `fd` is needed
|
||||
for a fair comparison, as *find* does this by default:
|
||||
``` bash
|
||||
> time fd --hidden '\.jpg$' > /dev/null
|
||||
0,39s user 0,40s system 99% cpu 0,790 total
|
||||
## Parallel command execution
|
||||
If the `-x`/`--exec` option is specified alongside a command template, a job pool will be created
|
||||
for executing commands in parallel for each discovered path as the input. The syntax for generating
|
||||
commands is similar to that of GNU Parallel:
|
||||
|
||||
- `{}`: A placeholder token that will be replaced with the path of the search result
|
||||
(`documents/images/party.jpg`).
|
||||
- `{.}`: Like `{}`, but without the file extension (`documents/images/party`).
|
||||
- `{/}`: A placeholder that will be replaced by the basename of the search result (`party.jpg`).
|
||||
- `{//}`: Uses the parent of the discovered path (`documents/images`).
|
||||
- `{/.}`: Uses the basename, with the extension removed (`party`).
|
||||
|
||||
> time find -iname '*.jpg' > /dev/null
|
||||
0,36s user 0,42s system 98% cpu 0,789 total
|
||||
```
|
||||
Both tools found the exact same 5504 files and have
|
||||
a comparable performance (averaged over multiple runs),
|
||||
even though *fd* performs a regex search.
|
||||
If we do the same for *find*, it is significantly slower:
|
||||
``` bash
|
||||
> time find -iregex '.*\.jpg$' > /dev/null
|
||||
1,29s user 0,41s system 99% cpu 1,705 total
|
||||
# Convert all jpg files to png files:
|
||||
fd -e jpg -x convert {} {.}.png
|
||||
|
||||
# Unpack all zip files (if no placeholder is given, the path is appended):
|
||||
fd -e zip -x unzip
|
||||
|
||||
# Convert all flac files into opus files:
|
||||
fd -e flac -x ffmpeg -i {} -c:a libopus {.}.opus
|
||||
|
||||
# Count the number of lines in Rust files (the command template can be terminated with ';'):
|
||||
fd -x wc -l \; -e rs
|
||||
```
|
||||
|
||||
## Install
|
||||
With [cargo](https://github.com/rust-lang/cargo), you can clone, build and install *fd* with a single command:
|
||||
With Rust's package manager [cargo](https://github.com/rust-lang/cargo), you can install *fd* via:
|
||||
```
|
||||
cargo install --git https://github.com/sharkdp/fd
|
||||
cargo install fd-find
|
||||
```
|
||||
Note that rust version *1.19.0* or later is required.
|
||||
The release page of this repository also includes precompiled binaries for Linux.
|
||||
|
||||
On **macOS**, you can use [Homebrew](http://braumeister.org/formula/fd):
|
||||
```
|
||||
brew install fd
|
||||
```
|
||||
|
||||
On **Arch Linux**, you can install the package from the official repos:
|
||||
```
|
||||
pacman -S fd-rs
|
||||
```
|
||||
|
||||
On **NixOS**, or any Linux distro you can use [Nix](https://nixos.org/nix/):
|
||||
```
|
||||
nix-env -i fd
|
||||
```
|
||||
|
||||
On **Windows**, you can download the pre-built binaries from the [Release page](https://github.com/sharkdp/fd/releases).
|
||||
|
||||
On **FreeBSD**, you can install from ports, it's located in `sysutils/fd`:
|
||||
```
|
||||
portmaster sysutils/fd
|
||||
```
|
||||
|
||||
## Development
|
||||
```bash
|
||||
cargo build --release
|
||||
git clone https://github.com/sharkdp/fd
|
||||
|
||||
# Build
|
||||
cd fd
|
||||
cargo build
|
||||
|
||||
# Run unit tests and integration tests
|
||||
cargo test
|
||||
|
||||
# Install
|
||||
cargo install
|
||||
```
|
||||
|
||||
## Command-line options
|
||||
```
|
||||
USAGE:
|
||||
fd [FLAGS/OPTIONS] [<pattern>] [<path>]
|
||||
|
||||
FLAGS:
|
||||
-H, --hidden Search hidden files and directories
|
||||
-I, --no-ignore Do not respect .(git)ignore files
|
||||
-s, --case-sensitive Case-sensitive search (default: smart case)
|
||||
-i, --ignore-case Case-insensitive search (default: smart case)
|
||||
-a, --absolute-path Show absolute instead of relative paths
|
||||
-L, --follow Follow symbolic links
|
||||
-p, --full-path Search full path (default: file-/dirname only)
|
||||
-0, --print0 Separate results by the null character
|
||||
-h, --help Prints help information
|
||||
-V, --version Prints version information
|
||||
|
||||
OPTIONS:
|
||||
-d, --max-depth <depth> Set maximum search depth (default: none)
|
||||
-t, --type <filetype> Filter by type: f(ile), d(irectory), (sym)l(ink)
|
||||
-e, --extension <ext> Filter by file extension
|
||||
-x, --exec <cmd>... Execute a command for each search result
|
||||
-E, --exclude <pattern>... Exclude entries that match the given glob pattern.
|
||||
-c, --color <when> When to use colors: never, *auto*, always
|
||||
-j, --threads <num> Set number of threads to use for searching &
|
||||
executing
|
||||
|
||||
ARGS:
|
||||
<pattern> the search pattern, a regular expression (optional)
|
||||
<path> the root directory for the filesystem search (optional)
|
||||
```
|
||||
|
||||
## Tutorial
|
||||
|
||||
First, to get an overview of all available command line options, you can either run
|
||||
`fd -h` for a concise help message (see above) or `fd --help` for a more detailed
|
||||
version.
|
||||
|
||||
### Simple search
|
||||
|
||||
*fd* is designed to find entries in your filesystem. The most basic search you can perform is to
|
||||
run *fd* with a single argument: the search pattern. For example, assume that you want to find an
|
||||
old script of yours (the name included `netflix`):
|
||||
``` bash
|
||||
> fd netfl
|
||||
Software/python/imdb-ratings/netflix-details.py
|
||||
```
|
||||
If called with just a single argument like this, *fd* searches the current directory recursively
|
||||
for any entries that *contain* the pattern `netfl`.
|
||||
|
||||
### Regular expression search
|
||||
|
||||
The search pattern is treated as a regular expression. Here, we search for entries that start
|
||||
with `x` and end with `rc`:
|
||||
``` bash
|
||||
> cd /etc
|
||||
> fd '^x.*rc$'
|
||||
X11/xinit/xinitrc
|
||||
X11/xinit/xserverrc
|
||||
```
|
||||
|
||||
### Specifying the root directory
|
||||
|
||||
If we want so search a specific directory, it can be given as a second argument to *fd*:
|
||||
``` bash
|
||||
> fd passwd /etc
|
||||
/etc/default/passwd
|
||||
/etc/pam.d/passwd
|
||||
/etc/passwd
|
||||
```
|
||||
|
||||
### Running *fd* without any arguments
|
||||
|
||||
*fd* can be called with no arguments. This is very useful to get a quick overview of all entries
|
||||
in the current directory, recursively (similar to `ls -R`):
|
||||
``` bash
|
||||
> cd fd/tests
|
||||
> fd
|
||||
testenv
|
||||
testenv/mod.rs
|
||||
tests.rs
|
||||
```
|
||||
|
||||
### Searching for a particular file extension
|
||||
|
||||
Often, we are interested in all files of a particular type. This can be done with the `-e` (or
|
||||
`--extension`) option. Here, we search for all Markdown files in the fd repository:
|
||||
``` bash
|
||||
> cd fd
|
||||
> fd -e md
|
||||
CONTRIBUTING.md
|
||||
README.md
|
||||
```
|
||||
|
||||
The `-e` option can be used in combination with a search pattern:
|
||||
``` bash
|
||||
> fd -e rs mod
|
||||
src/fshelper/mod.rs
|
||||
src/lscolors/mod.rs
|
||||
tests/testenv/mod.rs
|
||||
```
|
||||
|
||||
### Hidden and ignored files
|
||||
By default, *fd* does not search hidden directories and does not show hidden files in the
|
||||
search results. To disable this behavior, we can use the `-H` (or `--hidden`) option:
|
||||
``` bash
|
||||
> fd pre-commit
|
||||
> fd -H pre-commit
|
||||
.git/hooks/pre-commit.sample
|
||||
```
|
||||
|
||||
If we work in a directory that is a Git repository (or includes Git repositories), *fd* does not
|
||||
search folders (and does not show files) that match one of the `.gitignore` patterns. To disable
|
||||
this behavior, we can use the `-I` (or `--ignore`) option:
|
||||
``` bash
|
||||
> fd num_cpu
|
||||
> fd -I num_cpu
|
||||
target/debug/deps/libnum_cpus-f5ce7ef99006aa05.rlib
|
||||
```
|
||||
|
||||
To really search *all* files and directories, simply combine the hidden and ignore features to show
|
||||
everything (`-HI`).
|
||||
|
||||
### Using fd with `xargs` or `parallel`
|
||||
|
||||
If we want to run a command on all search results, we can pipe the output to `xargs`:
|
||||
``` bash
|
||||
> fd -0 -e rs | xargs -0 wc -l
|
||||
```
|
||||
Here, the `-0` option tells *fd* to separate search results by the NULL character (instead of .
|
||||
newlines) In the same way, the `-0` option of `xargs` tells it to read the input in this way .
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Source: https://github.com/starkat99/appveyor-rust/
|
||||
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: fd
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: stable
|
||||
# Beta channel (disabled for speed reasons)
|
||||
#- TARGET: i686-pc-windows-gnu
|
||||
# CHANNEL: beta
|
||||
#- TARGET: i686-pc-windows-msvc
|
||||
# CHANNEL: beta
|
||||
#- TARGET: x86_64-pc-windows-gnu
|
||||
# CHANNEL: beta
|
||||
#- TARGET: x86_64-pc-windows-msvc
|
||||
# CHANNEL: beta
|
||||
# Nightly channel (disabled for speed reasons)
|
||||
#- TARGET: i686-pc-windows-gnu
|
||||
# CHANNEL: nightly
|
||||
#- TARGET: i686-pc-windows-msvc
|
||||
# CHANNEL: nightly
|
||||
#- TARGET: x86_64-pc-windows-gnu
|
||||
# CHANNEL: nightly
|
||||
#- TARGET: x86_64-pc-windows-msvc
|
||||
# CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
# (Based on from https://github.com/rust-lang/libc/blob/master/appveyor.yml)
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
- rustup-init.exe --default-host %TARGET% --default-toolchain %CHANNEL% -y
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents
|
||||
# the "directory does not contain a project or solution file" error.
|
||||
# source: https://github.com/starkat99/appveyor-rust/blob/master/appveyor.yml#L113
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo run
|
||||
- cargo test
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo build --release
|
||||
- mkdir staging
|
||||
- copy target\release\fd.exe staging
|
||||
- ps: copy target\release\build\fd-find*\out\_fd.ps1 staging
|
||||
- cd staging
|
||||
# release zipfile will look like 'rust-everywhere-v1.2.3-x86_64-pc-windows-msvc'
|
||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
# All the zipped artifacts will be deployed
|
||||
artifact: /.*\.zip/
|
||||
# Here's how:
|
||||
# - Go to 'https://github.com/settings/tokens/new' and generate a Token with only the
|
||||
# `public_repo` scope enabled
|
||||
# - Then go to 'https://ci.appveyor.com/tools/encrypt' and enter the newly generated token.
|
||||
# - Enter the "encrypted value" below
|
||||
auth_token:
|
||||
secure: mWJ8ieZdGEgHf232fdMyzb9T1rKbkJivYbp/REMo8ax3X5vkQJDHhFjWeuWA3wIC
|
||||
provider: GitHub
|
||||
# deploy when a new tag is pushed and only on the stable channel
|
||||
on:
|
||||
# channel to use to produce the release artifacts
|
||||
CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
# IMPORTANT Regex to match tags. Required, or appveyor may not trigger deploys when a new tag
|
||||
# is pushed. This regex matches semantic versions like v1.2.3-rc4+2016.02.22
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
# Disable caching, for now
|
||||
#cache:
|
||||
# - '%USERPROFILE%\.cargo'
|
||||
# - 'target -> Cargo.lock'
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate version_check;
|
||||
|
||||
use clap::Shell;
|
||||
use std::io::{self, Write};
|
||||
use std::process::exit;
|
||||
use std::fs;
|
||||
|
||||
include!("src/app.rs");
|
||||
|
||||
fn main() {
|
||||
match version_check::is_min_version("1.19") {
|
||||
// rustc >= 1.19
|
||||
Some((true, _)) => {}
|
||||
// rustc < 1.19 or can't figure it out
|
||||
_ => {
|
||||
writeln!(&mut io::stderr(), "This crate requires rustc >= 1.19").unwrap();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let var = std::env::var_os("SHELL_COMPLETIONS_DIR").or(std::env::var_os("OUT_DIR"));
|
||||
let outdir = match var {
|
||||
None => return,
|
||||
Some(outdir) => outdir,
|
||||
};
|
||||
fs::create_dir_all(&outdir).unwrap();
|
||||
|
||||
let mut app = build_app();
|
||||
app.gen_completions("fd", Shell::Bash, &outdir);
|
||||
app.gen_completions("fd", Shell::Fish, &outdir);
|
||||
app.gen_completions("fd", Shell::Zsh, &outdir);
|
||||
app.gen_completions("fd", Shell::PowerShell, &outdir);
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Building and packaging for release
|
||||
|
||||
set -ex
|
||||
|
||||
build() {
|
||||
cargo build --target "$TARGET" --release --verbose
|
||||
}
|
||||
|
||||
pack() {
|
||||
local tempdir
|
||||
local out_dir
|
||||
local package_name
|
||||
|
||||
tempdir=$(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
out_dir=$(pwd)
|
||||
package_name="$PROJECT_NAME-$TRAVIS_TAG-$TARGET"
|
||||
|
||||
# create a "staging" directory
|
||||
mkdir "$tempdir/$package_name"
|
||||
mkdir "$tempdir/$package_name/autocomplete"
|
||||
|
||||
# copying the main binary
|
||||
cp "target/$TARGET/release/$PROJECT_NAME" "$tempdir/$package_name/"
|
||||
strip "$tempdir/$package_name/$PROJECT_NAME"
|
||||
|
||||
# manpage, readme and license
|
||||
cp "doc/$PROJECT_NAME.1" "$tempdir/$package_name"
|
||||
cp README.md "$tempdir/$package_name"
|
||||
cp LICENSE-MIT "$tempdir/$package_name"
|
||||
cp LICENSE-APACHE "$tempdir/$package_name"
|
||||
|
||||
# various autocomplete
|
||||
cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".bash-completion "$tempdir/$package_name/autocomplete"
|
||||
cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/"$PROJECT_NAME".fish "$tempdir/$package_name/autocomplete"
|
||||
cp target/"$TARGET"/release/build/"$PROJECT_NAME"-*/out/_"$PROJECT_NAME" "$tempdir/$package_name/autocomplete"
|
||||
|
||||
# archiving
|
||||
pushd "$tempdir"
|
||||
tar czf "$out_dir/$package_name.tar.gz" "$package_name"/*
|
||||
popd
|
||||
rm -r "$tempdir"
|
||||
}
|
||||
|
||||
main() {
|
||||
build
|
||||
pack
|
||||
}
|
||||
|
||||
main
|
||||
@@ -0,0 +1,136 @@
|
||||
.TH FD 1
|
||||
.SH NAME
|
||||
fd \- find entries in the filesystem
|
||||
.SH SYNOPSIS
|
||||
.B fd
|
||||
.RB [ \-HIEsiaLp0hV ]
|
||||
.RB [ \-d
|
||||
.IR depth ]
|
||||
.RB [ \-t
|
||||
.IR filetype ]
|
||||
.RB [ \-e
|
||||
.IR ext ]
|
||||
.RB [ \-E
|
||||
.IR exclude ]
|
||||
.RB [ \-c
|
||||
.IR when ]
|
||||
.RB [ \-j
|
||||
.IR num ]
|
||||
.RB [ \-x
|
||||
.IR cmd ]
|
||||
.RI [ pattern ]
|
||||
.RI [ path ]
|
||||
.SH DESCRIPTION
|
||||
.B fd
|
||||
is a simple, fast and user-friendly alternative to
|
||||
.BR find (1).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-H, \-\-hidden
|
||||
Search hidden files and directories.
|
||||
.TP
|
||||
.B \-I, \-\-no\-ignore
|
||||
Do not respect any
|
||||
.BR gitignore (5)
|
||||
files.
|
||||
.TP
|
||||
.B \-s, \-\-case\-sensitive
|
||||
Perform a case-sensitive search (default: smart case).
|
||||
.TP
|
||||
.B \-i, \-\-ignore\-case
|
||||
Perform a case-insensitive search (default: smart case).
|
||||
.TP
|
||||
.B \-a, \-\-absolute\-path
|
||||
Show absolute instead of relative paths.
|
||||
.TP
|
||||
.B \-L, \-\-follow
|
||||
Dereference all symbolic links encountered.
|
||||
.TP
|
||||
.B \-p, \-\-full\-path
|
||||
Match
|
||||
.I pattern
|
||||
against the full path
|
||||
.TP
|
||||
.B \-0, \-\-print0
|
||||
Print results followed by null character instead of newlines.
|
||||
.TP
|
||||
.B \-h, \-\-help
|
||||
Prints help information.
|
||||
.TP
|
||||
.B \-V, \-\-version
|
||||
Prints version information.
|
||||
.TP
|
||||
.BI "\-d, \-\-max\-depth " levels
|
||||
Limit directory traversal to at most
|
||||
.I levels
|
||||
of depth.
|
||||
.TP
|
||||
.BI "\-t, \-\-type " filetype
|
||||
Filter search by type:
|
||||
.RS
|
||||
.IP "f, file"
|
||||
regular files
|
||||
.IP "d, directories"
|
||||
directories
|
||||
.IP "s, symlink"
|
||||
symbolic links
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-e, \-\-extension " ext
|
||||
Filter search results by file extension
|
||||
.IR ext .
|
||||
.TP
|
||||
.BI "\-E, \-\-exclude " pattern
|
||||
Exclude files/directories that match the given glob pattern.
|
||||
This overrides any other ignore logic.
|
||||
Multiple exclude patterns can be specified.
|
||||
.TP
|
||||
.BI "\-c, \-\-color " when
|
||||
Declare
|
||||
.I when
|
||||
to colorize search results:
|
||||
.RS
|
||||
.IP auto
|
||||
Colorize output when standard output is connected to terminal (default).
|
||||
.IP never
|
||||
Do not colorize output.
|
||||
.IP always
|
||||
Always colorize output.
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-j, \-\-threads " num
|
||||
Number of threads to use for searching (default: number of available CPUs).
|
||||
.TP
|
||||
.BI "\-x, \-\-exec " command "\fR [args...] ;"
|
||||
Execute
|
||||
.I command
|
||||
for each search result. The following placeholders are substituted by a path derived from the current search result:
|
||||
.RS
|
||||
.IP {}
|
||||
path
|
||||
.IP {/}
|
||||
basename
|
||||
.IP {//}
|
||||
parent directory
|
||||
.IP {.}
|
||||
path without file extension
|
||||
.IP {/.}
|
||||
basename without file extension
|
||||
.RE
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B LS_COLORS
|
||||
Determines how to colorize search results, see
|
||||
.BR dircolors (1) .
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
.RI "Find files and directories that match the pattern '" needle "':"
|
||||
$ fd needle
|
||||
.TP
|
||||
.RI "Start a search in a given directory (" /var/log "):"
|
||||
$ fd nginx /var/log
|
||||
.TP
|
||||
.RI "Find all Python files (all files with the extention " .py ") in the current directory:"
|
||||
$ fd -e py
|
||||
.SH SEE ALSO
|
||||
.BR find (1)
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use clap::{App, AppSettings, Arg};
|
||||
|
||||
struct Help {
|
||||
short: &'static str,
|
||||
long: &'static str,
|
||||
}
|
||||
|
||||
macro_rules! doc {
|
||||
($map:expr, $name:expr, $short:expr) => {
|
||||
doc!($map, $name, $short, $short)
|
||||
};
|
||||
($map:expr, $name:expr, $short:expr, $long:expr) => {
|
||||
$map.insert($name, Help {
|
||||
short: $short,
|
||||
long: concat!($long, "\n ")
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn build_app() -> App<'static, 'static> {
|
||||
let helps = usage();
|
||||
let arg = |name| {
|
||||
Arg::with_name(name).help(helps[name].short).long_help(
|
||||
helps[name].long,
|
||||
)
|
||||
};
|
||||
|
||||
App::new("fd")
|
||||
.version(crate_version!())
|
||||
.usage("fd [FLAGS/OPTIONS] [<pattern>] [<path>]")
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.setting(AppSettings::DeriveDisplayOrder)
|
||||
.arg(arg("hidden").long("hidden").short("H"))
|
||||
.arg(arg("no-ignore").long("no-ignore").short("I"))
|
||||
.arg(
|
||||
arg("rg-alias-hidden-ignore")
|
||||
.short("u")
|
||||
.multiple(true)
|
||||
.hidden(true),
|
||||
)
|
||||
.arg(
|
||||
arg("case-sensitive")
|
||||
.long("case-sensitive")
|
||||
.short("s")
|
||||
.overrides_with("ignore-case"),
|
||||
)
|
||||
.arg(
|
||||
arg("ignore-case")
|
||||
.long("ignore-case")
|
||||
.short("i")
|
||||
.overrides_with("case-sensitive"),
|
||||
)
|
||||
.arg(arg("absolute-path").long("absolute-path").short("a"))
|
||||
.arg(arg("follow").long("follow").short("L").alias("dereference"))
|
||||
.arg(arg("full-path").long("full-path").short("p"))
|
||||
.arg(arg("null_separator").long("print0").short("0"))
|
||||
.arg(arg("depth").long("max-depth").short("d").takes_value(true))
|
||||
.arg(
|
||||
arg("file-type")
|
||||
.long("type")
|
||||
.short("t")
|
||||
.takes_value(true)
|
||||
.value_name("filetype")
|
||||
.possible_values(&["f", "file", "d", "directory", "l", "symlink"])
|
||||
.hide_possible_values(true),
|
||||
)
|
||||
.arg(
|
||||
arg("extension")
|
||||
.long("extension")
|
||||
.short("e")
|
||||
.takes_value(true)
|
||||
.value_name("ext"),
|
||||
)
|
||||
.arg(
|
||||
arg("exec")
|
||||
.long("exec")
|
||||
.short("x")
|
||||
.multiple(true)
|
||||
.min_values(1)
|
||||
.allow_hyphen_values(true)
|
||||
.value_terminator(";")
|
||||
.value_name("cmd"),
|
||||
)
|
||||
.arg(
|
||||
arg("exclude")
|
||||
.long("exclude")
|
||||
.short("E")
|
||||
.takes_value(true)
|
||||
.value_name("pattern")
|
||||
.number_of_values(1)
|
||||
.multiple(true),
|
||||
)
|
||||
.arg(
|
||||
arg("color")
|
||||
.long("color")
|
||||
.short("c")
|
||||
.takes_value(true)
|
||||
.value_name("when")
|
||||
.possible_values(&["never", "auto", "always"])
|
||||
.hide_possible_values(true),
|
||||
)
|
||||
.arg(
|
||||
arg("threads")
|
||||
.long("threads")
|
||||
.short("j")
|
||||
.takes_value(true)
|
||||
.value_name("num"),
|
||||
)
|
||||
.arg(
|
||||
arg("max-buffer-time")
|
||||
.long("max-buffer-time")
|
||||
.takes_value(true)
|
||||
.hidden(true),
|
||||
)
|
||||
.arg(arg("pattern"))
|
||||
.arg(arg("path"))
|
||||
}
|
||||
|
||||
#[cfg_attr(rustfmt, rustfmt_skip)]
|
||||
fn usage() -> HashMap<&'static str, Help> {
|
||||
let mut h = HashMap::new();
|
||||
doc!(h, "hidden"
|
||||
, "Search hidden files and directories"
|
||||
, "Include hidden directories and files in the search results (default: hidden files \
|
||||
and directories are skipped).");
|
||||
doc!(h, "no-ignore"
|
||||
, "Do not respect .(git)ignore files"
|
||||
, "Show search results from files and directories that would otherwise be ignored by \
|
||||
'.*ignore' files.");
|
||||
doc!(h, "case-sensitive"
|
||||
, "Case-sensitive search (default: smart case)"
|
||||
, "Perform a case-sensitive search. By default, fd uses case-insensitive searches, \
|
||||
unless the pattern contains an uppercase character (smart case).");
|
||||
doc!(h, "ignore-case"
|
||||
, "Case-insensitive search (default: smart case)"
|
||||
, "Perform a case-insensitive search. By default, fd uses case-insensitive searches, \
|
||||
unless the pattern contains an uppercase character (smart case).");
|
||||
doc!(h, "absolute-path"
|
||||
, "Show absolute instead of relative paths"
|
||||
, "Shows the full path starting from the root as opposed to relative paths.");
|
||||
doc!(h, "follow"
|
||||
, "Follow symbolic links"
|
||||
, "By default, fd does not descend into symlinked directories. Using this flag, symbolic \
|
||||
links are also traversed.");
|
||||
doc!(h, "full-path"
|
||||
, "Search full path (default: file-/dirname only)"
|
||||
, "By default, the search pattern is only matched against the filename (or directory \
|
||||
name). Using this flag, the pattern is matched against the full path.");
|
||||
doc!(h, "null_separator"
|
||||
, "Separate results by the null character"
|
||||
, "Separate search results by the null character (instead of newlines). Useful for \
|
||||
piping results to 'xargs'.");
|
||||
doc!(h, "depth"
|
||||
, "Set maximum search depth (default: none)"
|
||||
, "Limit the directory traversal to a given depth. By default, there is no limit \
|
||||
on the search depth.");
|
||||
doc!(h, "file-type"
|
||||
, "Filter by type: f(ile), d(irectory), (sym)l(ink)"
|
||||
, "Filter the search by type:\n \
|
||||
'f' or 'file': regular files\n \
|
||||
'd' or 'directory': directories\n \
|
||||
'l' or 'symlink': symbolic links");
|
||||
doc!(h, "extension"
|
||||
, "Filter by file extension"
|
||||
, "(Additionally) filter search results by their file extension.");
|
||||
doc!(h, "exec"
|
||||
, "Execute a command for each search result"
|
||||
, "Execute a command for each search result.\n\
|
||||
All arguments following --exec are taken to be arguments to the command until the \
|
||||
argument ';' is encountered.\n\
|
||||
Each occurrence of the following placeholders is substituted by a path derived from the \
|
||||
current search result before the command is executed:\n \
|
||||
'{}': path\n \
|
||||
'{/}': basename\n \
|
||||
'{//}': parent directory\n \
|
||||
'{.}': path without file extension\n \
|
||||
'{/.}': basename without file extension");
|
||||
doc!(h, "exclude"
|
||||
, "Exclude entries that match the given glob pattern."
|
||||
, "Exclude files/directories that match the given glob pattern. This overrides any \
|
||||
other ignore logic. Multiple exclude patterns can be specified.");
|
||||
doc!(h, "color"
|
||||
, "When to use colors: never, *auto*, always"
|
||||
, "Declare when to use color for the pattern match output:\n \
|
||||
'auto': show colors if the output goes to an interactive console (default)\n \
|
||||
'never': do not use colorized output\n \
|
||||
'always': always use colorized output");
|
||||
doc!(h, "threads"
|
||||
, "Set number of threads to use for searching & executing"
|
||||
, "Set number of threads to use for searching & executing (default: number of available \
|
||||
CPU cores)");
|
||||
doc!(h, "max-buffer-time"
|
||||
, "the time (in ms) to buffer, before streaming to the console"
|
||||
, "Amount of time in milliseconds to buffer, before streaming the search results to\
|
||||
the console.");
|
||||
doc!(h, "pattern"
|
||||
, "the search pattern, a regular expression (optional)");
|
||||
doc!(h, "path"
|
||||
, "the root directory for the filesystem search (optional)"
|
||||
, "The directory where the filesystem search is rooted (optional). \
|
||||
If omitted, search the current working directory.");
|
||||
doc!(h, "rg-alias-hidden-ignore"
|
||||
, "Alias for no-ignore and/or hidden"
|
||||
, "Alias for no-ignore ('u') and no-ignore and hidden ('uu')");
|
||||
|
||||
h
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::io;
|
||||
|
||||
/// Executes a command.
|
||||
#[cfg(not(unix))]
|
||||
pub fn execute_command(mut cmd: Command, out_perm: Arc<Mutex<()>>) {
|
||||
use std::process::Stdio;
|
||||
use std::io::Write;
|
||||
|
||||
// Spawn the supplied command.
|
||||
let output = cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).output();
|
||||
|
||||
// Then wait for the command to exit, if it was spawned.
|
||||
match output {
|
||||
Ok(output) => {
|
||||
// While this lock is active, this thread will be the only thread allowed
|
||||
// to write it's outputs.
|
||||
let _lock = out_perm.lock().unwrap();
|
||||
|
||||
let stdout = io::stdout();
|
||||
let stderr = io::stderr();
|
||||
|
||||
let _ = stdout.lock().write_all(&output.stdout);
|
||||
let _ = stderr.lock().write_all(&output.stderr);
|
||||
}
|
||||
Err(why) => {
|
||||
if why.kind() == io::ErrorKind::NotFound {
|
||||
eprintln!("fd: execution error: command not found");
|
||||
} else {
|
||||
eprintln!("fd: execution error: {}", why);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a command.
|
||||
#[cfg(all(unix))]
|
||||
pub fn execute_command(mut cmd: Command, out_perm: Arc<Mutex<()>>) {
|
||||
use libc::{close, dup2, pipe, STDERR_FILENO, STDOUT_FILENO};
|
||||
use std::fs::File;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::os::unix::io::FromRawFd;
|
||||
|
||||
// Initial a pair of pipes that will be used to
|
||||
// pipe the std{out,err} of the spawned process.
|
||||
let mut stdout_fds = [0; 2];
|
||||
let mut stderr_fds = [0; 2];
|
||||
|
||||
unsafe {
|
||||
pipe(stdout_fds.as_mut_ptr());
|
||||
pipe(stderr_fds.as_mut_ptr());
|
||||
}
|
||||
|
||||
// Configure the pipes accordingly in the child.
|
||||
let child = cmd.before_exec(move || unsafe {
|
||||
// Redirect the child's std{out,err} to the write ends of our pipe.
|
||||
dup2(stdout_fds[1], STDOUT_FILENO);
|
||||
dup2(stderr_fds[1], STDERR_FILENO);
|
||||
|
||||
// Close all the fds we created here, so EOF will be sent when the program exits.
|
||||
close(stdout_fds[0]);
|
||||
close(stdout_fds[1]);
|
||||
close(stderr_fds[0]);
|
||||
close(stderr_fds[1]);
|
||||
Ok(())
|
||||
}).spawn();
|
||||
|
||||
// Open the read end of the pipes as `File`s.
|
||||
let (mut pout, mut perr) = unsafe {
|
||||
// Close the write ends of the pipes in the parent
|
||||
close(stdout_fds[1]);
|
||||
close(stderr_fds[1]);
|
||||
(
|
||||
// But create files from the read ends.
|
||||
File::from_raw_fd(stdout_fds[0]),
|
||||
File::from_raw_fd(stderr_fds[0]),
|
||||
)
|
||||
};
|
||||
|
||||
match child {
|
||||
Ok(mut child) => {
|
||||
let _ = child.wait();
|
||||
|
||||
// Create a lock to ensure that this thread has exclusive access to writing.
|
||||
let _lock = out_perm.lock().unwrap();
|
||||
|
||||
// And then write the outputs of the process until EOF is sent to each file.
|
||||
let stdout = io::stdout();
|
||||
let stderr = io::stderr();
|
||||
let _ = io::copy(&mut pout, &mut stdout.lock());
|
||||
let _ = io::copy(&mut perr, &mut stderr.lock());
|
||||
}
|
||||
Err(why) => {
|
||||
if why.kind() == io::ErrorKind::NotFound {
|
||||
eprintln!("fd: execution error: command not found");
|
||||
} else {
|
||||
eprintln!("fd: execution error: {}", why);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::path::MAIN_SEPARATOR;
|
||||
|
||||
/// Removes the parent component of the path
|
||||
pub fn basename(path: &str) -> &str {
|
||||
let mut index = 0;
|
||||
for (id, character) in path.char_indices() {
|
||||
if character == MAIN_SEPARATOR {
|
||||
index = id;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: On Windows, should return what for C:file.txt D:file.txt and \\server\share ?
|
||||
if index != 0 {
|
||||
return &path[index + 1..];
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Removes the extension from the path
|
||||
pub fn remove_extension(path: &str) -> &str {
|
||||
let mut has_dir = false;
|
||||
let mut dir_index = 0;
|
||||
let mut ext_index = 0;
|
||||
|
||||
for (id, character) in path.char_indices() {
|
||||
if character == MAIN_SEPARATOR {
|
||||
has_dir = true;
|
||||
dir_index = id;
|
||||
}
|
||||
if character == '.' {
|
||||
ext_index = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Account for hidden files and directories
|
||||
if ext_index != 0 && (!has_dir || dir_index + 2 <= ext_index) {
|
||||
return &path[0..ext_index];
|
||||
}
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Removes the basename from the path.
|
||||
pub fn dirname(path: &str) -> &str {
|
||||
let mut has_dir = false;
|
||||
let mut index = 0;
|
||||
for (id, character) in path.char_indices() {
|
||||
if character == MAIN_SEPARATOR {
|
||||
has_dir = true;
|
||||
index = id;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: On Windows, return what for C:file.txt D:file.txt and \\server\share ?
|
||||
if !has_dir {
|
||||
"."
|
||||
} else if index == 0 {
|
||||
&path[..1]
|
||||
} else {
|
||||
&path[0..index]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{MAIN_SEPARATOR, basename, dirname, remove_extension};
|
||||
|
||||
fn correct(input: &str) -> String {
|
||||
input.replace('/', &MAIN_SEPARATOR.to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_remove_ext_simple() {
|
||||
assert_eq!(remove_extension("foo.txt"), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_remove_ext_dir() {
|
||||
assert_eq!(
|
||||
remove_extension(&correct("dir/foo.txt")),
|
||||
correct("dir/foo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_hidden() {
|
||||
assert_eq!(remove_extension(".foo"), ".foo")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_remove_ext_utf8() {
|
||||
assert_eq!(remove_extension("💖.txt"), "💖");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_remove_ext_empty() {
|
||||
assert_eq!(remove_extension(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_basename_simple() {
|
||||
assert_eq!(basename("foo.txt"), "foo.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_basename_no_ext() {
|
||||
assert_eq!(remove_extension(basename("foo.txt")), "foo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_basename_dir() {
|
||||
assert_eq!(basename(&correct("dir/foo.txt")), "foo.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_basename_empty() {
|
||||
assert_eq!(basename(""), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_basename_utf8() {
|
||||
assert_eq!(basename(&correct("💖/foo.txt")), "foo.txt");
|
||||
assert_eq!(basename(&correct("dir/💖.txt")), "💖.txt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dirname_simple() {
|
||||
assert_eq!(dirname("foo.txt"), ".");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dirname_dir() {
|
||||
assert_eq!(dirname(&correct("dir/foo.txt")), "dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dirname_utf8() {
|
||||
assert_eq!(dirname(&correct("💖/foo.txt")), "💖");
|
||||
assert_eq!(dirname(&correct("dir/💖.txt")), "dir");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dirname_empty() {
|
||||
assert_eq!(dirname(""), ".");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_dirname_root() {
|
||||
#[cfg(windows)]
|
||||
assert_eq!(dirname("C:\\"), "C:");
|
||||
#[cfg(windows)]
|
||||
assert_eq!(dirname("\\"), "\\");
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(dirname("/"), "/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
use super::CommandTemplate;
|
||||
|
||||
/// An event loop that listens for inputs from the `rx` receiver. Each received input will
|
||||
/// generate a command with the supplied command template. The generated command will then
|
||||
/// be executed, and this process will continue until the receiver's sender has closed.
|
||||
pub fn job(rx: Arc<Mutex<Receiver<PathBuf>>>, cmd: Arc<CommandTemplate>, out_perm: Arc<Mutex<()>>) {
|
||||
loop {
|
||||
// Create a lock on the shared receiver for this thread.
|
||||
let lock = rx.lock().unwrap();
|
||||
|
||||
// Obtain the next path from the receiver, else if the channel
|
||||
// has closed, exit from the loop
|
||||
let value: PathBuf = match lock.recv() {
|
||||
Ok(value) => value,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
// Drop the lock so that other threads can read from the the receiver.
|
||||
drop(lock);
|
||||
// Generate a command and execute it.
|
||||
cmd.generate_and_execute(&value, Arc::clone(&out_perm));
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
// TODO: Possible optimization could avoid pushing characters on a buffer.
|
||||
mod command;
|
||||
mod token;
|
||||
mod job;
|
||||
mod input;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use self::input::{basename, dirname, remove_extension};
|
||||
use self::command::execute_command;
|
||||
use self::token::Token;
|
||||
pub use self::job::job;
|
||||
|
||||
/// Represents a template that is utilized to generate command strings.
|
||||
///
|
||||
/// The template is meant to be coupled with an input in order to generate a command. The
|
||||
/// `generate_and_execute()` method will be used to generate a command and execute it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CommandTemplate {
|
||||
args: Vec<ArgumentTemplate>,
|
||||
}
|
||||
|
||||
impl CommandTemplate {
|
||||
pub fn new<I, S>(input: I) -> CommandTemplate
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
lazy_static! {
|
||||
static ref PLACEHOLDER_PATTERN: Regex = Regex::new(r"\{(/?\.?|//)\}").unwrap();
|
||||
}
|
||||
|
||||
let mut args = Vec::new();
|
||||
let mut has_placeholder = false;
|
||||
|
||||
for arg in input {
|
||||
let arg = arg.as_ref();
|
||||
|
||||
let mut tokens = Vec::new();
|
||||
let mut start = 0;
|
||||
|
||||
for placeholder in PLACEHOLDER_PATTERN.find_iter(arg) {
|
||||
// Leading text before the placeholder.
|
||||
if placeholder.start() > start {
|
||||
tokens.push(Token::Text(arg[start..placeholder.start()].to_owned()));
|
||||
}
|
||||
|
||||
start = placeholder.end();
|
||||
|
||||
match placeholder.as_str() {
|
||||
"{}" => tokens.push(Token::Placeholder),
|
||||
"{.}" => tokens.push(Token::NoExt),
|
||||
"{/}" => tokens.push(Token::Basename),
|
||||
"{//}" => tokens.push(Token::Parent),
|
||||
"{/.}" => tokens.push(Token::BasenameNoExt),
|
||||
_ => panic!("Unhandled placeholder"),
|
||||
}
|
||||
|
||||
has_placeholder = true;
|
||||
}
|
||||
|
||||
// Without a placeholder, the argument is just fixed text.
|
||||
if tokens.is_empty() {
|
||||
args.push(ArgumentTemplate::Text(arg.to_owned()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if start < arg.len() {
|
||||
// Trailing text after last placeholder.
|
||||
tokens.push(Token::Text(arg[start..].to_owned()));
|
||||
}
|
||||
|
||||
args.push(ArgumentTemplate::Tokens(tokens));
|
||||
}
|
||||
|
||||
// If a placeholder token was not supplied, append one at the end of the command.
|
||||
if !has_placeholder {
|
||||
args.push(ArgumentTemplate::Tokens(vec![Token::Placeholder]));
|
||||
}
|
||||
|
||||
CommandTemplate { args: args }
|
||||
}
|
||||
|
||||
/// Generates and executes a command.
|
||||
///
|
||||
/// Using the internal `args` field, and a supplied `input` variable, a `Command` will be
|
||||
/// build. Once all arguments have been processed, the command is executed.
|
||||
pub fn generate_and_execute(&self, input: &Path, out_perm: Arc<Mutex<()>>) {
|
||||
let input = input
|
||||
.strip_prefix(".")
|
||||
.unwrap_or(input)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
let mut cmd = Command::new(self.args[0].generate(&input).as_ref());
|
||||
for arg in &self.args[1..] {
|
||||
cmd.arg(arg.generate(&input).as_ref());
|
||||
}
|
||||
|
||||
execute_command(cmd, out_perm)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a template for a single command argument.
|
||||
///
|
||||
/// The argument is either a collection of `Token`s including at least one placeholder variant, or
|
||||
/// a fixed text.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum ArgumentTemplate {
|
||||
Tokens(Vec<Token>),
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl ArgumentTemplate {
|
||||
pub fn generate<'a>(&'a self, path: &str) -> Cow<'a, str> {
|
||||
use self::Token::*;
|
||||
|
||||
match *self {
|
||||
ArgumentTemplate::Tokens(ref tokens) => {
|
||||
let mut s = String::new();
|
||||
for token in tokens {
|
||||
match *token {
|
||||
Basename => s += basename(path),
|
||||
BasenameNoExt => s += remove_extension(basename(path)),
|
||||
NoExt => s += remove_extension(path),
|
||||
Parent => s += dirname(path),
|
||||
Placeholder => s += path,
|
||||
Text(ref string) => s += string,
|
||||
}
|
||||
}
|
||||
Cow::Owned(s)
|
||||
}
|
||||
ArgumentTemplate::Text(ref text) => Cow::Borrowed(text),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CommandTemplate, ArgumentTemplate, Token};
|
||||
|
||||
#[test]
|
||||
fn tokens() {
|
||||
let expected = CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Text("${SHELL}:".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::Placeholder]),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(CommandTemplate::new(&[&"echo", &"${SHELL}:"]), expected);
|
||||
|
||||
assert_eq!(
|
||||
CommandTemplate::new(&["echo", "{.}"]),
|
||||
CommandTemplate {
|
||||
args: vec![
|
||||
ArgumentTemplate::Text("echo".into()),
|
||||
ArgumentTemplate::Tokens(vec![Token::NoExt]),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Designates what should be written to a buffer
|
||||
///
|
||||
/// Each `Token` contains either text, or a placeholder variant, which will be used to generate
|
||||
/// commands after all tokens for a given command template have been collected.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Token {
|
||||
Placeholder,
|
||||
Basename,
|
||||
Parent,
|
||||
NoExt,
|
||||
BasenameNoExt,
|
||||
Text(String),
|
||||
}
|
||||
|
||||
impl Display for Token {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Token::Placeholder => f.write_str("{}")?,
|
||||
Token::Basename => f.write_str("{/}")?,
|
||||
Token::Parent => f.write_str("{//}")?,
|
||||
Token::NoExt => f.write_str("{.}")?,
|
||||
Token::BasenameNoExt => f.write_str("{/.}")?,
|
||||
Token::Text(ref string) => f.write_str(string)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::env::current_dir;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io;
|
||||
|
||||
pub fn path_absolute_form(path: &Path) -> io::Result<PathBuf> {
|
||||
if path.is_absolute() {
|
||||
Ok(path.to_path_buf())
|
||||
} else {
|
||||
let path = path.strip_prefix(".").unwrap_or(path);
|
||||
current_dir().map(|path_buf| path_buf.join(path))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn absolute_path(path: &Path) -> io::Result<PathBuf> {
|
||||
let path_buf = path_absolute_form(path)?;
|
||||
|
||||
#[cfg(windows)]
|
||||
let path_buf = Path::new(path_buf.as_path().to_string_lossy().trim_left_matches(
|
||||
r"\\?\",
|
||||
)).to_path_buf();
|
||||
|
||||
Ok(path_buf)
|
||||
}
|
||||
|
||||
// Path::is_dir() is not guarandteed to be intuitively correct for "." and ".."
|
||||
// See: https://github.com/rust-lang/rust/issues/45302
|
||||
pub fn is_dir(path: &Path) -> bool {
|
||||
if path.file_name().is_some() {
|
||||
path.is_dir()
|
||||
} else {
|
||||
path.is_dir() && path.canonicalize().is_ok()
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std::process;
|
||||
use std::time;
|
||||
use std::io::Write;
|
||||
|
||||
use exec::CommandTemplate;
|
||||
use lscolors::LsColors;
|
||||
use walk::FileType;
|
||||
use regex_syntax::{Expr, ExprBuilder};
|
||||
|
||||
/// Defines how to display search result paths.
|
||||
#[derive(PartialEq)]
|
||||
pub enum PathDisplay {
|
||||
/// As an absolute path
|
||||
Absolute,
|
||||
|
||||
/// As a relative path
|
||||
Relative,
|
||||
}
|
||||
|
||||
/// Configuration options for *fd*.
|
||||
pub struct FdOptions {
|
||||
/// Whether the search is case-sensitive or case-insensitive.
|
||||
pub case_sensitive: bool,
|
||||
|
||||
/// Whether to search within the full file path or just the base name (filename or directory
|
||||
/// name).
|
||||
pub search_full_path: bool,
|
||||
|
||||
/// Whether to ignore hidden files and directories (or not).
|
||||
pub ignore_hidden: bool,
|
||||
|
||||
/// Whether to respect VCS ignore files (`.gitignore`, `.ignore`, ..) or not.
|
||||
pub read_ignore: bool,
|
||||
|
||||
/// Whether to follow symlinks or not.
|
||||
pub follow_links: bool,
|
||||
|
||||
/// Whether elements of output should be separated by a null character
|
||||
pub null_separator: bool,
|
||||
|
||||
/// The maximum search depth, or `None` if no maximum search depth should be set.
|
||||
///
|
||||
/// A depth of `1` includes all files under the current directory, a depth of `2` also includes
|
||||
/// all files under subdirectories of the current directory, etc.
|
||||
pub max_depth: Option<usize>,
|
||||
|
||||
/// The number of threads to use.
|
||||
pub threads: usize,
|
||||
|
||||
/// Time to buffer results internally before streaming to the console. This is useful to
|
||||
/// provide a sorted output, in case the total execution time is shorter than
|
||||
/// `max_buffer_time`.
|
||||
pub max_buffer_time: Option<time::Duration>,
|
||||
|
||||
/// Display results as relative or absolute path.
|
||||
pub path_display: PathDisplay,
|
||||
|
||||
/// `None` if the output should not be colorized. Otherwise, a `LsColors` instance that defines
|
||||
/// how to style different filetypes.
|
||||
pub ls_colors: Option<LsColors>,
|
||||
|
||||
/// The type of file to search for. All files other than the specified type will be ignored.
|
||||
pub file_type: FileType,
|
||||
|
||||
/// The extension to search for. Only entries matching the extension will be included.
|
||||
///
|
||||
/// The value (if present) will be a lowercase string without leading dots.
|
||||
pub extension: Option<String>,
|
||||
|
||||
/// If a value is supplied, each item found will be used to generate and execute commands.
|
||||
pub command: Option<CommandTemplate>,
|
||||
|
||||
/// A list of glob patterns that should be excluded from the search.
|
||||
pub exclude_patterns: Vec<String>,
|
||||
}
|
||||
|
||||
/// Print error message to stderr and exit with status `1`.
|
||||
pub fn error(message: &str) -> ! {
|
||||
writeln!(&mut ::std::io::stderr(), "{}", message).expect("Failed writing to stderr");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
/// Determine if a regex pattern contains a literal uppercase character.
|
||||
pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
|
||||
ExprBuilder::new()
|
||||
.parse(pattern)
|
||||
.map(|expr| expr_has_uppercase_char(&expr))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Determine if a regex expression contains a literal uppercase character.
|
||||
fn expr_has_uppercase_char(expr: &Expr) -> bool {
|
||||
match *expr {
|
||||
Expr::Literal { ref chars, .. } => chars.iter().any(|c| c.is_uppercase()),
|
||||
Expr::Class(ref ranges) => {
|
||||
ranges.iter().any(|r| {
|
||||
r.start.is_uppercase() || r.end.is_uppercase()
|
||||
})
|
||||
}
|
||||
Expr::Group { ref e, .. } |
|
||||
Expr::Repeat { ref e, .. } => expr_has_uppercase_char(e),
|
||||
Expr::Concat(ref es) => es.iter().any(expr_has_uppercase_char),
|
||||
Expr::Alternate(ref es) => es.iter().any(expr_has_uppercase_char),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
/// A parser for the `LS_COLORS` environment variable.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use ansi_term::{Colour, Style};
|
||||
|
||||
/// Maps file extensions to ANSI colors / styles.
|
||||
pub type ExtensionStyles = HashMap<String, Style>;
|
||||
|
||||
/// Maps filenames to ANSI colors / styles.
|
||||
pub type FilenameStyles = HashMap<String, Style>;
|
||||
|
||||
const LS_CODES: &[&str] = &[
|
||||
"no",
|
||||
"no",
|
||||
"fi",
|
||||
"rs",
|
||||
"di",
|
||||
"ln",
|
||||
"ln",
|
||||
"ln",
|
||||
"or",
|
||||
"mi",
|
||||
"pi",
|
||||
"pi",
|
||||
"so",
|
||||
"bd",
|
||||
"bd",
|
||||
"cd",
|
||||
"cd",
|
||||
"do",
|
||||
"ex",
|
||||
"lc",
|
||||
"lc",
|
||||
"rc",
|
||||
"rc",
|
||||
"ec",
|
||||
"ec",
|
||||
"su",
|
||||
"su",
|
||||
"sg",
|
||||
"sg",
|
||||
"st",
|
||||
"ow",
|
||||
"ow",
|
||||
"tw",
|
||||
"tw",
|
||||
"ca",
|
||||
"mh",
|
||||
"cl",
|
||||
];
|
||||
|
||||
/// Defines how different file system entries should be colorized / styled.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct LsColors {
|
||||
/// ANSI Style for directories.
|
||||
pub directory: Style,
|
||||
|
||||
/// ANSI style for symbolic links.
|
||||
pub symlink: Style,
|
||||
|
||||
/// ANSI style for executable files.
|
||||
pub executable: Style,
|
||||
|
||||
/// A map that defines ANSI styles for different file extensions.
|
||||
pub extensions: ExtensionStyles,
|
||||
|
||||
/// A map that defines ANSI styles for different specific filenames.
|
||||
pub filenames: FilenameStyles,
|
||||
}
|
||||
|
||||
impl Default for LsColors {
|
||||
/// Get a default LsColors structure.
|
||||
fn default() -> LsColors {
|
||||
LsColors {
|
||||
directory: Colour::Blue.bold(),
|
||||
symlink: Colour::Cyan.normal(),
|
||||
executable: Colour::Red.bold(),
|
||||
extensions: HashMap::new(),
|
||||
filenames: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LsColors {
|
||||
/// Parse a single text-decoration code (normal, bold, italic, ...).
|
||||
fn parse_decoration(code: &str) -> Option<fn(Colour) -> Style> {
|
||||
match code {
|
||||
"0" | "00" => Some(Colour::normal),
|
||||
"1" | "01" => Some(Colour::bold),
|
||||
"3" | "03" => Some(Colour::italic),
|
||||
"4" | "04" => Some(Colour::underline),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse ANSI escape sequences like `38;5;10;1`.
|
||||
fn parse_style(code: &str) -> Option<Style> {
|
||||
let mut split = code.split(';');
|
||||
|
||||
if let Some(first) = split.next() {
|
||||
// Try to match the first part as a text-decoration argument
|
||||
let mut decoration = LsColors::parse_decoration(first);
|
||||
|
||||
let c1 = if decoration.is_none() {
|
||||
Some(first)
|
||||
} else {
|
||||
split.next()
|
||||
};
|
||||
let c2 = split.next();
|
||||
let c3 = split.next();
|
||||
|
||||
let color = if c1 == Some("38") && c2 == Some("5") {
|
||||
let n_white = 7;
|
||||
let n = if let Some(num) = c3 {
|
||||
u8::from_str_radix(num, 10).unwrap_or(n_white)
|
||||
} else {
|
||||
n_white
|
||||
};
|
||||
|
||||
Colour::Fixed(n)
|
||||
} else if let Some(color_s) = c1 {
|
||||
match color_s {
|
||||
"30" => Colour::Black,
|
||||
"31" => Colour::Red,
|
||||
"32" => Colour::Green,
|
||||
"33" => Colour::Yellow,
|
||||
"34" => Colour::Blue,
|
||||
"35" => Colour::Purple,
|
||||
"36" => Colour::Cyan,
|
||||
_ => Colour::White,
|
||||
}
|
||||
} else {
|
||||
Colour::White
|
||||
};
|
||||
|
||||
if decoration.is_none() {
|
||||
// Try to find a decoration somewhere in the sequence
|
||||
decoration = code.split(';').flat_map(LsColors::parse_decoration).next();
|
||||
}
|
||||
|
||||
let ansi_style = decoration.unwrap_or(Colour::normal)(color);
|
||||
|
||||
Some(ansi_style)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a new `LS_COLORS` entry.
|
||||
fn add_entry(&mut self, input: &str) {
|
||||
let mut parts = input.trim().split('=');
|
||||
if let Some(pattern) = parts.next() {
|
||||
if let Some(style_code) = parts.next() {
|
||||
// Ensure that the input was split into exactly two parts:
|
||||
if !parts.next().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(style) = LsColors::parse_style(style_code) {
|
||||
// Try to match against one of the known codes
|
||||
let res = LS_CODES.iter().find(|&&c| c == pattern);
|
||||
|
||||
if let Some(code) = res {
|
||||
match code.as_ref() {
|
||||
"di" => self.directory = style,
|
||||
"ln" => self.symlink = style,
|
||||
"ex" => self.executable = style,
|
||||
_ => return,
|
||||
}
|
||||
} else if pattern.starts_with("*.") {
|
||||
let extension = String::from(pattern).split_off(2);
|
||||
self.extensions.insert(extension, style);
|
||||
} else if pattern.starts_with('*') {
|
||||
let filename = String::from(pattern).split_off(1);
|
||||
self.filenames.insert(filename, style);
|
||||
} else {
|
||||
// Unknown/corrupt pattern
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a `LsColors` structure from a string.
|
||||
pub fn from_string(input: &str) -> LsColors {
|
||||
let mut lscolors = LsColors::default();
|
||||
|
||||
for s in input.split(':') {
|
||||
lscolors.add_entry(s);
|
||||
}
|
||||
|
||||
lscolors
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_simple() {
|
||||
assert_eq!(Some(Colour::Red.normal()), LsColors::parse_style("31"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decoration() {
|
||||
assert_eq!(Some(Colour::Red.normal()), LsColors::parse_style("00;31"));
|
||||
|
||||
assert_eq!(Some(Colour::Blue.italic()), LsColors::parse_style("03;34"));
|
||||
|
||||
assert_eq!(Some(Colour::Cyan.bold()), LsColors::parse_style("01;36"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_decoration_backwards() {
|
||||
assert_eq!(Some(Colour::Blue.italic()), LsColors::parse_style("34;03"));
|
||||
|
||||
assert_eq!(Some(Colour::Cyan.bold()), LsColors::parse_style("36;01"));
|
||||
|
||||
assert_eq!(Some(Colour::Red.normal()), LsColors::parse_style("31;00"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_256() {
|
||||
assert_eq!(
|
||||
Some(Colour::Fixed(115).normal()),
|
||||
LsColors::parse_style("38;5;115")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Some(Colour::Fixed(115).normal()),
|
||||
LsColors::parse_style("00;38;5;115")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Some(Colour::Fixed(119).bold()),
|
||||
LsColors::parse_style("01;38;5;119")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Some(Colour::Fixed(119).bold()),
|
||||
LsColors::parse_style("38;5;119;01")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
assert_eq!(LsColors::default(), LsColors::from_string(&String::new()));
|
||||
|
||||
let result = LsColors::from_string(&String::from(
|
||||
"rs=0:di=03;34:ln=01;36:*.foo=01;35:*README=33",
|
||||
));
|
||||
|
||||
assert_eq!(Colour::Blue.italic(), result.directory);
|
||||
assert_eq!(Colour::Cyan.bold(), result.symlink);
|
||||
assert_eq!(Some(&Colour::Purple.bold()), result.extensions.get("foo"));
|
||||
assert_eq!(
|
||||
Some(&Colour::Yellow.normal()),
|
||||
result.filenames.get("README")
|
||||
);
|
||||
}
|
||||
+139
-241
@@ -1,261 +1,159 @@
|
||||
extern crate ansi_term;
|
||||
extern crate getopts;
|
||||
extern crate isatty;
|
||||
extern crate regex;
|
||||
extern crate walkdir;
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
extern crate ansi_term;
|
||||
extern crate atty;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate ignore;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[cfg(all(unix, not(target_os = "redox")))]
|
||||
extern crate libc;
|
||||
extern crate num_cpus;
|
||||
extern crate regex;
|
||||
extern crate regex_syntax;
|
||||
|
||||
pub mod fshelper;
|
||||
pub mod lscolors;
|
||||
mod app;
|
||||
mod exec;
|
||||
mod internal;
|
||||
mod output;
|
||||
mod walk;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::File;
|
||||
use std::io::{Write, BufReader,BufRead};
|
||||
use std::path::{Path, Component};
|
||||
use std::process;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time;
|
||||
|
||||
use ansi_term::{Style, Colour};
|
||||
use getopts::Options;
|
||||
use isatty::stdout_isatty;
|
||||
use regex::{Regex, RegexBuilder};
|
||||
use walkdir::{WalkDir, DirEntry, WalkDirIterator};
|
||||
use atty::Stream;
|
||||
use regex::RegexBuilder;
|
||||
|
||||
/// Maps file extensions to ANSI colors / styles.
|
||||
type ExtensionStyles = HashMap<String, ansi_term::Style>;
|
||||
|
||||
/// Configuration options for *fd*.
|
||||
struct FdOptions {
|
||||
case_sensitive: bool,
|
||||
search_full_path: bool,
|
||||
search_hidden: bool,
|
||||
follow_links: bool,
|
||||
colored: bool,
|
||||
max_depth: usize,
|
||||
extension_styles: Option<ExtensionStyles>
|
||||
}
|
||||
|
||||
/// The default maximum recursion depth.
|
||||
const MAX_DEPTH_DEFAULT : usize = 25;
|
||||
|
||||
/// Print a search result to the console.
|
||||
fn print_entry(path_root: &Path, path_entry: &Path, config: &FdOptions) {
|
||||
let path_full = path_root.join(path_entry);
|
||||
|
||||
let path_str = match path_entry.to_str() {
|
||||
Some(p) => p,
|
||||
None => return
|
||||
};
|
||||
|
||||
if !config.colored {
|
||||
println!("{}", path_str);
|
||||
} else {
|
||||
let mut component_path = path_root.to_path_buf();
|
||||
|
||||
// Traverse the path and colorize each component
|
||||
for component in path_entry.components() {
|
||||
let comp_str = match component {
|
||||
Component::Normal(p) => p,
|
||||
_ => error("Unexpected path component")
|
||||
};
|
||||
|
||||
component_path.push(Path::new(comp_str));
|
||||
|
||||
let style =
|
||||
if component_path.symlink_metadata()
|
||||
.map(|md| md.file_type().is_symlink())
|
||||
.unwrap_or(false) {
|
||||
Colour::Cyan.normal()
|
||||
} else if component_path.is_dir() {
|
||||
Colour::Blue.bold()
|
||||
} else {
|
||||
// Loop up file extension
|
||||
if let Some(ref ext_styles) = config.extension_styles {
|
||||
component_path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.and_then(|e| ext_styles.get(e))
|
||||
.map(|r| r.clone())
|
||||
.unwrap_or(Style::new())
|
||||
}
|
||||
else {
|
||||
Style::new()
|
||||
}
|
||||
};
|
||||
|
||||
print!("{}", style.paint(comp_str.to_str().unwrap()));
|
||||
|
||||
if component_path.is_dir() && component_path != path_full {
|
||||
let sep = std::path::MAIN_SEPARATOR.to_string();
|
||||
print!("{}", style.paint(sep));
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if filename of entry starts with a dot.
|
||||
fn is_hidden(entry: &DirEntry) -> bool {
|
||||
entry.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with("."))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Recursively scan the given root path and search for files / pathnames
|
||||
/// matching the pattern.
|
||||
fn scan(root: &Path, pattern: &Regex, config: &FdOptions) {
|
||||
let walker = WalkDir::new(root)
|
||||
.follow_links(config.follow_links)
|
||||
.max_depth(config.max_depth)
|
||||
.into_iter()
|
||||
.filter_entry(|e| config.search_hidden || !is_hidden(e))
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path() != root);
|
||||
|
||||
for entry in walker {
|
||||
let path_rel = match entry.path().strip_prefix(root) {
|
||||
Ok(p) => p,
|
||||
Err(_) => continue
|
||||
};
|
||||
|
||||
let search_str =
|
||||
if config.search_full_path {
|
||||
path_rel.to_str()
|
||||
} else {
|
||||
if !path_rel.is_file() { continue }
|
||||
|
||||
path_rel.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
};
|
||||
|
||||
search_str.and_then(|s| pattern.find(s))
|
||||
.map(|_| print_entry(&root, path_rel, &config));
|
||||
}
|
||||
}
|
||||
|
||||
/// Print error message to stderr and exit with status `1`.
|
||||
fn error(message: &str) -> ! {
|
||||
writeln!(&mut std::io::stderr(), "{}", message)
|
||||
.expect("Failed writing to stderr");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
/// Parse `dircolors` file.
|
||||
fn parse_dircolors(path: &Path) -> std::io::Result<ExtensionStyles> {
|
||||
let file = File::open(path)?;
|
||||
let mut ext_styles = HashMap::new();
|
||||
|
||||
let pattern_ansi_256 =
|
||||
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:00;)?38;5;([0-9]+)\b")
|
||||
.unwrap();
|
||||
|
||||
let pattern_ansi =
|
||||
Regex::new(r"^\.([A-Za-z0-9]+)\s*(?:([0-9]+);)?([0-9][0-9])\b")
|
||||
.unwrap();
|
||||
|
||||
for line in BufReader::new(file).lines() {
|
||||
let line_s = line.unwrap();
|
||||
if let Some(caps) = pattern_ansi_256.captures(line_s.as_str()) {
|
||||
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
||||
let fg = caps.get(2)
|
||||
.map(|m| m.as_str())
|
||||
.and_then(|n| u8::from_str_radix(n, 10).ok())
|
||||
.unwrap_or(7); // white
|
||||
ext_styles.insert(String::from(ext),
|
||||
Colour::Fixed(fg).normal());
|
||||
}
|
||||
} else if let Some(caps) = pattern_ansi.captures(line_s.as_str()) {
|
||||
if let Some(ext) = caps.get(1).map(|m| m.as_str()) {
|
||||
let color_s = caps.get(3)
|
||||
.map_or("", |m| m.as_str());
|
||||
let color = match color_s {
|
||||
"31" => Colour::Red,
|
||||
"32" => Colour::Green,
|
||||
"33" => Colour::Yellow,
|
||||
"34" => Colour::Blue,
|
||||
"35" => Colour::Purple,
|
||||
"36" => Colour::Cyan,
|
||||
_ => Colour::White
|
||||
};
|
||||
let style_s = caps.get(2)
|
||||
.map_or("", |m| m.as_str());
|
||||
let style = match style_s {
|
||||
"1" => color.bold(),
|
||||
"01" => color.bold(),
|
||||
"3" => color.italic(),
|
||||
"03" => color.italic(),
|
||||
"4" => color.underline(),
|
||||
"04" => color.underline(),
|
||||
_ => color.normal()
|
||||
};
|
||||
ext_styles.insert(String::from(ext), style);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ext_styles)
|
||||
}
|
||||
use exec::CommandTemplate;
|
||||
use internal::{error, pattern_has_uppercase_char, FdOptions, PathDisplay};
|
||||
use lscolors::LsColors;
|
||||
use walk::FileType;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let matches = app::build_app().get_matches();
|
||||
|
||||
let mut opts = Options::new();
|
||||
opts.optflag("h", "help", "print this help message");
|
||||
opts.optflag("s", "sensitive",
|
||||
"case-sensitive search (default: smart case)");
|
||||
opts.optflag("f", "filename",
|
||||
"search filenames only (default: full path)");
|
||||
opts.optflag("H", "hidden",
|
||||
"search hidden files/directories (default: off)");
|
||||
opts.optflag("F", "follow", "follow symlinks (default: off)");
|
||||
opts.optflag("n", "no-color", "do not colorize output (default: on)");
|
||||
opts.optopt("d", "max-depth",
|
||||
format!("maximum search depth (default: {})",
|
||||
MAX_DEPTH_DEFAULT).as_str(),
|
||||
"D");
|
||||
// Get the search pattern
|
||||
let empty_pattern = String::new();
|
||||
let pattern = matches.value_of("pattern").unwrap_or(&empty_pattern);
|
||||
|
||||
let matches = match opts.parse(&args[1..]) {
|
||||
Ok(m) => m,
|
||||
Err(e) => error(e.description())
|
||||
};
|
||||
|
||||
if matches.opt_present("h") {
|
||||
let brief = "Usage: fd [options] [PATTERN]";
|
||||
print!("{}", opts.usage(&brief));
|
||||
process::exit(1);
|
||||
// Get the current working directory
|
||||
let current_dir = Path::new(".");
|
||||
if !fshelper::is_dir(current_dir) {
|
||||
error("Error: could not get current directory.");
|
||||
}
|
||||
|
||||
let empty = String::new();
|
||||
let pattern = matches.free.get(0).unwrap_or(&empty);
|
||||
|
||||
let current_dir_buf = match env::current_dir() {
|
||||
Ok(cd) => cd,
|
||||
Err(_) => error("Could not get current directory!")
|
||||
// Get the root directory for the search
|
||||
let mut root_dir_buf = match matches.value_of("path") {
|
||||
Some(path) => PathBuf::from(path),
|
||||
None => current_dir.to_path_buf(),
|
||||
};
|
||||
let current_dir = current_dir_buf.as_path();
|
||||
if !fshelper::is_dir(&root_dir_buf) {
|
||||
error(&format!(
|
||||
"Error: '{}' is not a directory.",
|
||||
root_dir_buf.to_string_lossy()
|
||||
));
|
||||
}
|
||||
|
||||
let ext_styles = env::home_dir()
|
||||
.map(|h| h.join(".dir_colors"))
|
||||
.and_then(|path| parse_dircolors(&path).ok());
|
||||
let path_display = if matches.is_present("absolute-path") || root_dir_buf.is_absolute() {
|
||||
PathDisplay::Absolute
|
||||
} else {
|
||||
PathDisplay::Relative
|
||||
};
|
||||
|
||||
if path_display == PathDisplay::Absolute && root_dir_buf.is_relative() {
|
||||
root_dir_buf = fshelper::absolute_path(root_dir_buf.as_path()).unwrap();
|
||||
}
|
||||
let root_dir = root_dir_buf.as_path();
|
||||
|
||||
// The search will be case-sensitive if the command line flag is set or
|
||||
// if the pattern has an uppercase character (smart case).
|
||||
let case_sensitive = !matches.is_present("ignore-case") &&
|
||||
(matches.is_present("case-sensitive") || pattern_has_uppercase_char(pattern));
|
||||
|
||||
let colored_output = match matches.value_of("color") {
|
||||
Some("always") => true,
|
||||
Some("never") => false,
|
||||
_ => atty::is(Stream::Stdout),
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
let colored_output = colored_output && windows::enable_colored_output();
|
||||
|
||||
let ls_colors = if colored_output {
|
||||
Some(
|
||||
env::var("LS_COLORS")
|
||||
.ok()
|
||||
.map(|val| LsColors::from_string(&val))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command = matches.values_of("exec").map(CommandTemplate::new);
|
||||
|
||||
let config = FdOptions {
|
||||
// The search will be case-sensitive if the command line flag is set or
|
||||
// if the pattern has an uppercase character (smart case).
|
||||
case_sensitive: matches.opt_present("sensitive") ||
|
||||
pattern.chars().any(char::is_uppercase),
|
||||
search_full_path: !matches.opt_present("filename"),
|
||||
search_hidden: matches.opt_present("hidden"),
|
||||
colored: !matches.opt_present("no-color") &&
|
||||
stdout_isatty(),
|
||||
follow_links: matches.opt_present("follow"),
|
||||
max_depth:
|
||||
matches.opt_str("max-depth")
|
||||
.and_then(|ds| usize::from_str_radix(&ds, 10).ok())
|
||||
.unwrap_or(MAX_DEPTH_DEFAULT),
|
||||
extension_styles: ext_styles
|
||||
case_sensitive,
|
||||
search_full_path: matches.is_present("full-path"),
|
||||
ignore_hidden: !(matches.is_present("hidden") ||
|
||||
matches.occurrences_of("rg-alias-hidden-ignore") >= 2),
|
||||
read_ignore: !(matches.is_present("no-ignore") ||
|
||||
matches.is_present("rg-alias-hidden-ignore")),
|
||||
follow_links: matches.is_present("follow"),
|
||||
null_separator: matches.is_present("null_separator"),
|
||||
max_depth: matches.value_of("depth").and_then(|n| {
|
||||
usize::from_str_radix(n, 10).ok()
|
||||
}),
|
||||
threads: std::cmp::max(
|
||||
matches
|
||||
.value_of("threads")
|
||||
.and_then(|n| usize::from_str_radix(n, 10).ok())
|
||||
.unwrap_or_else(num_cpus::get),
|
||||
1,
|
||||
),
|
||||
max_buffer_time: matches
|
||||
.value_of("max-buffer-time")
|
||||
.and_then(|n| u64::from_str_radix(n, 10).ok())
|
||||
.map(time::Duration::from_millis),
|
||||
path_display,
|
||||
ls_colors,
|
||||
file_type: match matches.value_of("file-type") {
|
||||
Some("f") | Some("file") => FileType::RegularFile,
|
||||
Some("d") |
|
||||
Some("directory") => FileType::Directory,
|
||||
Some("l") | Some("symlink") => FileType::SymLink,
|
||||
_ => FileType::Any,
|
||||
},
|
||||
extension: matches.value_of("extension").map(|e| {
|
||||
e.trim_left_matches('.').to_lowercase()
|
||||
}),
|
||||
command,
|
||||
exclude_patterns: matches
|
||||
.values_of("exclude")
|
||||
.map(|v| v.map(|p| String::from("!") + p).collect())
|
||||
.unwrap_or_else(|| vec![]),
|
||||
};
|
||||
|
||||
match RegexBuilder::new(pattern)
|
||||
.case_insensitive(!config.case_sensitive)
|
||||
.build() {
|
||||
Ok(re) => scan(¤t_dir, &re, &config),
|
||||
Err(err) => error(err.description())
|
||||
.case_insensitive(!config.case_sensitive)
|
||||
.dot_matches_new_line(true)
|
||||
.build() {
|
||||
Ok(re) => walk::scan(root_dir, Arc::new(re), Arc::new(config)),
|
||||
Err(err) => error(err.description()),
|
||||
}
|
||||
}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use internal::FdOptions;
|
||||
use lscolors::LsColors;
|
||||
|
||||
use std::{fs, process};
|
||||
use std::io::{self, Write};
|
||||
use std::ops::Deref;
|
||||
use std::path::{self, Path, PathBuf, Component};
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
use ansi_term;
|
||||
|
||||
pub fn print_entry(entry: &PathBuf, config: &FdOptions) {
|
||||
let path = entry.strip_prefix(".").unwrap_or(entry);
|
||||
|
||||
let r = if let Some(ref ls_colors) = config.ls_colors {
|
||||
print_entry_colorized(path, config, ls_colors)
|
||||
} else {
|
||||
print_entry_uncolorized(path, config)
|
||||
};
|
||||
|
||||
if r.is_err() {
|
||||
// Probably a broken pipe. Exit gracefully.
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_entry_colorized(path: &Path, config: &FdOptions, ls_colors: &LsColors) -> io::Result<()> {
|
||||
let default_style = ansi_term::Style::default();
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut handle = stdout.lock();
|
||||
|
||||
// Separator to use before the current component.
|
||||
let mut separator = String::new();
|
||||
|
||||
// Full path to the current component.
|
||||
let mut component_path = PathBuf::new();
|
||||
|
||||
// Traverse the path and colorize each component
|
||||
for component in path.components() {
|
||||
let comp_str = component.as_os_str().to_string_lossy();
|
||||
component_path.push(Path::new(comp_str.deref()));
|
||||
|
||||
let style = get_path_style(&component_path, ls_colors).unwrap_or(&default_style);
|
||||
|
||||
write!(handle, "{}{}", separator, style.paint(comp_str))?;
|
||||
|
||||
// Determine separator to print before next component.
|
||||
separator = match component {
|
||||
// Prefix needs no separator, as it is always followed by RootDir.
|
||||
Component::Prefix(_) => String::new(),
|
||||
// RootDir is already a separator.
|
||||
Component::RootDir => String::new(),
|
||||
// Everything else uses a separator that is painted the same way as the component.
|
||||
_ => style.paint(path::MAIN_SEPARATOR.to_string()).to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if config.null_separator {
|
||||
write!(handle, "\0")
|
||||
} else {
|
||||
writeln!(handle, "")
|
||||
}
|
||||
}
|
||||
|
||||
fn print_entry_uncolorized(path: &Path, config: &FdOptions) -> io::Result<()> {
|
||||
let separator = if config.null_separator { "\0" } else { "\n" };
|
||||
|
||||
let path_str = path.to_string_lossy();
|
||||
write!(&mut io::stdout(), "{}{}", path_str, separator)
|
||||
}
|
||||
|
||||
fn get_path_style<'a>(path: &Path, ls_colors: &'a LsColors) -> Option<&'a ansi_term::Style> {
|
||||
if path.symlink_metadata()
|
||||
.map(|md| md.file_type().is_symlink())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Some(&ls_colors.symlink);
|
||||
}
|
||||
|
||||
let metadata = path.metadata();
|
||||
|
||||
if metadata.as_ref().map(|md| md.is_dir()).unwrap_or(false) {
|
||||
Some(&ls_colors.directory)
|
||||
} else if metadata.map(|md| is_executable(&md)).unwrap_or(false) {
|
||||
Some(&ls_colors.executable)
|
||||
} else if let Some(filename_style) =
|
||||
path.file_name().and_then(|n| n.to_str()).and_then(|n| {
|
||||
ls_colors.filenames.get(n)
|
||||
})
|
||||
{
|
||||
Some(filename_style)
|
||||
} else if let Some(extension_style) =
|
||||
path.extension().and_then(|e| e.to_str()).and_then(|e| {
|
||||
ls_colors.extensions.get(e)
|
||||
})
|
||||
{
|
||||
Some(extension_style)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
fn is_executable(md: &fs::Metadata) -> bool {
|
||||
md.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_executable(_: &fs::Metadata) -> bool {
|
||||
false
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use exec;
|
||||
use fshelper;
|
||||
use internal::{error, FdOptions};
|
||||
use output;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread;
|
||||
use std::time;
|
||||
|
||||
use ignore::{self, WalkBuilder};
|
||||
use ignore::overrides::OverrideBuilder;
|
||||
use regex::Regex;
|
||||
|
||||
/// The receiver thread can either be buffering results or directly streaming to the console.
|
||||
enum ReceiverMode {
|
||||
/// Receiver is still buffering in order to sort the results, if the search finishes fast
|
||||
/// enough.
|
||||
Buffering,
|
||||
|
||||
/// Receiver is directly printing results to the output.
|
||||
Streaming,
|
||||
}
|
||||
|
||||
/// The type of file to search for.
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum FileType {
|
||||
Any,
|
||||
RegularFile,
|
||||
Directory,
|
||||
SymLink,
|
||||
}
|
||||
|
||||
/// Recursively scan the given search path for files / pathnames matching the pattern.
|
||||
///
|
||||
/// If the `--exec` argument was supplied, this will create a thread pool for executing
|
||||
/// jobs in parallel from a given command line and the discovered paths. Otherwise, each
|
||||
/// path will simply be written to standard output.
|
||||
pub fn scan(root: &Path, pattern: Arc<Regex>, config: Arc<FdOptions>) {
|
||||
let (tx, rx) = channel();
|
||||
let threads = config.threads;
|
||||
|
||||
let mut override_builder = OverrideBuilder::new(root);
|
||||
|
||||
for pattern in &config.exclude_patterns {
|
||||
let res = override_builder.add(pattern);
|
||||
if res.is_err() {
|
||||
error(&format!("Error: malformed exclude pattern '{}'", pattern));
|
||||
}
|
||||
}
|
||||
let overrides = override_builder.build().unwrap_or_else(|_| {
|
||||
error("Mismatch in exclude patterns");
|
||||
});
|
||||
|
||||
let walker = WalkBuilder::new(root)
|
||||
.hidden(config.ignore_hidden)
|
||||
.ignore(config.read_ignore)
|
||||
.git_ignore(config.read_ignore)
|
||||
.parents(config.read_ignore)
|
||||
.git_global(config.read_ignore)
|
||||
.git_exclude(config.read_ignore)
|
||||
.overrides(overrides)
|
||||
.follow_links(config.follow_links)
|
||||
.max_depth(config.max_depth)
|
||||
.threads(threads)
|
||||
.build_parallel();
|
||||
|
||||
// Spawn the thread that receives all results through the channel.
|
||||
let rx_config = Arc::clone(&config);
|
||||
let receiver_thread = thread::spawn(move || {
|
||||
// This will be set to `Some` if the `--exec` argument was supplied.
|
||||
if let Some(ref cmd) = rx_config.command {
|
||||
let shared_rx = Arc::new(Mutex::new(rx));
|
||||
|
||||
let out_perm = Arc::new(Mutex::new(()));
|
||||
|
||||
// TODO: the following line is a workaround to replace the `unsafe` block that was
|
||||
// previously used here to avoid the (unnecessary?) cloning of the command. The
|
||||
// `unsafe` block caused problems on some platforms (SIGILL instructions on Linux) and
|
||||
// therefore had to be removed.
|
||||
let cmd = Arc::new(cmd.clone());
|
||||
|
||||
// Each spawned job will store it's thread handle in here.
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let rx = Arc::clone(&shared_rx);
|
||||
let cmd = Arc::clone(&cmd);
|
||||
let out_perm = Arc::clone(&out_perm);
|
||||
|
||||
// Spawn a job thread that will listen for and execute inputs.
|
||||
let handle = thread::spawn(move || exec::job(rx, cmd, out_perm));
|
||||
|
||||
// Push the handle of the spawned thread into the vector for later joining.
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to exit before exiting the program.
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
} else {
|
||||
let start = time::Instant::now();
|
||||
|
||||
let mut buffer = vec![];
|
||||
|
||||
// Start in buffering mode
|
||||
let mut mode = ReceiverMode::Buffering;
|
||||
|
||||
// Maximum time to wait before we start streaming to the console.
|
||||
let max_buffer_time = rx_config.max_buffer_time.unwrap_or_else(
|
||||
|| time::Duration::from_millis(100),
|
||||
);
|
||||
|
||||
for value in rx {
|
||||
match mode {
|
||||
ReceiverMode::Buffering => {
|
||||
buffer.push(value);
|
||||
|
||||
// Have we reached the maximum time?
|
||||
if time::Instant::now() - start > max_buffer_time {
|
||||
// Flush the buffer
|
||||
for v in &buffer {
|
||||
output::print_entry(v, &rx_config);
|
||||
}
|
||||
buffer.clear();
|
||||
|
||||
// Start streaming
|
||||
mode = ReceiverMode::Streaming;
|
||||
}
|
||||
}
|
||||
ReceiverMode::Streaming => {
|
||||
output::print_entry(&value, &rx_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have finished fast enough (faster than max_buffer_time), we haven't streamed
|
||||
// anything to the console, yet. In this case, sort the results and print them:
|
||||
if !buffer.is_empty() {
|
||||
buffer.sort();
|
||||
for value in buffer {
|
||||
output::print_entry(&value, &rx_config);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn the sender threads.
|
||||
walker.run(|| {
|
||||
let config = Arc::clone(&config);
|
||||
let pattern = Arc::clone(&pattern);
|
||||
let tx_thread = tx.clone();
|
||||
let root = root.to_owned();
|
||||
|
||||
Box::new(move |entry_o| {
|
||||
let entry = match entry_o {
|
||||
Ok(e) => e,
|
||||
Err(_) => return ignore::WalkState::Continue,
|
||||
};
|
||||
|
||||
let entry_path = entry.path();
|
||||
|
||||
if entry_path == root {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
|
||||
// Filter out unwanted file types.
|
||||
match config.file_type {
|
||||
FileType::Any => (),
|
||||
FileType::RegularFile => {
|
||||
if entry.file_type().map_or(true, |ft| !ft.is_file()) {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
}
|
||||
FileType::Directory => {
|
||||
if entry.file_type().map_or(true, |ft| !ft.is_dir()) {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
}
|
||||
FileType::SymLink => {
|
||||
if entry.file_type().map_or(true, |ft| !ft.is_symlink()) {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out unwanted extensions.
|
||||
if let Some(ref filter_ext) = config.extension {
|
||||
let entry_ext = entry_path.extension().map(
|
||||
|e| e.to_string_lossy().to_lowercase(),
|
||||
);
|
||||
if entry_ext.map_or(true, |ext| ext != *filter_ext) {
|
||||
return ignore::WalkState::Continue;
|
||||
}
|
||||
}
|
||||
|
||||
let search_str_o = if config.search_full_path {
|
||||
match fshelper::path_absolute_form(entry_path) {
|
||||
Ok(path_abs_buf) => Some(path_abs_buf.to_string_lossy().into_owned().into()),
|
||||
Err(_) => error("Error: unable to get full path."),
|
||||
}
|
||||
} else {
|
||||
entry_path.file_name().map(|f| f.to_string_lossy())
|
||||
};
|
||||
|
||||
if let Some(search_str) = search_str_o {
|
||||
if pattern.is_match(&*search_str) {
|
||||
// TODO: take care of the unwrap call
|
||||
tx_thread.send(entry_path.to_owned()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
ignore::WalkState::Continue
|
||||
})
|
||||
});
|
||||
|
||||
// Drop the initial sender. If we don't do this, the receiver will block even
|
||||
// if all threads have finished, since there is still one sender around.
|
||||
drop(tx);
|
||||
|
||||
// Wait for the receiver thread to print out all results.
|
||||
receiver_thread.join().unwrap();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
extern crate kernel32;
|
||||
extern crate winapi;
|
||||
|
||||
use self::kernel32::{GetStdHandle, GetConsoleMode, SetConsoleMode};
|
||||
use self::winapi::{STD_OUTPUT_HANDLE, INVALID_HANDLE_VALUE};
|
||||
|
||||
const ENABLE_VIRTUAL_TERMINAL_PROCESSING: u32 = 0x0004;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#example
|
||||
pub fn enable_colored_output() -> bool {
|
||||
unsafe {
|
||||
let handle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/console/getconsolemode
|
||||
let mut mode = 0;
|
||||
if GetConsoleMode(handle, &mut mode) == 0 {
|
||||
return false;
|
||||
}
|
||||
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
|
||||
//
|
||||
// A console consists of an input buffer and one or more screen buffers. ... Setting the
|
||||
// output modes of one screen buffer does not affect the output modes of other screen
|
||||
// buffers.
|
||||
SetConsoleMode(handle, mode) != 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
use std;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix;
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows;
|
||||
|
||||
extern crate diff;
|
||||
extern crate tempdir;
|
||||
|
||||
use self::tempdir::TempDir;
|
||||
|
||||
/// Environment for the integration tests.
|
||||
pub struct TestEnv {
|
||||
/// Temporary working directory.
|
||||
temp_dir: TempDir,
|
||||
|
||||
/// Path to the *fd* executable.
|
||||
fd_exe: PathBuf,
|
||||
}
|
||||
|
||||
/// Create the working directory and the test files.
|
||||
fn create_working_directory() -> Result<TempDir, io::Error> {
|
||||
let temp_dir = TempDir::new("fd-tests")?;
|
||||
|
||||
{
|
||||
let root = temp_dir.path();
|
||||
|
||||
fs::create_dir_all(root.join("one/two/three"))?;
|
||||
|
||||
fs::File::create(root.join("a.foo"))?;
|
||||
fs::File::create(root.join("one/b.foo"))?;
|
||||
fs::File::create(root.join("one/two/c.foo"))?;
|
||||
fs::File::create(root.join("one/two/C.Foo2"))?;
|
||||
fs::File::create(root.join("one/two/three/d.foo"))?;
|
||||
fs::create_dir(root.join("one/two/three/directory_foo"))?;
|
||||
fs::File::create(root.join("ignored.foo"))?;
|
||||
fs::File::create(root.join(".hidden.foo"))?;
|
||||
fs::File::create(root.join("e1 e2"))?;
|
||||
|
||||
#[cfg(unix)] unix::fs::symlink(root.join("one/two"), root.join("symlink"))?;
|
||||
|
||||
// Note: creating symlinks on Windows requires the `SeCreateSymbolicLinkPrivilege` which
|
||||
// is by default only granted for administrators.
|
||||
#[cfg(windows)] windows::fs::symlink_dir(root.join("one/two"), root.join("symlink"))?;
|
||||
|
||||
fs::File::create(root.join(".ignore"))?.write_all(
|
||||
b"ignored.foo",
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(temp_dir)
|
||||
}
|
||||
|
||||
/// Find the *fd* executable.
|
||||
fn find_fd_exe() -> PathBuf {
|
||||
// Tests exe is in target/debug/deps, the *fd* exe is in target/debug
|
||||
let root = env::current_exe()
|
||||
.expect("tests executable")
|
||||
.parent()
|
||||
.expect("tests executable directory")
|
||||
.parent()
|
||||
.expect("fd executable directory")
|
||||
.to_path_buf();
|
||||
|
||||
let exe_name = if cfg!(windows) { "fd.exe" } else { "fd" };
|
||||
|
||||
root.join(exe_name)
|
||||
}
|
||||
|
||||
/// Format an error message for when *fd* did not exit successfully.
|
||||
fn format_exit_error(args: &[&str], output: &process::Output) -> String {
|
||||
format!(
|
||||
"`fd {}` did not exit successfully.\nstdout:\n---\n{}---\nstderr:\n---\n{}---",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
}
|
||||
|
||||
/// Format an error message for when the output of *fd* did not match the expected output.
|
||||
fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
|
||||
// Generate diff text.
|
||||
let diff_text = diff::lines(expected, actual)
|
||||
.into_iter()
|
||||
.map(|diff| match diff {
|
||||
diff::Result::Left(l) => format!("-{}", l),
|
||||
diff::Result::Both(l, _) => format!(" {}", l),
|
||||
diff::Result::Right(r) => format!("+{}", r),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
format!(
|
||||
concat!(
|
||||
"`fd {}` did not produce the expected output.\n",
|
||||
"Showing diff between expected and actual:\n{}\n"
|
||||
),
|
||||
args.join(" "),
|
||||
diff_text
|
||||
)
|
||||
}
|
||||
|
||||
/// Normalize the output for comparison.
|
||||
fn normalize_output(s: &str, trim_left: bool) -> String {
|
||||
// Split into lines and normalize separators.
|
||||
let mut lines = s.replace('\0', "NULL\n")
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let line = if trim_left { line.trim_left() } else { line };
|
||||
line.replace('/', &std::path::MAIN_SEPARATOR.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
lines.sort_by_key(|s| s.clone());
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
pub fn new() -> TestEnv {
|
||||
let temp_dir = create_working_directory().expect("working directory");
|
||||
let fd_exe = find_fd_exe();
|
||||
|
||||
TestEnv {
|
||||
temp_dir: temp_dir,
|
||||
fd_exe: fd_exe,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the root directory for the tests.
|
||||
pub fn test_root(&self) -> PathBuf {
|
||||
self.temp_dir.path().to_path_buf()
|
||||
}
|
||||
|
||||
/// Get the root directory of the file system.
|
||||
pub fn system_root(&self) -> PathBuf {
|
||||
let mut components = self.temp_dir.path().components();
|
||||
PathBuf::from(components.next().expect("root directory").as_os_str())
|
||||
}
|
||||
|
||||
/// Assert that calling *fd* with the specified arguments produces the expected output.
|
||||
pub fn assert_output(&self, args: &[&str], expected: &str) {
|
||||
self.assert_output_subdirectory(".", args, expected)
|
||||
}
|
||||
|
||||
/// Assert that calling *fd* in the specified path under the root working directory,
|
||||
/// and with the specified arguments produces the expected output.
|
||||
pub fn assert_output_subdirectory<P: AsRef<Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
args: &[&str],
|
||||
expected: &str,
|
||||
) {
|
||||
// Setup *fd* command.
|
||||
let mut cmd = process::Command::new(&self.fd_exe);
|
||||
cmd.current_dir(self.temp_dir.path().join(path));
|
||||
cmd.args(args);
|
||||
|
||||
// Run *fd*.
|
||||
let output = cmd.output().expect("fd output");
|
||||
|
||||
// Check for exit status.
|
||||
if !output.status.success() {
|
||||
panic!(format_exit_error(args, &output));
|
||||
}
|
||||
|
||||
// Normalize both expected and actual output.
|
||||
let expected = normalize_output(expected, true);
|
||||
let actual = normalize_output(&String::from_utf8_lossy(&output.stdout), false);
|
||||
|
||||
// Compare actual output to expected output.
|
||||
if expected != actual {
|
||||
panic!(format_output_error(args, &expected, &actual));
|
||||
}
|
||||
}
|
||||
}
|
||||
+673
@@ -0,0 +1,673 @@
|
||||
// Copyright (c) 2017 fd developers
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0>
|
||||
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
|
||||
// at your option. All files in the project carrying such
|
||||
// notice may not be copied, modified, or distributed except
|
||||
// according to those terms.
|
||||
|
||||
//! Integration tests for the CLI interface of fd.
|
||||
|
||||
extern crate regex;
|
||||
|
||||
mod testenv;
|
||||
|
||||
use testenv::TestEnv;
|
||||
use regex::escape;
|
||||
|
||||
fn get_absolute_root_path(env: &TestEnv) -> String {
|
||||
let path = env.test_root()
|
||||
.canonicalize()
|
||||
.expect("absolute path")
|
||||
.to_str()
|
||||
.expect("string")
|
||||
.to_string();
|
||||
|
||||
#[cfg(windows)]
|
||||
let path = path.trim_left_matches(r"\\?\").to_string();
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Simple tests
|
||||
#[test]
|
||||
fn test_simple() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(&["a.foo"], "a.foo");
|
||||
te.assert_output(&["b.foo"], "one/b.foo");
|
||||
te.assert_output(&["d.foo"], "one/two/three/d.foo");
|
||||
|
||||
te.assert_output(
|
||||
&["foo"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&[],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one
|
||||
one/b.foo
|
||||
one/two
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo
|
||||
symlink",
|
||||
);
|
||||
}
|
||||
|
||||
/// Explicit root path
|
||||
#[test]
|
||||
fn test_explicit_root_path() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "one"],
|
||||
"one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "one/two/three"],
|
||||
"one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output_subdirectory(
|
||||
"one/two",
|
||||
&["foo", "../../"],
|
||||
"../../a.foo
|
||||
../../one/b.foo
|
||||
../../one/two/c.foo
|
||||
../../one/two/C.Foo2
|
||||
../../one/two/three/d.foo
|
||||
../../one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output_subdirectory(
|
||||
"one/two/three",
|
||||
&["", ".."],
|
||||
"../c.foo
|
||||
../C.Foo2
|
||||
../three
|
||||
../three/d.foo
|
||||
../three/directory_foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Regex searches
|
||||
#[test]
|
||||
fn test_regex_searches() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["[a-c].foo"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--case-sensitive", "[a-c].foo"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Smart case
|
||||
#[test]
|
||||
fn test_smart_case() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["c.foo"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
|
||||
te.assert_output(&["C.Foo"], "one/two/C.Foo2");
|
||||
|
||||
te.assert_output(&["Foo"], "one/two/C.Foo2");
|
||||
|
||||
// Only literal uppercase chars should trigger case sensitivity.
|
||||
te.assert_output(
|
||||
&["\\Ac"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
te.assert_output(&["\\AC"], "one/two/C.Foo2");
|
||||
}
|
||||
|
||||
/// Case sensitivity (--case-sensitive)
|
||||
#[test]
|
||||
fn test_case_sensitive() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(&["--case-sensitive", "c.foo"], "one/two/c.foo");
|
||||
|
||||
te.assert_output(&["--case-sensitive", "C.Foo"], "one/two/C.Foo2");
|
||||
|
||||
te.assert_output(
|
||||
&["--ignore-case", "--case-sensitive", "C.Foo"],
|
||||
"one/two/C.Foo2",
|
||||
);
|
||||
}
|
||||
|
||||
/// Case insensitivity (--ignore-case)
|
||||
#[test]
|
||||
fn test_case_insensitive() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--ignore-case", "C.Foo"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--case-sensitive", "--ignore-case", "C.Foo"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2",
|
||||
);
|
||||
}
|
||||
|
||||
/// Full path search (--full-path)
|
||||
#[test]
|
||||
fn test_full_path() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
let root = te.system_root();
|
||||
let prefix = escape(&root.to_string_lossy());
|
||||
|
||||
te.assert_output(
|
||||
&[
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*three.*foo$", prefix = prefix),
|
||||
],
|
||||
"one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Hidden files (--hidden)
|
||||
#[test]
|
||||
fn test_hidden() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--hidden", "foo"],
|
||||
".hidden.foo
|
||||
a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Ignored files (--no-ignore)
|
||||
#[test]
|
||||
fn test_no_ignore() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--no-ignore", "foo"],
|
||||
"a.foo
|
||||
ignored.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--hidden", "--no-ignore", "foo"],
|
||||
".hidden.foo
|
||||
a.foo
|
||||
ignored.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Ignored files with ripgrep aliases (-u / -uu)
|
||||
#[test]
|
||||
fn test_no_ignore_aliases() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["-u", "foo"],
|
||||
"a.foo
|
||||
ignored.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["-uu", "foo"],
|
||||
".hidden.foo
|
||||
a.foo
|
||||
ignored.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
}
|
||||
|
||||
/// Symlinks (--follow)
|
||||
#[test]
|
||||
fn test_follow() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--follow", "c.foo"],
|
||||
"one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
symlink/c.foo
|
||||
symlink/C.Foo2",
|
||||
);
|
||||
}
|
||||
|
||||
/// Null separator (--print0)
|
||||
#[test]
|
||||
fn test_print0() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--print0", "foo"],
|
||||
"a.fooNULL
|
||||
one/b.fooNULL
|
||||
one/two/C.Foo2NULL
|
||||
one/two/c.fooNULL
|
||||
one/two/three/d.fooNULL
|
||||
one/two/three/directory_fooNULL",
|
||||
);
|
||||
}
|
||||
|
||||
/// Maximum depth (--max-depth)
|
||||
#[test]
|
||||
fn test_max_depth() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--max-depth", "3"],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one
|
||||
one/b.foo
|
||||
one/two
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three
|
||||
symlink",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--max-depth", "2"],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one
|
||||
one/b.foo
|
||||
one/two
|
||||
symlink",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--max-depth", "1"],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one
|
||||
symlink",
|
||||
);
|
||||
}
|
||||
|
||||
/// Absolute paths (--absolute-path)
|
||||
#[test]
|
||||
fn test_absolute_path() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
let abs_path = get_absolute_root_path(&te);
|
||||
|
||||
te.assert_output(
|
||||
&["--absolute-path"],
|
||||
&format!(
|
||||
"{abs_path}/a.foo
|
||||
{abs_path}/e1 e2
|
||||
{abs_path}/one
|
||||
{abs_path}/one/b.foo
|
||||
{abs_path}/one/two
|
||||
{abs_path}/one/two/c.foo
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo
|
||||
{abs_path}/symlink",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--absolute-path", "foo"],
|
||||
&format!(
|
||||
"{abs_path}/a.foo
|
||||
{abs_path}/one/b.foo
|
||||
{abs_path}/one/two/c.foo
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", &abs_path],
|
||||
&format!(
|
||||
"{abs_path}/a.foo
|
||||
{abs_path}/one/b.foo
|
||||
{abs_path}/one/two/c.foo
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// File type filter (--type)
|
||||
#[test]
|
||||
fn test_type() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--type", "f"],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/C.Foo2
|
||||
one/two/three/d.foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--type", "d"],
|
||||
"one
|
||||
one/two
|
||||
one/two/three
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(&["--type", "l"], "symlink");
|
||||
}
|
||||
|
||||
/// File extension (--extension)
|
||||
#[test]
|
||||
fn test_extension() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--extension", "foo"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/three/d.foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--extension", ".foo"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/c.foo
|
||||
one/two/three/d.foo",
|
||||
);
|
||||
|
||||
te.assert_output(&["--extension", "foo2"], "one/two/C.Foo2");
|
||||
}
|
||||
|
||||
/// Symlinks misc
|
||||
#[test]
|
||||
fn test_symlink() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
let abs_path = get_absolute_root_path(&te);
|
||||
|
||||
// From: http://pubs.opengroup.org/onlinepubs/9699919799/functions/getcwd.html
|
||||
// The getcwd() function shall place an absolute pathname of the current working directory in
|
||||
// the array pointed to by buf, and return buf. The pathname shall contain no components that
|
||||
// are dot or dot-dot, or are symbolic links.
|
||||
//
|
||||
// Key points:
|
||||
// 1. The path of the current working directory of a Unix process cannot contain symlinks.
|
||||
// 2. The path of the current working directory of a Windows process can contain symlinks.
|
||||
//
|
||||
// More:
|
||||
// 1. On Windows, symlinks are resolved after the ".." component.
|
||||
// 2. On Unix, symlinks are resolved immediately as encountered.
|
||||
|
||||
let parent_parent = if cfg!(windows) { ".." } else { "../.." };
|
||||
te.assert_output_subdirectory(
|
||||
"symlink",
|
||||
&["", parent_parent],
|
||||
&format!(
|
||||
"{dir}/a.foo
|
||||
{dir}/e1 e2
|
||||
{dir}/one
|
||||
{dir}/one/b.foo
|
||||
{dir}/one/two
|
||||
{dir}/one/two/c.foo
|
||||
{dir}/one/two/C.Foo2
|
||||
{dir}/one/two/three
|
||||
{dir}/one/two/three/d.foo
|
||||
{dir}/one/two/three/directory_foo
|
||||
{dir}/symlink",
|
||||
dir = &parent_parent
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output_subdirectory(
|
||||
"symlink",
|
||||
&["--absolute-path"],
|
||||
&format!(
|
||||
"{abs_path}/{dir}/c.foo
|
||||
{abs_path}/{dir}/C.Foo2
|
||||
{abs_path}/{dir}/three
|
||||
{abs_path}/{dir}/three/d.foo
|
||||
{abs_path}/{dir}/three/directory_foo",
|
||||
dir = if cfg!(windows) { "symlink" } else { "one/two" },
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["", &format!("{abs_path}/symlink", abs_path = abs_path)],
|
||||
&format!(
|
||||
"{abs_path}/symlink/c.foo
|
||||
{abs_path}/symlink/C.Foo2
|
||||
{abs_path}/symlink/three
|
||||
{abs_path}/symlink/three/d.foo
|
||||
{abs_path}/symlink/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
let root = te.system_root();
|
||||
let prefix = escape(&root.to_string_lossy());
|
||||
|
||||
te.assert_output_subdirectory(
|
||||
"symlink",
|
||||
&[
|
||||
"--absolute-path",
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*three", prefix = prefix),
|
||||
],
|
||||
&format!(
|
||||
"{abs_path}/{dir}/three
|
||||
{abs_path}/{dir}/three/d.foo
|
||||
{abs_path}/{dir}/three/directory_foo",
|
||||
dir = if cfg!(windows) { "symlink" } else { "one/two" },
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&[
|
||||
"--full-path",
|
||||
&format!("^{prefix}.*symlink.*three", prefix = prefix),
|
||||
&format!("{abs_path}/symlink", abs_path = abs_path),
|
||||
],
|
||||
&format!(
|
||||
"{abs_path}/symlink/three
|
||||
{abs_path}/symlink/three/d.foo
|
||||
{abs_path}/symlink/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Exclude patterns (--exclude)
|
||||
#[test]
|
||||
fn test_excludes() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
te.assert_output(
|
||||
&["--exclude", "*.foo"],
|
||||
"one
|
||||
one/two
|
||||
one/two/C.Foo2
|
||||
one/two/three
|
||||
one/two/three/directory_foo
|
||||
e1 e2
|
||||
symlink",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--exclude", "*.foo", "--exclude", "*.Foo2"],
|
||||
"one
|
||||
one/two
|
||||
one/two/three
|
||||
one/two/three/directory_foo
|
||||
e1 e2
|
||||
symlink",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--exclude", "*.foo", "--exclude", "*.Foo2", "foo"],
|
||||
"one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--exclude", "one/two", "foo"],
|
||||
"a.foo
|
||||
one/b.foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["--exclude", "one/**/*.foo"],
|
||||
"a.foo
|
||||
e1 e2
|
||||
one
|
||||
one/two
|
||||
one/two/C.Foo2
|
||||
one/two/three
|
||||
one/two/three/directory_foo
|
||||
symlink",
|
||||
);
|
||||
}
|
||||
|
||||
/// Shell script execution (--exec)
|
||||
#[test]
|
||||
fn test_exec() {
|
||||
let te = TestEnv::new();
|
||||
|
||||
let abs_path = get_absolute_root_path(&te);
|
||||
|
||||
// TODO Windows tests: D:file.txt \file.txt \\server\share\file.txt ...
|
||||
if !cfg!(windows) {
|
||||
te.assert_output(
|
||||
&["--absolute-path", "foo", "--exec", "echo"],
|
||||
&format!(
|
||||
"{abs_path}/a.foo
|
||||
{abs_path}/one/b.foo
|
||||
{abs_path}/one/two/C.Foo2
|
||||
{abs_path}/one/two/c.foo
|
||||
{abs_path}/one/two/three/d.foo
|
||||
{abs_path}/one/two/three/directory_foo",
|
||||
abs_path = &abs_path
|
||||
),
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "--exec", "echo", "{}"],
|
||||
"a.foo
|
||||
one/b.foo
|
||||
one/two/C.Foo2
|
||||
one/two/c.foo
|
||||
one/two/three/d.foo
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "--exec", "echo", "{.}"],
|
||||
"a
|
||||
one/b
|
||||
one/two/C
|
||||
one/two/c
|
||||
one/two/three/d
|
||||
one/two/three/directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "--exec", "echo", "{/}"],
|
||||
"a.foo
|
||||
b.foo
|
||||
C.Foo2
|
||||
c.foo
|
||||
d.foo
|
||||
directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "--exec", "echo", "{/.}"],
|
||||
"a
|
||||
b
|
||||
C
|
||||
c
|
||||
d
|
||||
directory_foo",
|
||||
);
|
||||
|
||||
te.assert_output(
|
||||
&["foo", "--exec", "echo", "{//}"],
|
||||
".
|
||||
one
|
||||
one/two
|
||||
one/two
|
||||
one/two/three
|
||||
one/two/three",
|
||||
);
|
||||
|
||||
te.assert_output(&["e1", "--exec", "printf", "%s.%s\n"], "e1 e2.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user