diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 58eb9f2e..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Bug Report -about: Report a bug. -title: "" -labels: bug -assignees: '' - ---- - -**Describe the bug you encountered:** - - - - -**Describe what you expected to happen:** - - -**What version of `fd` are you using?** - - -**Which operating system / distribution are you on?** - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..bd8c609b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,41 @@ +name: Bug Report +description: Report a bug. +title: "[BUG] " +labels: bug +body: + - type: markdown + attributes: + value: | + Please check out the [troubleshooting section](https://github.com/sharkdp/fd#troubleshooting) first. + - type: checkboxes + attributes: + options: + - label: I have read the troubleshooting section and still think this is a bug. + required: true + - type: textarea + id: bug + attributes: + label: "Describe the bug you encountered:" + validations: + required: true + - type: textarea + id: expected + attributes: + label: "Describe what you expected to happen:" + - type: input + id: version + attributes: + label: "What version of `fd` are you using?" + placeholder: "paste the output of `fd --version` here" + validations: + required: true + - type: textarea + id: os + attributes: + label: Which operating system / distribution are you on? + placeholder: | + Unix: paste the output of `uname -srm` and `lsb_release -a` here. + Windows: please tell us your Windows version + render: shell + validations: + required: true diff --git a/CHANGELOG.md b/CHANGELOG.md index afe8f317..529cd527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,17 @@ - File metadata is now cached between the different filters that require it (e.g. `--owner`, `--size`), reducing the number of `stat` syscalls when multiple filters are used; see #863 +- Colorized output is now significantly faster, see #720 and #853 (@tavianator) ## Features + - Don't buffer command output from `--exec` when using a single thread. See #522 - - Add new `-q, --quiet` flag, see #303 (@Asha20) - - Add new `--no-ignore-parent` flag, see #787 (@will459) +- Add new `--batch-size` flag, see #410 (@devonhollowood) +- Add opposing command-line options, see #595 (@Asha20) +- Add support for more filesystem indicators in `LS_COLORS`, see + https://github.com/sharkdp/lscolors/pull/35 (@tavianator) ## Bugfixes @@ -23,6 +27,7 @@ - Support `--list-details` on more platforms (like BusyBox), see #783 - The filters `--owner`, `--size`, and `--changed-{within,before}` now apply to symbolic links themselves, rather than the link target, except when `--follow` is specified; see #863 +- Change time comparisons to be exclusive, see #794 (@jacobmischka) ## Changes diff --git a/Cargo.lock b/Cargo.lock index 7cfa182d..4cc7fcfc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "ee10e43ae4a853c0a3591d4e2ada1719e553be18199d9da9d4a83f5927c2f5c7" [[package]] name = "atty" @@ -54,24 +54,24 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" dependencies = [ "memchr", ] [[package]] name = "cc" -version = "1.0.70" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -120,9 +120,9 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377c9b002a72a0b2c1a18c62e2f3864bdfea4a015e3683a96e24aa45dd6c02d1" +checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf" dependencies = [ "nix", "winapi", @@ -172,15 +172,16 @@ dependencies = [ "humantime", "ignore", "jemallocator", - "lazy_static", "libc", "lscolors", + "nix", "normpath", "num_cpus", "once_cell", "regex", "regex-syntax", "tempdir", + "test-case", "users", "version_check", ] @@ -301,9 +302,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.103" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" +checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" [[package]] name = "log" @@ -316,9 +317,9 @@ dependencies = [ [[package]] name = "lscolors" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b894c45c9da468621cdd615a5a79ee5e5523dd4f75c76ebc03d458940c16e" +checksum = "9dd58d8727f3035fa6d5272f16b519741fd4875936b99d8a7cde21291b7d9174" dependencies = [ "ansi_term 0.12.1", ] @@ -340,9 +341,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.22.1" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7555d6c7164cc913be1ce7f95cbecdabda61eb2ccd89008524af306fb7f5031" +checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188" dependencies = [ "bitflags", "cc", @@ -353,9 +354,9 @@ dependencies = [ [[package]] name = "normpath" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27e6e8f70e9fbbe3752d330d769e3424f24b9458ce266df93a3b456902fd696a" +checksum = "640c20e9df4a2d4a5adad5b47e17d76dac3e824346b181931c3ec9f7a85687b1" dependencies = [ "winapi", ] @@ -395,6 +396,24 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rand" version = "0.4.6" @@ -492,6 +511,17 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "syn" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "tempdir" version = "0.3.7" @@ -512,6 +542,19 @@ dependencies = [ "winapi", ] +[[package]] +name = "test-case" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7cad0a06f9a61e94355aa3b3dc92d85ab9c83406722b1ca5e918d4297c12c23" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", + "version_check", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -543,9 +586,15 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "users" diff --git a/Cargo.toml b/Cargo.toml index 93fe8456..e8f21290 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,13 +37,12 @@ version_check = "0.9" ansi_term = "0.12" atty = "0.2" ignore = "0.4.3" -lazy_static = "1.1.0" num_cpus = "1.8" regex = "1.5.4" regex-syntax = "0.6" ctrlc = "3.2" humantime = "2.1" -lscolors = "0.7" +lscolors = "0.8" globset = "0.4" anyhow = "1.0" dirs-next = "2.0" @@ -57,6 +56,7 @@ features = ["suggestions", "color", "wrap_help"] [target.'cfg(unix)'.dependencies] users = "0.11.0" +nix = "0.23.0" [target.'cfg(all(unix, not(target_os = "redox")))'.dependencies] libc = "0.2" @@ -70,7 +70,8 @@ jemallocator = "0.3.0" [dev-dependencies] diff = "0.1" tempdir = "0.3" -filetime = "0.2.15" +filetime = "0.2" +test-case = "1.2" [profile.release] lto = true diff --git a/README.md b/README.md index e7b56d38..e0fc261e 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,14 @@ use a character class with a single hyphen character: > fd '[-]pattern' ``` +### "Command not found" for `alias`es or shell functions + +Shell `alias`es and shell functions can not be used for command execution via `fd -x` or +`fd -X`. In `zsh`, you can make the alias global via `alias -g myalias="…"`. In `bash`, +you can use `export -f my_function` to make available to child processes. You would still +need to call `fd -x bash -c 'my_function "$1"' bash`. For other use cases or shells, use +a (temporary) shell script. + ## Integration with other programs ### Using fd with `fzf` @@ -667,7 +675,6 @@ cargo install --path . - [sharkdp](https://github.com/sharkdp) - [tmccombs](https://github.com/tmccombs) - [tavianator](https://github.com/tavianator) -- [pemistahl](https://github.com/pemistahl/) ## License diff --git a/contrib/completion/_fd b/contrib/completion/_fd index 28b37eef..f8521a76 100644 --- a/contrib/completion/_fd +++ b/contrib/completion/_fd @@ -138,6 +138,7 @@ _fd() { + '(exec-cmds)' # execute command '(long-listing max-results)'{-x+,--exec=}'[execute command for each search result]:command: _command_names -e:*\;::program arguments: _normal' '(long-listing max-results)'{-X+,--exec-batch=}'[execute command for all search results at once]:command: _command_names -e:*\;::program arguments: _normal' + '(long-listing max-results)--batch-size=[max number of args for each -X call]:size' + other '!(--max-buffer-time)--max-buffer-time=[set amount of time to buffer before showing output]:time (ms)' diff --git a/doc/fd.1 b/doc/fd.1 index 69e84385..51deaff6 100644 --- a/doc/fd.1 +++ b/doc/fd.1 @@ -28,7 +28,7 @@ is a simple, fast and user-friendly alternative to .TP .B \-H, \-\-hidden Include hidden files and directories in the search results -(default: hidden files and directories are skipped). +(default: hidden files and directories are skipped). The flag can be overridden with '--no-hidden'. .TP .B \-I, \-\-no\-ignore Show search results from files and directories that would otherwise be ignored by @@ -49,6 +49,8 @@ The global fd ignore file (usually .I $HOME/.config/fd/ignore ) .RE +.IP +The flag can be overridden with '--ignore'. .TP .B \-u, \-\-unrestricted Alias for '--no-ignore'. Can be repeated; '-uu' is an alias for '--no-ignore --hidden'. @@ -62,6 +64,7 @@ and the global gitignore configuration .RI ( core.excludesFile git setting, which defaults to .IR $HOME/.config/git/ignore ). +The flag can be overridden with '--ignore-vcs'. .TP .B \-s, \-\-case\-sensitive Perform a case-sensitive search. By default, fd uses case-insensitive searches, unless the @@ -84,6 +87,7 @@ performs substring comparison. If you want to match on an exact filename, consid .TP .B \-a, \-\-absolute\-path Shows the full path starting from the root as opposed to relative paths. +The flag can be overridden with '--relative-path'. .TP .B \-l, \-\-list\-details Use a detailed listing format like 'ls -l'. This is basically an alias @@ -93,7 +97,7 @@ sort order. .TP .B \-L, \-\-follow By default, fd does not descend into symlinked directories. Using this flag, symbolic links are -also traversed. +also traversed. The flag can be overridden with '--no-follow'. .TP .B \-p, \-\-full\-path By default, the search pattern is only matched against the filename (or directory name). Using @@ -154,18 +158,40 @@ regular files directories .IP "l, symlink" symbolic links -.IP "x, executable" -executable (files) -.IP "e, empty" -empty files or directories .IP "s, socket" sockets .IP "p, pipe" named pipes (FIFOs) +.IP "x, executable" +executable (files) +.IP "e, empty" +empty files or directories .RE .RS -This option can be used repeatedly to allow for multiple file types. +This option can be specified more than once to include multiple file types. +Searching for '--type file --type symlink' will show both regular files as well as +symlinks. Note that the 'executable' and 'empty' filters work differently: '--type +executable' implies '--type file' by default. And '--type empty' searches for +empty files and directories, unless either '--type file' or '--type directory' is +specified in addition. + +Examples: + - Only search for files: + fd --type file … + fd -tf … + - Find both files and symlinks + fd --type file --type symlink … + fd -tf -tl … + - Find executable files: + fd --type executable + fd -tx + - Find empty files: + fd --type empty --type file + fd -te -tf + - Find empty directories: + fd --type empty --type directory + fd -te -td .RE .TP .BI "\-e, \-\-extension " ext @@ -243,7 +269,7 @@ tebibytes .TP .BI "\-\-changed-within " date|duration Filter results based on the file modification time. -Files with modification times greater than or equal to the argument will be returned. +Files with modification times greater than the argument will be returned. The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point in time in either full RFC3339 format with time zone, or as a date or datetime in the local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR). @@ -256,7 +282,7 @@ Examples: .TP .BI "\-\-changed-before " date|duration Filter results based on the file modification time. -Files with modification times less than or equal to the argument will be returned. +Files with modification times less than the argument will be returned. The argument can be provided as a duration (\fI10h, 1d, 35min\fR) or as a specific point in time in either full RFC3339 format with time zone, or as a date or datetime in the local time zone (\fIYYYY-MM-DD\fR or \fIYYYY-MM-DD HH:MM:SS\fR). @@ -405,5 +431,11 @@ $ fd -e py .TP .RI "Open all search results with vim:" $ fd pattern -X vim +.TP +.BI "\-\-batch\-size " size +Pass at most +.I size +arguments to each call to the command given with -X. +.TP .SH SEE ALSO .BR find (1) diff --git a/src/app.rs b/src/app.rs index 9c1c04e8..dd4282b2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,7 +25,17 @@ pub fn build_app() -> App<'static, 'static> { .long_help( "Include hidden directories and files in the search results (default: \ hidden files and directories are skipped). Files and directories are \ - considered to be hidden if their name starts with a `.` sign (dot).", + considered to be hidden if their name starts with a `.` sign (dot). \ + The flag can be overridden with --no-hidden.", + ), + ) + .arg( + Arg::with_name("no-hidden") + .long("no-hidden") + .overrides_with("hidden") + .hidden(true) + .long_help( + "Overrides --hidden.", ), ) .arg( @@ -36,7 +46,17 @@ pub fn build_app() -> App<'static, 'static> { .help("Do not respect .(git|fd)ignore files") .long_help( "Show search results from files and directories that would otherwise be \ - ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file.", + ignored by '.gitignore', '.ignore', '.fdignore', or the global ignore file. \ + The flag can be overridden with --ignore.", + ), + ) + .arg( + Arg::with_name("ignore") + .long("ignore") + .overrides_with("no-ignore") + .hidden(true) + .long_help( + "Overrides --no-ignore.", ), ) .arg( @@ -47,7 +67,16 @@ pub fn build_app() -> App<'static, 'static> { .help("Do not respect .gitignore files") .long_help( "Show search results from files and directories that would otherwise be \ - ignored by '.gitignore' files.", + ignored by '.gitignore' files. The flag can be overridden with --ignore-vcs.", + ), + ) + .arg( + Arg::with_name("ignore-vcs") + .long("ignore-vcs") + .overrides_with("no-ignore-vcs") + .hidden(true) + .long_help( + "Overrides --no-ignore-vcs.", ), ) .arg( @@ -72,6 +101,7 @@ pub fn build_app() -> App<'static, 'static> { Arg::with_name("rg-alias-hidden-ignore") .short("u") .long("unrestricted") + .overrides_with_all(&["ignore", "no-hidden"]) .multiple(true) .hidden_short_help(true) .help("Alias for '--no-ignore', and '--hidden' when given twice") @@ -145,7 +175,17 @@ pub fn build_app() -> App<'static, 'static> { .overrides_with("absolute-path") .help("Show absolute instead of relative paths") .long_help( - "Shows the full path starting from the root as opposed to relative paths.", + "Shows the full path starting from the root as opposed to relative paths. \ + The flag can be overridden with --relative-path.", + ), + ) + .arg( + Arg::with_name("relative-path") + .long("relative-path") + .overrides_with("absolute-path") + .hidden(true) + .long_help( + "Overrides --absolute-path.", ), ) .arg( @@ -170,7 +210,17 @@ pub fn build_app() -> App<'static, 'static> { .help("Follow symbolic links") .long_help( "By default, fd does not descend into symlinked directories. Using this \ - flag, symbolic links are also traversed.", + flag, symbolic links are also traversed. \ + Flag can be overriden with --no-follow.", + ), + ) + .arg( + Arg::with_name("no-follow") + .long("no-follow") + .overrides_with("follow") + .hidden(true) + .long_help( + "Overrides --follow.", ), ) .arg( @@ -178,11 +228,12 @@ pub fn build_app() -> App<'static, 'static> { .long("full-path") .short("p") .overrides_with("full-path") - .help("Search full path (default: file-/dirname only)") + .help("Search full abs. path (default: filename only)") .long_help( "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.", + directory name). Using this flag, the pattern is matched against the full \ + (absolute) path. Example:\n \ + fd --glob -p '**/.git/config'", ), ) .arg( @@ -249,7 +300,8 @@ pub fn build_app() -> App<'static, 'static> { .conflicts_with_all(&["size", "exact-depth"]) .hidden_short_help(true) .help("Do not traverse into matching directories") - .long_help("Do not traverse into matching directories.") + .long_help("Do not traverse into directories that match the search criteria. If \ + you want to exclude specific directories, use the '--exclude=…' option.") ) .arg( Arg::with_name("file-type") @@ -281,14 +333,36 @@ pub fn build_app() -> App<'static, 'static> { empty (e), socket (s), pipe (p)", ) .long_help( - "Filter the search by type (multiple allowable filetypes can be specified):\n \ + "Filter the search by type:\n \ 'f' or 'file': regular files\n \ 'd' or 'directory': directories\n \ 'l' or 'symlink': symbolic links\n \ - 'x' or 'executable': executables\n \ - 'e' or 'empty': empty files or directories\n \ 's' or 'socket': socket\n \ - 'p' or 'pipe': named pipe (FIFO)", + 'p' or 'pipe': named pipe (FIFO)\n\n \ + 'x' or 'executable': executables\n \ + 'e' or 'empty': empty files or directories\n\n\ + This option can be specified more than once to include multiple file types. \ + Searching for '--type file --type symlink' will show both regular files as \ + well as symlinks. Note that the 'executable' and 'empty' filters work differently: \ + '--type executable' implies '--type file' by default. And '--type empty' searches \ + for empty files and directories, unless either '--type file' or '--type directory' \ + is specified in addition.\n\n\ + Examples:\n \ + - Only search for files:\n \ + fd --type file …\n \ + fd -tf …\n \ + - Find both files and symlinks\n \ + fd --type file --type symlink …\n \ + fd -tf -tl …\n \ + - Find executable files:\n \ + fd --type executable\n \ + fd -tx\n \ + - Find empty files:\n \ + fd --type empty --type file\n \ + fd -te -tf\n \ + - Find empty directories:\n \ + fd --type empty --type directory\n \ + fd -te -td" ), ) .arg( @@ -365,6 +439,21 @@ pub fn build_app() -> App<'static, 'static> { " ), ) + .arg( + Arg::with_name("batch-size") + .long("batch-size") + .takes_value(true) + .value_name("size") + .hidden_short_help(true) + .requires("exec-batch") + .help("Max number of arguments to run as a batch with -X") + .long_help( + "Maximum number of arguments to pass to the command given with -X. \ + If the number of results is greater than the given size, \ + the command given with -X is run again with remaining arguments. \ + A batch size of zero means there is no limit.", + ), + ) .arg( Arg::with_name("exclude") .long("exclude") diff --git a/src/config.rs b/src/config.rs index 91ca6014..5a717148 100644 --- a/src/config.rs +++ b/src/config.rs @@ -85,6 +85,10 @@ pub struct Config { /// If a value is supplied, each item found will be used to generate and execute commands. pub command: Option>, + /// Maximum number of search results to pass to each `command`. If zero, the number is + /// unlimited. + pub batch_size: usize, + /// A list of glob patterns that should be excluded from the search. pub exclude_patterns: Vec, diff --git a/src/exec/job.rs b/src/exec/job.rs index 83abf1a1..aa8164cd 100644 --- a/src/exec/job.rs +++ b/src/exec/job.rs @@ -50,6 +50,7 @@ pub fn batch( cmd: &CommandTemplate, show_filesystem_errors: bool, buffer_output: bool, + limit: usize, ) -> ExitCode { let paths = rx.iter().filter_map(|value| match value { WorkerResult::Entry(val) => Some(val), @@ -60,5 +61,17 @@ pub fn batch( None } }); - cmd.generate_and_execute_batch(paths, buffer_output) + if limit == 0 { + // no limit + return cmd.generate_and_execute_batch(paths, buffer_output); + } + + let mut exit_codes = Vec::new(); + let mut peekable = paths.peekable(); + while peekable.peek().is_some() { + let limited = peekable.by_ref().take(limit); + let exit_code = cmd.generate_and_execute_batch(limited, buffer_output); + exit_codes.push(exit_code); + } + merge_exitcodes(exit_codes) } diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 15e1ae52..3f93acdb 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -10,7 +10,7 @@ use std::process::{Command, Stdio}; use std::sync::{Arc, Mutex}; use anyhow::{anyhow, Result}; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; use crate::exit_codes::ExitCode; @@ -71,9 +71,8 @@ impl CommandTemplate { I: IntoIterator, S: AsRef, { - lazy_static! { - static ref PLACEHOLDER_PATTERN: Regex = Regex::new(r"\{(/?\.?|//)\}").unwrap(); - } + static PLACEHOLDER_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\{(/?\.?|//)\}").unwrap()); let mut args = Vec::new(); let mut has_placeholder = false; diff --git a/src/exit_codes.rs b/src/exit_codes.rs index 4f8a9748..e44111bb 100644 --- a/src/exit_codes.rs +++ b/src/exit_codes.rs @@ -1,3 +1,8 @@ +use std::process; + +#[cfg(unix)] +use nix::sys::signal::{raise, signal, SigHandler, Signal}; + #[derive(Debug, Clone, Copy, PartialEq)] pub enum ExitCode { Success, @@ -21,6 +26,21 @@ impl ExitCode { fn is_error(self) -> bool { i32::from(self) != 0 } + + /// Exit the process with the appropriate code. + pub fn exit(self) -> ! { + #[cfg(unix)] + if self == ExitCode::KilledBySigint { + // Get rid of the SIGINT handler, if present, and raise SIGINT + unsafe { + if signal(Signal::SIGINT, SigHandler::SigDfl).is_ok() { + let _ = raise(Signal::SIGINT); + } + } + } + + process::exit(self.into()) + } } pub fn merge_exitcodes(results: impl IntoIterator) -> ExitCode { diff --git a/src/filter/size.rs b/src/filter/size.rs index 5435b195..2af20bc4 100644 --- a/src/filter/size.rs +++ b/src/filter/size.rs @@ -1,9 +1,8 @@ -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; -lazy_static! { - static ref SIZE_CAPTURES: Regex = Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap(); -} +static SIZE_CAPTURES: Lazy = + Lazy::new(|| Regex::new(r"(?i)^([+-]?)(\d+)(b|[kmgt]i?b?)$").unwrap()); #[derive(Clone, Copy, Debug, PartialEq)] pub enum SizeFilter { diff --git a/src/filter/time.rs b/src/filter/time.rs index 39979afa..eb43c4bb 100644 --- a/src/filter/time.rs +++ b/src/filter/time.rs @@ -39,8 +39,8 @@ impl TimeFilter { pub fn applies_to(&self, t: &SystemTime) -> bool { match self { - TimeFilter::Before(limit) => t <= limit, - TimeFilter::After(limit) => t >= limit, + TimeFilter::Before(limit) => t < limit, + TimeFilter::After(limit) => t > limit, } } } diff --git a/src/main.rs b/src/main.rs index 974c5612..cb6dfa8f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,6 @@ mod walk; use std::env; use std::path::{Path, PathBuf}; -use std::process; use std::sync::Arc; use std::time; @@ -54,11 +53,11 @@ fn main() { let result = run(); match result { Ok(exit_code) => { - process::exit(exit_code.into()); + exit_code.exit(); } Err(err) => { eprintln!("[fd error]: {:#}", err); - process::exit(ExitCode::GeneralError.into()); + ExitCode::GeneralError.exit(); } } } @@ -348,6 +347,12 @@ fn construct_config(matches: clap::ArgMatches, pattern_regex: &str) -> Result()) + .transpose() + .context("Failed to parse --batch-size argument")? + .unwrap_or_default(), exclude_patterns: matches .values_of("exclude") .map(|v| v.map(|p| String::from("!") + p).collect()) diff --git a/src/output.rs b/src/output.rs index d4f9174c..5fae330f 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,10 +1,10 @@ +use std::borrow::Cow; use std::io::{self, StdoutLock, Write}; use std::path::Path; -use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use lscolors::{LsColors, Style}; +use lscolors::{Indicator, LsColors, Style}; use crate::config::Config; use crate::error::print_error; @@ -37,10 +37,10 @@ pub fn print_entry( if let Err(e) = r { if e.kind() == ::std::io::ErrorKind::BrokenPipe { // Exit gracefully in case of a broken pipe (e.g. 'fd ... | head -n 3'). - process::exit(0); + ExitCode::Success.exit(); } else { print_error(format!("Could not write to output: {}", e)); - process::exit(ExitCode::GeneralError.into()); + ExitCode::GeneralError.exit(); } } } @@ -53,32 +53,51 @@ fn print_entry_colorized( ls_colors: &LsColors, wants_to_quit: &Arc, ) -> io::Result<()> { - let default_style = ansi_term::Style::default(); + // Split the path between the parent and the last component + let mut offset = 0; + let path_str = path.to_string_lossy(); - // Traverse the path and colorize each component - for (component, style) in ls_colors.style_for_path_components(path) { - let style = style - .map(Style::to_ansi_term_style) - .unwrap_or(default_style); - - let mut path_string = component.to_string_lossy(); - if let Some(ref separator) = config.path_separator { - *path_string.to_mut() = replace_path_separator(&path_string, separator); - } - write!(stdout, "{}", style.paint(path_string))?; - - // TODO: can we move this out of the if-statement? Why do we call it that often? - if wants_to_quit.load(Ordering::Relaxed) { - writeln!(stdout)?; - process::exit(ExitCode::KilledBySigint.into()); + if let Some(parent) = path.parent() { + offset = parent.to_string_lossy().len(); + for c in path_str[offset..].chars() { + if std::path::is_separator(c) { + offset += c.len_utf8(); + } else { + break; + } } } + if offset > 0 { + let mut parent_str = Cow::from(&path_str[..offset]); + if let Some(ref separator) = config.path_separator { + *parent_str.to_mut() = replace_path_separator(&parent_str, separator); + } + + let style = ls_colors + .style_for_indicator(Indicator::Directory) + .map(Style::to_ansi_term_style) + .unwrap_or_default(); + write!(stdout, "{}", style.paint(parent_str))?; + } + + let style = ls_colors + .style_for_path(path) + .map(Style::to_ansi_term_style) + .unwrap_or_default(); + write!(stdout, "{}", style.paint(&path_str[offset..]))?; + if config.null_separator { - write!(stdout, "\0") + write!(stdout, "\0")?; } else { - writeln!(stdout) + writeln!(stdout)?; } + + if wants_to_quit.load(Ordering::Relaxed) { + ExitCode::KilledBySigint.exit(); + } + + Ok(()) } // TODO: this function is performance critical and can probably be optimized diff --git a/src/walk.rs b/src/walk.rs index 7850ad7e..ed3bf9f5 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -3,7 +3,6 @@ use std::ffi::OsStr; use std::fs::{FileType, Metadata}; use std::io; use std::path::{Path, PathBuf}; -use std::process; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc::{channel, Receiver, Sender}; use std::sync::{Arc, Mutex}; @@ -137,11 +136,9 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc, config: Arc) -> R if config.ls_colors.is_some() && config.command.is_none() { let wq = Arc::clone(&wants_to_quit); ctrlc::set_handler(move || { - if wq.load(Ordering::Relaxed) { + if wq.fetch_or(true, Ordering::Relaxed) { // Ctrl-C has been pressed twice, exit NOW - process::exit(ExitCode::KilledBySigint.into()); - } else { - wq.store(true, Ordering::Relaxed); + ExitCode::KilledBySigint.exit(); } }) .unwrap(); @@ -179,7 +176,13 @@ fn spawn_receiver( // This will be set to `Some` if the `--exec` argument was supplied. if let Some(ref cmd) = config.command { if cmd.in_batch_mode() { - exec::batch(rx, cmd, show_filesystem_errors, enable_output_buffering) + exec::batch( + rx, + cmd, + show_filesystem_errors, + enable_output_buffering, + config.batch_size, + ) } else { let shared_rx = Arc::new(Mutex::new(rx)); diff --git a/tests/tests.rs b/tests/tests.rs index 7c8d8ee2..807b91bf 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -4,6 +4,7 @@ use std::fs; use std::io::Write; use std::path::Path; use std::time::{Duration, SystemTime}; +use test_case::test_case; use normpath::PathExt; use regex::escape; @@ -1418,6 +1419,48 @@ fn test_exec_batch() { } } +#[test] +fn test_exec_batch_with_limit() { + // TODO Test for windows + if cfg!(windows) { + return; + } + + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + + te.assert_output( + &["foo", "--batch-size", "0", "--exec-batch", "echo", "{}"], + "./a.foo ./one/b.foo ./one/two/C.Foo2 ./one/two/c.foo ./one/two/three/d.foo ./one/two/three/directory_foo", + ); + + let output = te.assert_success_and_get_output( + ".", + &["foo", "--batch-size=2", "--exec-batch", "echo", "{}"], + ); + let stdout = String::from_utf8_lossy(&output.stdout); + + for line in stdout.lines() { + assert_eq!(2, line.split_whitespace().count()); + } + + let mut paths: Vec<_> = stdout + .lines() + .flat_map(|line| line.split_whitespace()) + .collect(); + paths.sort_unstable(); + assert_eq!( + &paths, + &[ + "./a.foo", + "./one/b.foo", + "./one/two/C.Foo2", + "./one/two/c.foo", + "./one/two/three/d.foo", + "./one/two/three/directory_foo" + ], + ); +} + /// Shell script execution (--exec) with a custom --path-separator #[test] fn test_exec_with_separator() { @@ -1899,6 +1942,30 @@ fn test_number_parsing_errors() { te.assert_failure(&["--max-results=a"]); } +#[test_case("--hidden", &["--no-hidden"] ; "hidden")] +#[test_case("--no-ignore", &["--ignore"] ; "no-ignore")] +#[test_case("--no-ignore-vcs", &["--ignore-vcs"] ; "no-ignore-vcs")] +#[test_case("--follow", &["--no-follow"] ; "follow")] +#[test_case("--absolute-path", &["--relative-path"] ; "absolute-path")] +#[test_case("-u", &["--ignore"] ; "u")] +#[test_case("-uu", &["--ignore", "--no-hidden"] ; "uu")] +fn test_opposing(flag: &str, opposing_flags: &[&str]) { + let te = TestEnv::new(DEFAULT_DIRS, DEFAULT_FILES); + + let mut flags = vec![flag]; + flags.extend_from_slice(opposing_flags); + let out_no_flags = te.assert_success_and_get_output(".", &[]); + let out_opposing_flags = te.assert_success_and_get_output(".", &flags); + + assert_eq!( + out_no_flags, + out_opposing_flags, + "{} should override {}", + opposing_flags.join(" "), + flag + ); +} + /// Print error if search pattern starts with a dot and --hidden is not set /// (Unix only, hidden files on Windows work differently) #[test]