From 17dd2a6dfec1ca2086c3d279f5c458758bc9a71b Mon Sep 17 00:00:00 2001 From: Devon Hollowood Date: Thu, 21 Oct 2021 23:05:13 -0700 Subject: [PATCH] Implement `--batch-size` (#866) --- CHANGELOG.md | 2 ++ contrib/completion/_fd | 1 + doc/fd.1 | 6 ++++++ src/app.rs | 15 +++++++++++++++ src/config.rs | 4 ++++ src/exec/job.rs | 15 ++++++++++++++- src/main.rs | 6 ++++++ src/walk.rs | 8 +++++++- tests/tests.rs | 42 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 97 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d48c317..d016699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ - Add new `--no-ignore-parent` flag, see #787 (@will459) +- Add new `--batch-size` flag, see #410 (@devonhollowood) + ## Bugfixes - Set default path separator to `/` in MSYS, see #537 and #730 (@aswild) diff --git a/contrib/completion/_fd b/contrib/completion/_fd index 28b37ee..a17c748 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 69e8438..66413cc 100644 --- a/doc/fd.1 +++ b/doc/fd.1 @@ -405,5 +405,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 b26593b..3f5bc2b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -365,6 +365,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 a053e6e..c11f88b 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 83abf1a..aa8164c 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/main.rs b/src/main.rs index da5fcd9..321df54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -348,6 +348,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/walk.rs b/src/walk.rs index 7850ad7..789a500 100644 --- a/src/walk.rs +++ b/src/walk.rs @@ -179,7 +179,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 a7c04f4..1baf15e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1418,6 +1418,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() {