1

Im trying to use the find command to recursively search a directory tree and return all results with a directory matching a given name.

find . -type d -name '<NAME>'

I get a list of paths returned which are printed into a log file. I want to be able to omit any results which contain a file called omit.txt within a parent directory.

./parent_directory/ NAME>

I have been trying to do it with the following

find . -type d -name '<NAME>' \( ! -wholename "*omit.txt*" -prune \)

I am trying to use the prune command to omit results containing the file, but I think this may only work with directories. Any help would be greatly appreciated. I am trying to do this in just one command and maintain the output file as much as possible -

2
  • So, if we have ./a/x/omit.txt and ./b/x/foo.txt, you'd want a listing that only contains ./b/x/? Can you add a mock-up sample directory tree and the expected output on the question?
    – ilkkachu
    Commented Jan 24, 2018 at 10:12
  • So in a directory tree where the is ./a ./b ./c and each of those directories contains a directory called d, giving ./a/d ./b/d ./c/d. if i was to place a file called omit.txt into directory c, /i want to try and use the find unix command to return any case where there is directory called 'd' in the directory tree AND the file 'omit.txt' is not present within the parent directory. So you would expect it to return './a/d ./b/d' . , with my attempt above it returns './a/d ./b/d ./c/d' Commented Jan 24, 2018 at 10:25

3 Answers 3

2

I don't think you can do this completely within find, since you need to be able to see the file inside the directory when processing the directory. -prune has to operate on the directory itself, but -path and other conditions only see the file name after find has already descended to the directory.

All I can come up with is to fork off a shell to peek inside the directory. Something like this:

$ mkdir -p x y/z ; touch x/foo y/omit.txt
$ find -type d \( -exec sh -c '[ -e "$1/omit.txt" ]' sh {} \; -prune -o -print \)
.
./x

foo -o bar does not evaluate bar if foo succeeds, so if the directory is pruned, the print doesn't run. Add -o -print to the end to print the directory contents too. We can't use exec ... {} + here, since we need this to act as a condition for each directory separately.

2
  • Now that I see this I remember seeing a similar answer/question here some months back that took the same approach. Don't have time to find it now, though. ;)
    – B Layer
    Commented Jan 24, 2018 at 10:31
  • Thanks, you gave me a lot to work with here, I think I will read up on some of the arguments you used here. Thanks Commented Jan 24, 2018 at 10:45
1

If busybox is available on your system, you can do:

busybox find . -type d '(' -exec [ -e '{}/omit.txt' ] ';' -prune \
  -o -name '<NAME>' -print ')'

The find of busybox, like that of GNU does expand {} to the path of the file even if it's only part of an argument avoiding having to resort to sh, but more importantly, in recent versions, it is able to run the [ applet without a fork() nor execve() so it makes it several orders of magnitude more efficient than with most other find implementations.

If a directory called <NAME> may itself contain a omit.txt and you want to print it in that case even if still pruned, you can change it to:

busybox find . -type d '(' -exec [ ! -e '{}/omit.txt' ] ';' -o -prune ')' \
  -name '<NAME>'

In the bosh (not bash) shell, find (like [) is built-in and is able to run some shell code without forking with its -call predicate which would also save fork()s and execve()s:

find . -type d '(' -call '[ -e "$1/omit.txt" ]' {} ';' -prune \
  -o -name '<NAME>' -print ')'

A more portable alternative is to use find's File::Find module.

perl -MFile::Find -le '
  find sub {
    if (-e "$_/omit.txt") {
      $File::Find::prune = 1;
    } else {
      print $File::Find::name if ($_ eq "<NAME>") && -d;
    }
  }, @ARGV' -- .
0

@ilkkachu gave an awesome answer to a question I've had for a while! Definitely give them your upvotes. I just wanted to add a couple of variations I used that might benefit someone else :)

sample outputs are based on the following structure

./
├── a/
│   ├── b/
│   │   └── world.txt
│   └── hello.txt
├── foo.txt
└── some_git_repo/
    ├── .git/
    ├── README
    └── c/
        └── some_git_submodule/
            ├── .git/
            └── README

Print all directories EXCEPT git repos

find . -type d \( -exec sh -c 'test -e "$1/.git"' sh {} \; -prune -o -print \)
.
./a
./a/b

1. Print all directories, INCLUDING git repos

2. DO NOT recurse into any git repos

find . -type d \( -exec sh -c 'test -e "$1/.git"' sh {} \; -print -prune -o -print \)
.
./a
./a/b
./some_git_repo

Print all files, EXCEPT those in git repos

find . \( -exec sh -c 'test -e "$1/.git"' sh {} \; -prune -o -type f -print \)
./foo.txt
./a/b/world.txt
./a/hello.txt

1. Print all files, EXCEPT those in git repos

2. Print the excluded git dirs

find . \( -exec sh -c 'test -e "$1/.git"' sh {} \; -print -prune -o -type f -print \)
./foo.txt
./a/b/world.txt
./a/hello.txt
./some_git_repo

And then for the opposite...

Print all git repos

find . -type d \( -exec sh -c 'test -e "$1/.git"' sh {} \; -print -prune \)
./some_git_repo

Print all git repos including submodules

find . -type d \( -exec sh -c 'test -e "$1/.git"' sh {} \; -print \) -o \( -name '.git' -prune \)
./some_git_repo
./some_git_repo/c/some_git_submodule

You must log in to answer this question.

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