18

Something like the following is what I what I'm after, but my code doesn't work, no matter how I escape {} and + ;

find ./ -maxdepth 1 -type d -name '.*' -exec \
    find {} -maxdepth 1 -type f -name '*.ini' -exec \
        md5sum \{\} \\; \;

After seeing this Unix-&-Linux question, I found that the following code works, but it isn't nesting find as such, and I suspect there is a better way to do this particular job.

find ./ -maxdepth 1 -type d -name '.*' \
-exec bash -c 'for x; do
    find "$x" -maxdepth 1 -type f -name "*.ini" \
    -exec md5sum \{\} \;; \
done' _ {} \+

Is there some way to nest find -exec without the need to invoke a shell (as above), with all its whacky quoteing and escape constraints?

Or can this be done directly in a single find command, using a blend of its many parameters?

Peter.O
  • 32,916
  • 4
    While it may be possible to do what you're asking, when things get that complex, I switch to shell or Perl scripts. Your second code snippet is pretty much doing this, only with the shell script inline. Heroic one-liners are entertaining, but they're hard to understand, and thus hard to maintain. Unless this is a one-shot deal that you nevertheless somehow end up getting good at, I can't see a good reason to do it other than the intellectual challenge. – Warren Young Aug 05 '11 at 03:27
  • 1
    @Warren Young: I certainly don't think the concept is complex, but I assume you mean there is no simple way to do in with find, but if find can't do this, then why is find so revered(?) as the tool-to-use for finding files?... I've subseqeuntly found that find ./ -maxdepth 2 -path '.*/*.ini' -type f -exec md5sum {} \+ works fine in my situation (jw013's reference to -prune led me to this in the man page), but I wonder if it is a robust method(in genera). I've never really used find (in less than a year of Linux) as locate has done almost all I need, so it's unknown territory. – Peter.O Aug 05 '11 at 06:08
  • 1
    The -path test is exactly what I was going to suggest. With this, you should be able to do all that you want (sorry for the Ace Of Base association;) ) – rozcietrzewiacz Aug 05 '11 at 10:51

3 Answers3

12

I would try using a single find like:

find .*/ -maxdepth 1 -type f -name '*.ini' -execdir md5sum {} +

or even (no find at all, just shell globbing)

md5sum .*/*.ini

although this lacks the -type f check so only works if you have no directories/non-files ending in .ini. If you do you could use

for x in .*/*.ini; do 
    if [ -f "$x" ]; then 
        md5sum "$x"
    fi
done

which would however lose the advantage of only needing one md5sum invocation.

Edit

For a general and safe method of chaining find, you can do something like

find <paths> <args> -print0 | xargs -0 -I{.} find {.} <args for second find> [etc.]
jw013
  • 51,212
  • I get an error with the -f (f ?), and then another error with -execdir.. When I replace -execdir with -exec, and/or also replacing md5sum with print, I get nothing.. – Peter.O Aug 05 '11 at 05:54
  • Thanks, for the alternative... but I'm more after a way of doing this using find ... It's not so much that I just want this example resolved, I'm looking for insights into the ways of find-fu ... maybe there more fluff than fu in find.. (I don't know, because I've virtually never used it), and this is the first situation I've really wanted to use it (for image files actually) and the -exec feature I've heard so much about seems to be not as all-powerful as its rep(?) alludes to... (+1 for the alternatives, though) .. but your find example just doesn't work (still) – Peter.O Aug 05 '11 at 06:31
  • I get this error for the find command: ... find: The relative path~/bin' is included in the PATH environment variable, which is insecure in combination with the -execdir action of find. Please remove that entry from $PATH.... So maybe it will work, but I must say I've been trying to get rid of that ~/bin for a while now... I"ll have to take as more serious look at it... I don't know where I"ve set it... any ideas where It may be lurking; the~/bin` in my PATH – Peter.O Aug 05 '11 at 06:36
  • I think the power of find is best appreciated in situations which can't be done entirely with globs, but as those kinds of situations are rare I don't normally need find much. – jw013 Aug 05 '11 at 06:40
  • Okay.. that's an interesting and good point (about using globs)... – Peter.O Aug 05 '11 at 06:43
  • That's a built-in security feature of find. You shouldn't have relative paths in PATH if you want to use find -exec, and it's not a great idea anyways. I'd check ~/.profile, ~/.bash_profile and /etc/profile. – jw013 Aug 05 '11 at 06:43
  • I've finally sorted out the ~/bin in my path problem, and I've now tried your find examples.. The last one, with xargs, is certainly up to the task, as were the non-find methods.. well done! thanks... – Peter.O Aug 05 '11 at 18:28
  • 1
    The find ... | xargs ... method did the trick for me... I was trying to achieve exactly that. I needed a general solution that would look for a folder and THEN look for a specific file in that folder and execute a command on it – LukeSavefrogs Aug 18 '21 at 05:56
  • -I is not supported on some xargs versions, like toybox's... (it is in POSIX though). In those cases | xargs -r0 -n1 sh -c 'find "$@" -exec ls -l {} +' - seems to work. (that was my usecase - -ls on find would be nicer bu is also not in toybox) – Gert van den Berg Jan 28 '23 at 14:00
3

Your original problem does not require calling find recursively but I suppose that was not the point.

I believe it is not possible to call find recursively in the way you want.

The following is not calling find recursively (or nesting, whatever it is called) either, but can't you just take a result set of the first find and feed it to the second one? This is how I would instinctively do:

find `find ./ -maxdepth 1 -type d -name '.*'` \
    -maxdepth 1 -type f -name '*.ini' -exec md5sum {} \;

You could also use xargs for executing the second find.

Update:

I wanted to add that because most UNIX utilities take several file name arguments instead of one, you can usually avoid the -exec altogether:

md5sum `find \`find ./ -maxdepth 1 -type d -name '.*'\` -maxdepth 1 -type f -name '*.ini'`

When nesting backticks you just add backslashes \ before the inner ones.

If we imagine that md5sum takes only one filename argument, we can always wrap it in a for loop:

for f in `find \`find ./ -maxdepth 1 -type d -name '.*'\` -maxdepth 1 -type f -name '*.ini'`
do
    md5sum $f
done

Note that this becomes more difficult if file/directory names starting with - or containing a space are involved. UNIX utilities do not play nicely with them. In that case adding ./, -- or quotes is needed.

Obviously the original example is not a good one, because we could just do:

md5sum .*/*.ini
snap
  • 631
  • 1
    You've given a good overview of the situation, but all of the shown find methods get an error when a file/directory contains spaces... The md5sum .*/*.ini works fine... I'm starting to get the general feel that * Warren Young's* comment about things getting 'complex' for find happens early on in the game :), but I assume find comes into its own when the condition tests are more intricate, but as far as nesting -exec goes, I've pretty well dropped the idea, as it seems there are simpler ways to do it.. (but perl isn't "simple" to me (yet)... – Peter.O Aug 05 '11 at 10:13
0

At least I managed to nest 2 find commands:

find ~ -maxdepth 1 -type d -name '.*' -execdir \
    find {} -maxdepth 1 -type f -name '*.ini' \;

But I didn't solve to invoke another -exec(dir) - call from there.

user unknown
  • 10,482