3

How to use find to get all folders that have not a .git folder?

On this structure::

$ tree -a -d -L 2
.
├── a
│   └── .git
├── b
│   ├── b1
│   └── b2
├── c
└── d
    └── .git
        ├── lkdj
        └── qsdqdf

This::

$ find . -name ".git"  -prune -o -type d -print
.
./a
./b
./b/b1
./b/b2
./c
./d
$

get all folders except .git

I would like to get this::

$ find . ...
.
./b
./b/b1
./b/b2
./c
$
user3313834
  • 7,327
  • 12
  • 56
  • 99
  • I think that there is no way to do that "purely", but the answer below is exact. – linuxfan says Reinstate Monica Jul 30 '17 at 17:02
  • As an aside -- note that newline-delimited lists of files aren't necessarily safe: If someone ran `mkdir -p ./$'\n'/etc/passwd`, depending on your exact version of `find` and its implementation of `-print`, you could get `/etc/passwd` in your output list. This kind of ambiguity is much of the motivation for `find -print0`, though the POSIX-compliant `find -exec` can also be used to get exact names (literal newlines, control sequences and all) out of `find`. – Charles Duffy Jul 30 '17 at 18:47

3 Answers3

4

It's inefficient (runs a bunch of subprocesses), but the following will do the job with GNU or modern BSD find:

find . -type d -exec test -d '{}/.git' ';' -prune -o -type d -print

If you're not guaranteed to have a find with any functionality not guaranteed in the POSIX standard, then you might need to take even more of an efficiency loss (to make {} its own token, rather than a substring, by having a shell run the test):

find . -type d -exec sh -c 'test -d "$1/.git"' _ '{}' ';' -prune -o -type d -print

This works by using -exec as a predicate, running a test that find doesn't have support for built-in.

Note the use of the inefficient -exec [...] {} [...] \; rather than the more efficient -exec [...] {} +; as the latter passes multiple filenames to each invocation, it has no way to get back individual per-filename results and so always evaluates as true.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
1

If you don't mind using a temporary file, then:

find . -type d -print > all_dirs
fgrep -vxf <(grep '/\.git$' all_dirs | sed 's#/\.git$##') all_dirs | grep -vE '/\.git$|/\.git/'
rm all_dirs
  • The first step gets all subdirectory paths into all_dirs file
  • The second steps filters out the directories that have a .git subdirectory as well as the .git subdirectories. The -x option is necessary because we need to eliminate only the lines that match in entirety.

This will be a little more efficient compared to Charles' answer in that it doesn't run so many subprocesses. However, it would give a wrong output if any of the directories have a newline character in them.

codeforester
  • 39,467
  • 16
  • 112
  • 140
  • 1
    Whether it's more efficient depends on how deep your trees with `.git` siblings are (and how many directories you have without them); lacking `-prune` means you're telling `find` to recurse into them. – Charles Duffy Jul 31 '17 at 14:00
0

In case you want to find only the top directories add the option -maxdepth 1 like

$ find . -type d -exec test -d '{}/.git' ';' -maxdepth 1 -prune -o -type d -print
.
./b
./c
$
asmaier
  • 11,132
  • 11
  • 76
  • 103