Pass result of find command as another command's multiple options

I have a CLI tool that can take an option with multiple values – The syntax is like this:

CLI -I path/to/file1 -I path/to/file2 ...

How can I pass the result of the find command to this CLI?


For reference

If it’s positional multiple arguments:

CLI path/to/file1 path/to/file2 ...

I can do:

find "dir" -name "pattern" -exec CLI {} +
Asked By: hongweiy

||

As a first try,

find . -print | sed 's/^/-I /'| xargs  echo

would do. Warning: this has a number of issues with spaces and/or special characters in the filenames.

Answered By: Ljm Dullaart

If switching to zsh is an option, then, it’s easily done there with:

CLI **/pattern(P[-I])

Where the P glob qualifier is used to insert an argument as a Prefix to every matching file.

With GNU find and GNU xargs and a shell such as zsh or bash with support for ksh-style process substitution, you can also do:

xargs -xr0 -n100 -a <(find . -name pattern -printf '-I%p') CLI

100 being chosen here as an even number that is hoped to be small enough that the list of arguments will fit in a single invocation of CLI. Without it, xargs could split the list in between the -I and the following file path. If it still doesn’t fit within that -n 100 constraint, thanks to -x, xargs will exit instead of running CLI with fewer arguments.

If CLI accepts -Ipath/to/file in addition to -I path/to/file, you can also do:

find . -name pattern -exec bash -c '
  exec CLI "${@/#/-I}"' bash {} +

Note that though -exec cmd {} + is meant to work around the limit on size of arguments by calling cmd several times if necessary, since bash adds a -I to each argument, it could still run into that limit when it executes cmd itself.

However since the length of that -I prefix happens to be the same as that of the ./ prefix that is added to all file paths and that we don’t need here, we might as well tell bash to replace that ./ with -I and avoid the problem altogether:

find . -name pattern -exec bash -c '
  exec CLI "${@/#.//-I}"' bash {} +

(that assumes that . itself (the starting file) doesn’t match the pattern).

The zsh approach above doesn’t attempt to work around the execve() system call limit, and will either run CLI once or fail to run it altogether. The limit there can be worked around using the zargs autoloadable function.

If it’s important that CLI be called only once with one -I file per file, on modern versions of Linux, raising the stack size limit (with limit stacksize 100M in zsh for instance to raise it from the default of 8MiB) will help as the execve() limit is proportional to the stack size limit there.

Answered By: Stéphane Chazelas

Create an array of the args for CLI from the find output, adding -I before each file name, then call CLI with those args:

#!/usr/bin/env bash

args=()
while IFS= read -r file; do
    args+=( -I "$file" )
done < <(find "dir" -name "pattern" -print)

CLI "${args[@]}"

If your file names can contain newlines (doesn’t look like it from the example in your question) then add -d '' to the read arguments and use GNU find and change -print to -print0.

That above assumes the contents of args[] won’t exceed ARG_MAX.

Answered By: Ed Morton
Categories: Answers Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.