$ cat find-empty-git.pl
#!/usr/bin/perl
use File::Find;
use List::Util qw(uniq);
push @ARGV, './' unless @ARGV;
@ARGV = uniq(@ARGV);
foreach (@ARGV) { die "$_ is not a directory" unless -d $_ };
find(\&wanted, @ARGV);
sub wanted {
$File::Find::prune = 1 if $File::Find::name =~ m=/(Trash|Temp|opt)($|/)=;
return unless (-d && /^\.git$/);
opendir(my $dh, '.') ||
warn "Can't open $File::Find::dir: $!" &&
return;
return if (grep { ! /^(\.{1,2}|\.git)$/ } readdir($dh));
closedir($dh);
print "$File::Find::dir\n";
}
This perl script uses the directory names specified on the command-line as the top-level directory (or directories) to search.
It defaults to ./
if no directory is specified. It checks that each argument is actually a directory, and it uses the the uniq()
function from the List::Util module to eliminate duplicate directory names. The File::Find module is used to recursively search the specified directories. Both modules are core perl modules and are included with perl (i.e. they don't need to be installed separately).
For each filename found, the wanted
subroutine is executed.
First it checks if the full pathname for the current filename ends in /Trash
, /Temp
, or /opt
or is a descendant of one of those. If it does, it prunes that directory from the search tree.
Next, if the filename isn't a directory and isn't .git
, the subroutine returns immediately.
Otherwise, the directory containing the file is opened (with a variable called $dh
as the directory handle. See perldoc -f opendir
) and the directory's contents are examined. If the directory can't be opened for read for any reason (e.g. permissions), it is treated as a non-fatal error (a warning message is printed to stderr and the subroutine returns).
The grep
used in the wanted
subroutine is perl's built-in grep function. It is NOT the grep external command. perl's grep
function takes a list (array) as input and returns another list where the block of code evaluates as true. In list context, the readdir
function returns a list of filenames in a directory. See perldoc -f grep
and perldoc -f readdir
.
In short: The return if grep... readdir($dh)
line returns early (i.e. before printing the directory name) from the wanted function if there are any "files" in the directory that don't match either .
, ..
, or .git
. The word "files" is used here in the generic sense and includes regular files, symbolic links, directories, device nodes, named pipes, sockets, etc.
Finally, having made it this far, the directory name is printed.
BTW, the print "$File::Find::dir\n";
line could be changed to print "$File::Find::dir\0";
if you needed a NUL-separated list of directory names instead of newline-separated.
Sample run. First create a testing environment by making some directories (a, b, and c) with .git sub-directories in them. Create a file in one of those directories. Make another directory (d) that doesn't have a .git subdir but does have a subdir (e) which does. And some .git subdirs under ./Trash/ and ./Temp/
$ mkdir -p {a,b,c}/.git/
$ touch a/file1
$ mkdir -p d/e/.git
$ mkdir -p Trash/f/.git Temp/g/.git
$ tree --metafirst --noreport -paf a b c d Trash Temp
[drwxr-xr-x] a
[-rw-r--r--] ├── a/file1
[drwxr-xr-x] └── a/.git
[drwxr-xr-x] b
[drwxr-xr-x] └── b/.git
[drwxr-xr-x] c
[drwxr-xr-x] └── c/.git
[drwxr-xr-x] d
[drwxr-xr-x] └── d/e
[drwxr-xr-x] └── d/e/.git
[drwxr-xr-x] Trash
[drwxr-xr-x] └── Trash/f
[drwxr-xr-x] └── Trash/f/.git
[drwxr-xr-x] Temp
[drwxr-xr-x] └── Temp/g
[drwxr-xr-x] └── Temp/g/.git
Now make the script executable and run it. It prints the names of the directories that:
- aren't children of Trash, Temp, or opt directories,
- contain a .git subdir, and
- don't contain any other files
i.e. ./b
and ./c
and ./d/e
.
$ chmod +x ./find-empty-git.pl
$ ./find-empty-git.pl ./
./b
./d/e
./c
BTW, depending on how comfortable you are reading and working with fairly linear code, this may or may not be easier to read and understand than a moderately long and complicated find
command line. It's certainly easier for me (but that's probably because I've written dozens of little File::Find
-based scripts like this over the last few decades).
It's hard to say whether this would be faster than running find
or not. Probably. Maybe. Depends on how many directories you have that contain .git subdirectories. With this script, perl
only has to be run once and it doesn't execute any external programs. Stephen's find
command has to execute both sh
and ls
(and maybe pwd
) once for each .git directory it finds, which can add up to a significant overhead if there are lots of .git directories.