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 {} +
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.
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 P
refix 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 p
ath. 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.
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.