Stop recursion of find command if match found
Asked Answered
C

2

6

Using GNU findutils, I need to search a directory tree for a certain file. If the file has been found for a given branch, I want to prevent find from recursing further into the branch. Say I want to find the file foo, and this is my directory tree:

├── a
│   ├── a1
│   │   └── foo
│   └── foo
├── b
└── c
    └── foo

Given I am searching the tree above, I want to find a/foo and c/foo. However, I don't want to find a/a1/foo since I already found foo in a parent directory to a1. It seems I should use the -prune flag to the find command and I found this link https://unix.stackexchange.com/questions/24557/how-do-i-stop-a-find-from-descending-into-found-directories, for example, but I cannot make it work. My attempts include:

$ find -name foo -type f -prune
./a/a1/foo <- Unwanted hit
./a/foo
./c/foo

and

$ find -name foo -type f -prune -exec find ../foo -type f {} \;
find: paths must precede expression: ./a/a1/foo
Usage: find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec] [path...] [expression]
find: paths must precede expression: ./a/foo
Usage: find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec] [path...] [expression]
find: paths must precede expression: ./c/foo
Usage: find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec] [path...] [expression]
Canute answered 7/2, 2019 at 17:44 Comment(2)
Why isn't ./a/foo the unwanted hit? Are we interested in that hit that is less deeper? What if mkdir -p a/a1 a/b1; touch a/a1/foo a/b1/foo. Then is a/a1/foo or a/b1/foo the unwanted hit? Or it doesn't matter, at least one should do it, no matter where?Impartial
@Impartial as I understood, in the example you give, both a/a1/foo and a/b1/foo should be returned.Eggshaped
E
10

This will print the directories that contain foo, and will not recurse in their subdirectories:

find -type d -exec test -f {}/foo \; -print -prune

The behavior for {}/foo is explicitly left undefined by POSIX:

If a utility_name or argument string contains the two characters "{}", but not just the two characters "{}", it is implementation-defined whether find replaces those two characters or uses the string without change.

but works as expected with GNU find (and you tagged the question with ). As Kamil Cuk rightly suggests in the comments, if you're using non-GNU find or if you want a more portable solution, use:

find -type d -exec sh -c 'test -f "$1"/foo' -- {} \; -print -prune
Eggshaped answered 7/2, 2019 at 18:0 Comment(3)
And i think we can even get away from posixly undefined by just invoking the shell and passing path by parameter -exec sh -c 'test -f "$1"/foo' -- {} \;.Impartial
@chepner: that wouldn't work: the -prune predicate must be applied to a directory. Regarding the output, it's quite easy to modify the statement to append /foo to it.Eggshaped
Oops, for some reason in my tests I was making directories instead of regular files as the candidate matches, so -prune was coincidentally working for me.Houlberg
Z
2

it can't be done easily with find -prune because it works on directories and find's basic conditions are over current file.

an alternative could be to do it with bash programmatically, using recursive function, basically

rec_run() {
    local file
    for file in "${1:-.}"/*; do
        # to filter 'file=*' when no match is found
        if [[ ! -e $file ]]; then
            continue
        fi

        # do something with file
        echo "$file"

        # to filter known hard links
        if [[ $file = */. ]] || [[ $file = */.. ]]; then
            continue
        fi

        # if is a directory recursive call
        if [[ -d $file ]]; then
            rec_run "$file";
        fi
    done
}

and changing the part # do something with

    if [[ -f $file/foo ]]; then
        echo "$file/foo"
        continue
    fi

here foo is hardcoded but could be passed as second function argument

Note ${1:-.} is to take the first argument as root directory or . if not passed

Zinfandel answered 8/2, 2019 at 8:29 Comment(2)
yes i left it in case OP wanted to match "hidden files" .* and in this case pattern would be .*Zinfandel
Ok, makes sense!Eggshaped

© 2022 - 2024 — McMap. All rights reserved.