3

Is there a way to call find recursively?

I would like to search for files matching a given pattern only in directories matching another pattern.

Basically:

for each (sub)directory D matching "*.data"
do
  for each file F in directory D (with subdirs) matching "*.important.txt"
  do
    run command foo on F
  done
done

Now, if I leave out the innermost requirement (run command foo in F), it is pretty simple:

find . -type d -name "*.data" \
     -exec find \{\} -type f -name "*.important.txt" \;

However, I haven't found a way to pass a command to the inner find. E.g. the following prints out find: missing argument to `-exec' each time the inner find is called:

 find . -type d -name "*.data" \
      -exec find \{\} -type f -name "*.important.txt" \
                 -exec "foo \{\} \;" \;

Any solution should be posix compliant (runnable within a /bin/sh script), esp. I am not looking for solutions that

  • wrap the inner find into a separate shell-script
  • wrap the inner find into a bash-function
  • @don_crissti no it's not; but i figure a find calling another find was... – umläute Apr 7 '16 at 14:39
  • So you only need to find directories matching *.data and then for each file matching *.important in those directories (without descending) you have to run some command, right ? – don_crissti Apr 7 '16 at 14:41
  • 1
    How about something like find ... | while read dir; do? – terdon Apr 7 '16 at 15:17
  • 1
    FYI, you don't need to escape the {}; empty braces are not special to the shell, and quoting them doesn't change how find will "see" them anyway. – Wildcard Apr 21 '16 at 7:58
4

To run find on its own result, you can use the -c argument to sh (or bash) to prevent the outer find command from treating the inner {} specially. However, you then need to pass the result from the outer find as an argument to sh, which can be expanded with $0:

find . -type d -name "*.data" \
      -exec sh -c 'find "$0" -type f -name "*.important.txt" -exec echo \{\} \;' \{\} \;

Note: $0 should be quoted ("$0") to prevent issues with directory names containing whitespace.

This is not really a "recursive" solution, because it doesn't allow arbitrarily deep nesting without some hairy escaping, but it does support the two levels of find -execs you asked for in your example.

If your example is similar to your actual problem, you might also experiment with the -path argument to find instead:

find . -path '*.data/*important.txt'
  • I'm not sure if -path would be the right choice here - depends whether OP wants to descend (as -path would also match something like .../somedir.data/another_dir/somefile.important.txt) or not. If no recursion is needed inside those .data dirs then maybe something like find . -type d -name \*.data -exec sh -c 'for f in "$0"/*.important.txt;do stuff "$f";done' {} \; would be better. – don_crissti Apr 7 '16 at 18:25
  • While the pseudocode in OP's example suggests that OP wants only to operate on direct children of directories matching *.data, the broken attempt in OP's third code block suggests to me that files matching *.important.txt in all descendant subdirectories should be passed as an argument to foo. Both my solution and the -path suggestion would do this. You are correct that a simple glob would be easier if this is not required. – Ian Robertson Apr 9 '16 at 5:37
  • If you want to have more control about subdirectories than -path gives you you can try -regex. – Lucas Apr 14 '16 at 11:46
  • yes indeed, a simple globbing does not help me; i need to descend into the directories (i've updated my pseudo-code) – umläute Apr 21 '16 at 7:50
1

bash version (not POSIX compliant)

#!/bin/bash
find . -type d -name 'a *' -print0 \
    | while IFS= read -r -d '' dir ; do
        find "$dir" -type f -name "*.c" -exec echo \{\} \;
    done

Problems:


(bad) sh version (POSIX compliant)

#!/bin/sh
# WARNING: while this will work on directory names with spaces,
#          it will break on directory names that contain newlines
find . -type d -name 'a *' -print \
    | while IFS= read -r dir ; do
        find "$dir" -type f -name "*.c" -exec echo \{\} \;
    done

Problem: As stated in the comment, it will break on directory names containing newlines. You may think this is not a realistic issue, but this adds unnecessary fragility to your script. Maybe another program has a bug that will create such a directory, etc.


(better) sh version

see Ian Robertson's answer


Btw. if you need your scripts to be POSIX compliant, it's a good idea to use shellcheck. For example, it will notice that read -d is not defined in POSIX.

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.