Bash – find on a set of dirs that may not exist

bashfindshell

I need to invoke find on a set of "starting points" which is generated, but
some of paths may not be valid:

paths() {
    #
    # mock version of the generator
    #
    echo /bin
    echo kjsdfhk
    echo /etc
}

find $(paths) -type f name foo.sh

My problem is that I don't know if the path will be valid and if it's not, I want to silently ignore it. Easiest for me is now

paths \
  | while read path;
    do
        test -e "$path" || continue
        find "$path" -type f -name foo.sh
    done

but this is expensive: it invokes find for each valid path, and since the whole code may be called inside loop, I'd like to find a more effective way.

One easy and very unix-y solution would be to have find read the "starting points" from STDIN:

paths \
  | while read path;
    do
        test -e "$path" || continue
    done \
  | find - -type f -name foo.sh

except that… find does not support this! 🙂

Any ideas?

Note that the paths are provided by user, so spaces (and probably other funny things) need to be considered. Also I'm aiming for POSIX /bin/sh but that could be sacrificed. (Oh, and sinking whole STDERR to /dev/null is not an option either…)

Update: Sorry, I forgot to mention that I'm restricted to bash—I probably confused it more by the POSIX note. Actually the code is looking for snippets to source inside Bash scripts and is now mostly in sh with few bashisms that I might get rid of in future version. So if I could avoid putting more bashisms in, that would be cool but I certainly can't afford "zshisms", however elegant — as @stephane's answer (go vote it up now!).

Best Answer

In zsh, if you have the paths in an array as in:

files=($(paths))

(which would split the output of paths on space, tab newline or nul) or:

files=(${(f)"$(paths)"})

to split on lines, you can do:

find $^files(N) -type f -name foo.sh

Or if you want to restrict to directories:

find $^files(/N) -type f -name foo.sh

Now, if none of those files exist, you may end up running:

find -type f -name foo.sh

Which with some find implementations like GNU's means searching in the current directory. To avoid that, you could do:

dirs=($^files(/N))
(($#dirs)) && find $dirs -type f -name foo.sh

Or:

setopt cshnullglob
find $^files(/) -type f -name foo.sh

Now, with zsh, there's no real need for find here, you could simply do:

files=($^files/**/foo.sh(.N))

That would also have the benefit to work even if those files are like ! or -name which find would choke on.

There's a difference though in the case where those files are symlinks to directories (find would not look for files in them, while zsh would (which may actually be what you want)).

Related Question