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
4
  • @don_crissti no it's not; but i figure a find calling another find was...
    – umläute
    Commented Apr 7, 2016 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 ? Commented Apr 7, 2016 at 14:41
  • 1
    How about something like find ... | while read dir; do?
    – terdon
    Commented Apr 7, 2016 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
    Commented Apr 21, 2016 at 7:58

2 Answers 2

5

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'
4
  • 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. Commented Apr 7, 2016 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. Commented Apr 9, 2016 at 5:37
  • If you want to have more control about subdirectories than -path gives you you can try -regex.
    – Lucas
    Commented Apr 14, 2016 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
    Commented Apr 21, 2016 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.

You must log in to answer this question.

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