3

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!).

  • You could add all the valid (test -d) paths to a shell array and pass that array to find as argument. – Janis May 29 '15 at 16:31
  • @Janis not every path, nor what you specify for find, has to be a directory – Anthon May 29 '15 at 17:07
3

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)).

  • I thought about how to do this in a shell, then realised I would never get the quoting right (again), and feared to end up with another answer that would needed heavy revision by your hand to be acceptable. So I took the cowardly path and used Python. – Anthon May 29 '15 at 17:29
1

Since you're using bash, store the list of paths in an array. Iterate over the array to build an array of existing paths. You do need a special case if the resulting array is empty, as find would either error out or traverse the current directory.

To be fully robust, ensure that none of the path arguments begins with a -, which find would interpret as an option or primary.

paths=(/some/where around/here -print 'one  with
odd spaces')
existing_paths=()
for x in "${paths[@]}"; do
  if [ -e "$x" ]; then
    if [[ "$x" = -* ]]; then x="./$x";; fi
    existing_paths+=("$x")
  fi
done
if [[ ${#existing_paths[@]} -ne 0 ]]; then
  find "${existing_paths[@]}" -type f -name foo.sh
fi
0

You can test for the existence of the path using python, echo the paths found with NUL separated and feed them into xargs to pass on to find, unless the length of the python output exceeds the maximum argument length that should invoke find only once:

python -c 'import os, sys;  sys.stdout.write("\0".join([x for x in sys.argv[1:] if os.path.exists(x)]) + "\0")' a\ b xyz abc| xargs -0 --no-run-if-empty find

The python part (the single quoted argument to -c):

  • imports the necessary modules os and sys used in the command
  • walks over the arguments a b, xyz and abc using for x in sys.argv[1:]
  • only puts in in the list if the path exists: [x ... if os.path.exists(x)]
  • joins the list with NUL and appends a NUL and writes that out to stdout: sys.stdout.write("\0".join[...] + "\0")

If you have an empty directory and do touch a\ b xyz you will see that the first two arguments are found but abc is not and find is never handed the latter path.

  • Nice but I think I could do the python part in sh as well...however, the hard part is the find --- I need some filters there and xargs only puts args at the end of the line (as far as I know xargs--which is very little ;)) – Alois Mahdal May 29 '15 at 21:06
0

if this works for you

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

then you can change paths function to echo only valid paths like

if [ -d "$path" ]; then echo $path fi

or if path can be a file too

if [ -d "$path" ] || [ -e "$path" ]; then echo $path fi

in this way, find will be executed only one time. if you can't modify paths function you always can create another one that will filter all valid paths before passing them to find command, so no invalid paths will be passed.

  • Why test on existence of directories? If is perfectly fine to hand the path to a file to find and the OP never mentioned that the paths were directories only. – Anthon May 29 '15 at 17:17
  • and OP also never mentioned that paths are file too. testing for directories will suppose that "starting points" are directories but if they a more than that you always can add / perform more tests. – Scantlight May 29 '15 at 17:34
  • I also thought of this but abandoned it as expanding the path list on the find command line might lead to a quoting hell--at least I think so... – Alois Mahdal May 29 '15 at 20:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.