58

When I run this command in the shell (in a non-empty directory):

find . -exec invalid_command_here {} \;

I get this:

find: invalid_command_here: No such file or directory
find: invalid_command_here: No such file or directory
find: invalid_command_here: No such file or directory

(and so on for each file)

I need find to fail after the first error. Is there any way to get this to work? I can't use xargs, as I have spaces in my path, but I need the script calling this to return an error code.

1

3 Answers 3

52

This is a limitation of find. The POSIX standard specifies that the return status of find is 0 unless an error occurred while traversing the directories; the return status of executed commands doesn't enter into it.

You can make commands write their status to a file or to a descriptor:

find_status_file=$(mktemp findstatus)
: >"$find_status_file"
find … -exec sh -c 'trap "echo \$?" EXIT; invalid_command "$0"' {} \;
if [ -s "$find_status_file" ]; then
  echo 1>&2 "An error occurred"
fi
rm -f "$find_status_file"

Another method, as you discovered, is to use xargs. The xargs commands always processes all files, but returns the status 1 if any of the commands returns a nonzero status.

find … -print0 | xargs -0 -n1 invalid_command

Yet another method is to eschew find and use recursive globbing in the shell instead: **/ means any depth of subdirectories. This requires version 4 or above of bash; macOS is stuck at version 3.x so you'd have to install it from a port collection. Use set -e to halt the script on the first command returning a nonzero status.

shopt -s globstar
set -e
for x in **/*.xml; do invalid_command "$x"; done

Beware that in bash 4.0 through 4.2, this works but traverses symbolic links to directories, which is usually not desirable.

If you use zsh instead of bash, recursive globbing works out of the box with no gotchas. Zsh is available by default on OSX/macOS. In zsh, you can just write

set -e
for x in **/*.xml; do invalid_command "$x"; done
9
  • The xargs approach works in general but somehow breaks on bash -c commands. For example: find . -name '*.xml' -print0 | xargs -0 -n 1 -I '{}' bash -c "foo {}". This is executed multiple times whereas find . -name '2*.xml' -print0 | xargs -0 -n 1 -I '{}' foo {} is executed once and fails. Any idea why? Mar 11, 2019 at 3:26
  • 3
    @DKroot Never use {} inside bash -c. This takes the file name and inserts it directly inside the shell command. If the file name contains characters which have a special meaning in the shell such as spaces, the shell interprets these special characters as such. If you need a shell, pass {} as a separate argument, e.g. bash -c 'foo "$0"' {} (note the double quotes around $0, as well). Mar 11, 2019 at 8:14
  • 1
    @Giles Because by invalid_command I meant a script or a command returning a non-zero exit code. I think @Steven Fisher meant that too. Good point on RTFM. I went to xargs docs and converged onto a solution for custom scripts. Returning exit code 255 would tell xargs to terminate early with bash -c or without. All is good! Thanks. Apr 3, 2019 at 20:10
  • 1
    @Gilles Isn't ... | xargs -0 -n 1 -I {} bash -c 'echo "$0"' {} the same as ... | xargs -0 -n 1 bash -c 'echo "$0"'?
    – Rolf
    May 7, 2020 at 5:56
  • 1
    @Rolf Yes, if you're passing -0 and -n 1 then I don't think -I {} makes any difference. May 7, 2020 at 8:20
26

I can use this instead:

find . -name *.xml -print0 | xargs -n 1 -0 invalid_command
0
23

xargs is one option. However, it's actually trivially easy to do this with find as well by using + instead of \;

-exec  utility_name  [argument ...]   {} +

From the POSIX documentation:

If the primary expression is punctuated by a plus sign, the primary shall always evaluate as true, and the pathnames for which the primary is evaluated shall be aggregated into sets. The utility utility_name shall be invoked once for each set of aggregated pathnames. Each invocation shall begin after the last pathname in the set is aggregated, and shall be completed before the find utility exits and before the first pathname in the next set (if any) is aggregated for this primary, but it is otherwise unspecified whether the invocation occurs before, during, or after the evaluations of other primaries. If any invocation returns a non-zero value as exit status, the find utility shall return a non-zero exit status. An argument containing only the two characters “{}” shall be replaced by the set of aggregated pathnames, with each pathname passed as a separate argument to the invoked utility in the same order that it was aggregated. The size of any set of two or more pathnames shall be limited such that execution of the utility does not cause the system’s {ARG_MAX} limit to be exceeded. If more than one argument containing only the two characters “{}” is present, the behavior is unspecified.

4
  • this works for me, shouldn't this be the correct answer?
    – Higgs
    Aug 12, 2021 at 14:43
  • 2
    @Higgs On stackoverflow, 90% of the time being first is all that matters.
    – Swiss
    Aug 13, 2021 at 23:26
  • 3
    @Higgs and @Swiss — This will work for some use cases, but it requires the -execed command to accept multiple arguments (in this case all the elements found by the find expression), since this is what the + operator does. It collects node names, then executes -exec with all those names as arguments. Which, if I'm not mistaken, could exceed the max number of arguments possible in the shell, for one, and again also needs support from the invoked script or program. Dec 22, 2021 at 11:31
  • 1
    For what it's worth, this did not work for me with the current zsh. As for being first, it does matter more than it should… but six years later is a hard sell. 😀 Nov 10, 2022 at 19:35

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .