(Originally posted on Stack Overflow)

Following my findings and suggestions in my other post How to exclude a list of full directory paths in find command on Solaris, I have decided to write a Perl version of this script and see how I could optimize it to run faster than a native find command. So far, the results are impressive!

The purpose of this script is to report all unowned files and directories on a Unix system for audit compliance. The script has to accept a list of directories and files to exclude (either by full path or wildcard name), and must take as little processing power as possible. It is meant to be run on hundreds of Unix systems that we (the company I work for) support, and has be able to run on all those Unix systems (multiple OS, multiple platforms: AIX, HP-UX, Solaris and Linux) without us having to install or upgrade anything first. In other words, it has to run with standard libraries and binaries we can expect on all systems.

I have not yet made the script argument-aware, so all arguments are hard-coded in the script. I plan on having the following arguments in the end and will probably use getopts to do it:

-d = comma delimited list of directories to exclude by path name
-w = comma delimited list of directories to exclude by basename or wildcard
-f = comma delimited list of files to exclude by path name
-i = comma delimited list of files to exclude by basename or wildcard
-t:list|count = Defines the type of output I want to see (list of all findinds, or summary with count per directory)

Here is the source I have done so far:

#! /usr/bin/perl
use strict;
use File::Find;

# Full paths of directories to prune
my @exclude_dirs = ('/dev','/proc','/home');

# Basenames or wildcard names of directories I want to prune
my $exclude_dirs_wildcard = '.svn';

# Full paths of files I want to ignore
my @exclude_files = ('/tmp/test/dir3/.svn/svn_file1.txt','/tmp/test/dir3/.svn/svn_file2.txt');

# Basenames of wildcard names of files I want to ignore
my $exclude_files_wildcard = '*.tmp';
my %dir_globs = ();
my %file_globs = ();

# Results will be sroted in this hash
my %found = ();

# Used for storing uid's and gid's present on system
my %uids = ();
my %gids = ();

# Callback function for find
sub wanted {
    my $dir = $File::Find::dir;
    my $name = $File::Find::name;
    my $basename = $_;

    # Ignore symbolic links
    return if -l $name;
    # Search for wildcards if dir was never searched before
    if (!exists($dir_globs{$dir})) {
        @{$dir_globs{$dir}} = glob($exclude_dirs_wildcard);
    if (!exists($file_globs{$dir})) {
        @{$file_globs{$dir}} = glob($exclude_files_wildcard);
    # Prune directory if present in exclude list
    if (-d $name && in_array(\@exclude_dirs, $name)) {
        $File::Find::prune = 1;
    # Prune directory if present in dir_globs
    if (-d $name && in_array(\@{$dir_globs{$dir}},$basename)) {
        $File::Find::prune = 1;
    # Ignore excluded files
    return if (-f $name && in_array(\@exclude_files, $name));
    return if (-f $name && in_array(\@{$file_globs{$dir}},$basename));
    # Check ownership and add to the hash if unowned (uid or gid does not exist on system)
    my ($dev,$ino,$mode,$nlink,$uid,$gid) = stat($name);
    if (!exists $uids{$uid} || !exists($gids{$gid})) {
        push(@{$found{$dir}}, $basename);
    } else {

# Standard in_array perl implementation
sub in_array {
    my ($arr, $search_for) = @_;
    my %items = map {$_ => 1} @$arr;
    return (exists($items{$search_for}))?1:0;

# Get all uid's that exists on system and store in %uids
sub get_uids {
    while (my ($name, $pw, $uid) = getpwent) {
        $uids{$uid} = 1;

# Get all gid's that exists on system and store in %gids
sub get_gids {
    while (my ($name, $pw, $gid) = getgrent) {
        $gids{$gid} = 1;

# Print a list of unowned files in the format PARENT_DIR,BASENAME
sub print_list {
    foreach my $dir (sort keys %found) {
        foreach my $child (sort @{$found{$dir}}) {
            print "$dir,$child\n";

# Prints a list of directories with the count of unowned childs in the format DIR,COUNT
sub print_count {
    foreach my $dir (sort keys %found) {
        print "$dir,".scalar(@{$found{$dir}})."\n";

# Call it all

find(\&wanted, '/');
print "List:\n";

print "\nCount:\n";


If you want to test it on your system, simply create a test directory structure with generic files, chown the whole tree with a test user you create for this purpose, and then delete the user.

I'll take any hints, tips or recommendations you could give me.


2 Answers 2


Overall it looks clean and simple. Good job. Here are some things I noticed:

  1. You should use use warnings for production level code. It's odd that you use strict but not warnings.
  2. List::Util or List::MoreUtils should have a suitable replacement for your in_array() function if you're interested or have it installed on all your systems.
  3. If you're using a Perl version >= 5.10, you can replace in_array with the smart match operator ~~.
  4. Don't let your exclude lists get too big as storing them in an array an iterating through them is an O(n) operation. A hash lookup may be faster for large lists and would de-dup for you automatically.
  5. Using & before calling a sub is pretty much deprecated. Just call it directly.
  6. The shebang line /usr/bin/env perl is more portable than /usr/bin/perl
  7. You stat your file $name over and over for all your various tests. stat() it once, save the results, then re-use those results for all your tests. Remember that all tests like -d, -l and -f are stat() calls internally. Read up on the Perl stat call and all the fields to help determine how to re-create the -d, -l, and -f checks against the returned data.
  • \$\begingroup\$ Many years after this answer was posted, smart match (~~) is now scheduled for removal from the Perl language. \$\endgroup\$
    – toolic
    Commented Mar 19 at 21:14

In addition to the suggestions of the other answer, here are some other adjustments for you to consider.

The "quote words" operator (qw) can be used to simplify code and make it easier to read:

my @exclude_dirs = qw(/dev /proc /home);

There is no need to initialize hashes and arrays to an empty list:

my %found = ();

This is simpler:

my %found;

Although not required, it is a good idea to add a semicolon after this return:

} else {

It is best to import only what is needed to avoid namespace pollution. Change:

use File::Find;


use File::Find qw(find);

I use this script to identify imported functions: which module exports are used?

foreach is identical to for. I recommend for: less to type, less to read.

I recommend moving the subs to the bottom, after the exit statement. Having them in the middle of the code interrupts the natural flow of the code (from a human readability standpoint).

You should add a summary of the purpose of the code at the top of the file as plain old documentation (POD), something as simple as:


Finding all unowned files and directories on Unix


"sroted" should be "sorted".

"childs" should be "children".


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.