#!/usr/bin/env perl

use strict;
use Getopt::Long;
use Date::Calc qw( Localtime Add_Delta_YMDHMS Mktime );
use Pod::Usage;
use Cwd;

$| = 1;

my $VERSION = '1.0';


### OPTIONAL MODULES

my $module = "Term::ReadKey";
my $use_module_term = 1;
eval "use $module";
if ($@) {
    $use_module_term = 0;
} 


### GLOBAL VARIABLES

my $total_file_size;
my %dir_size;
my %parent_dir;
my %files;
my $cwd;
my ($nb_subdirs, $nb_files) = (0, 0);
my ($min_time, $max_time) = (0, 0);
my $abs_path        = 0;
my $max_lines       = 0;
my $sort_by_time    = 0;
my $sort_by_name    = 0;
my $reverse         = 0;
my $list_dirs       = 0;
my $human           = 0;
my $kilo            = 0;
my $line_size       = 0;
my $mega            = 0;
my $exclude_files   = '';
my $exclude_dirs    = '';
my $include_files   = '';
my $include_dirs    = '';
my $one_file_system = 0;
my $help            = 0;
my $man             = 0;
my $recursive       = 0;
my $simple          = 0;
my $show_version    = 0;
my $within          = '';
my $not_within      = '';
my $time_type       = 'm';


### GETTING OPTIONS

Getopt::Long::Configure(
                        "bundling",
                        "no_auto_abbrev",
                        "require_order",
                        "no_ignore_case"
                       );

GetOptions(           
           'A|absolute-path'        => \$abs_path,
           #'a'
           #'B'
           #'b'
           #'C'
           #'c'
           #'D'
           'd|directory'            => \$list_dirs,
           #'E'
           #'e'
           #'F'
           #'f'
           #'G'
           #'g'
           'H|human'                => \$human,
           'h|help'                 => \$help, 
           'I|include-only-dirs=s'  => \$include_dirs,
           'i|include-only-files=s' => \$include_files,
           #'J'
           #'j'
           'K'                      => \$kilo,
           #'k'
           'L|line-size:i'          => \$line_size,
           'l|lines=i'              => \$max_lines, 
           'M'                      => \$mega,
           'm|man'                  => \$man,
           #'N'
           'n'                      => \$sort_by_name,
           #'O'
           'o|one-file-system'      => \$one_file_system,
           #'P'
           #'p'
           #'Q'
           #'q'
           'R|recursive'            => \$recursive,
           'r|reverse'              => \$reverse,
           #'S'
           's'                      => \$simple,
           'T=s'                    => \$time_type,
           't'                      => \$sort_by_time,
           #'U'
           #'u'
           #'V'
           'v|version'              => \$show_version,
           'W|not-within-last=s'    => \$not_within,      
           'w|within-last=s'        => \$within,
           'X|exclude-dirs=s'       => \$exclude_dirs,
           'x|exclude-files=s'      => \$exclude_files,
           #'Y'
           #'y'
           #'Z'
           #'z'
          ) or pod2usage(-exitstatus => 0, -verbose => 1);

pod2usage(-exitstatus => 0, -verbose => 1) if $help;
pod2usage(-exitstatus => 0, -verbose => 2) if $man;


### IMPLIED OPTIONS

if ( $simple ) {
    $abs_path = 1;
}


### HANDLING OPTIONS

# -v
if ( $show_version ) {
    print "LL.pl v$VERSION\n";
    exit 1;
}

# -w
if ( $within !~ /^ *$/ ) {
    if ( $within =~ /^([1-9][0-9]*)(Y|M|D|h|m|s)$/ ) {
        my $number = $1;
        my $unit   = $2;
        
        $min_time = subtract_delta_units_from_now( $number, $unit );
    }
    elsif ( $within =~ /^([1-9][0-9]{3}[-\/]?[01][0-9][-\/]?[0123][0-9])(@[012][0-9][-:]?[0-5][0-9][-:]?[0-5][0-9])?$/ ) {
        my $date = $1;
        my $time = $2;

        $min_time = convert_date_time_to_seconds( $date, $time );        
    }
    else {
        print "Value '$within' invalid for option w\n";
        pod2usage(-exitstatus => 0, -verbose => 1);
    }
}

# -W
if ( $not_within !~ /^ *$/ ) {
    if ( $not_within =~ /^([1-9][0-9]*)(Y|M|D|h|m|s)$/ ) {
        my $number = $1;
        my $unit   = $2;
        
        $max_time = subtract_delta_units_from_now( $number, $unit );
    }
    elsif ( $not_within =~ /^([1-9][0-9]{3}[-\/]?[01][0-9][-\/]?[0123][0-9])(@[012][0-9][-:]?[0-5][0-9][-:]?[0-5][0-9])?$/ ) {
        my $date = $1;
        my $time = $2;
        
        $max_time = convert_date_time_to_seconds( $date, $time );
    }
    else {
        print "Value '$not_within' invalid for option W\n";
        pod2usage(-exitstatus => 0, -verbose => 1);
    }
}


### GETTING START DIRECTORY

$cwd = $ARGV[0];
if ( $cwd =~ /^ *$/ ) {
    chomp( $cwd = cwd() );
}

# . and .. dirs MUST be changed to full path
chdir $cwd;
chomp( $cwd = cwd() );

my $ori_device_number = ( stat $cwd )[0];

my $start_dir = $cwd;
$start_dir =~ s/\/*$//;


### SCAN FILESYSTEM

Scan($cwd);


### GETTING TERMINAL PROPERTIES

my ($term_width, $term_height, $term_p_width, $term_p_height) = (80, 0, 0, 0);

if ($use_module_term) {
    ($term_width, $term_height, $term_p_width, $term_p_height)
        = GetTerminalSize(*STDOUT);
    $term_width--;
}

if ($line_size > 0) {
    $term_width = $line_size;
}


### DIRECTORY STATISTICS

if ($list_dirs) {
    my $by_type = \&dirBySizeAsc;
    $by_type = \&dirBySizeDsc if $reverse;
    
    if ($sort_by_name) {
        $by_type = \&byNameAsc;
        $by_type = \&byNameDsc if $reverse;
    }

    ### DETERMINE MAX LENGTHS
    
    my ($max_name, $max_size) = (0, 0);
  
    my $i = 1;
    for my $dir (sort $by_type keys %dir_size) {
        my $name_length = length $dir;
        my $size_length = length format_size( $dir_size{$dir} );
        if ($name_length > $max_name) {
            $max_name = $name_length;
        }
        if ($size_length > $max_size) {
            $max_size = $size_length;
        }

        if ( $simple ) {
            print $dir . "\n";
        }

        last if ($max_lines and $i == $max_lines);
        $i++;    
    }

    exit 1 if $simple;

    my $separator = 2;
    my $dir_column_length = 11;
    my $size_column_length = 4;
    my $percent_column_length = 8;
    my ($min_name, $min_size, $space_for_percent )
        = (
           $dir_column_length,
           $size_column_length + $separator,
           $percent_column_length + $separator
          );

    my $space_for_size = $max_size + $separator;
    if ($space_for_size < $min_size) {
        $space_for_size = $min_size;
    }

    my $space_for_name = $term_width - $space_for_size - $space_for_percent;
    if ($line_size < 0) {
        $space_for_name = $max_name;
    }
    if ($space_for_name > $max_name) {
        $space_for_name = $max_name;
    }
    if ($space_for_name < $min_name) {
        $space_for_name = $min_name;
    }

    ### DISPLAY HEADER
    
    print (
           "\nDIRECTORIES"
           . ' ' x ($space_for_name - $min_name)
           . "  SIZE"
           . ' ' x ($space_for_size - $min_size)
           . "  PERCENT\n"
          );
    print "-" x ($space_for_name + $space_for_size + $space_for_percent) . "\n";
    
    if ( !$abs_path ) {
        print " $start_dir -->\n";
        print "-" x ($space_for_name + $space_for_size + $space_for_percent)
            . "\n";
    }

    
    ### DISPLAY DIRECTORIES
    
    $i = 1;
    for my $dir (sort $by_type keys %dir_size) {
        my $percent = ( $dir_size{$dir} / $total_file_size ) * 100;
	
        printf (
                "%-${space_for_name}.${space_for_name}s%"
                . "${space_for_size}s%8.2f %\n",
                $dir,
                format_size( $dir_size{$dir} ),
                $percent,
               );
	
        last if ($max_lines and $i == $max_lines);
        $i++;
    }
    
    ### DISPLAY FOOTER
    
    if ( !$abs_path ) {
        print "-" x ($space_for_name + $space_for_size + $space_for_percent)
            . "\n";
        print " --> $start_dir\n";
    }
    print "-" x ($space_for_name + $space_for_size + $space_for_percent) . "\n";
    print (
           "DIRECTORIES"
           . ' ' x ($space_for_name - $min_name)
           . "  SIZE"
           . ' ' x ($space_for_size - $min_size)
           . "  PERCENT\n\n"
          );
    print "NUMBER OF SUBDIRECTORIES: " . $nb_subdirs . "\n\n";
}

### FILE STATISTICS

else {
    my $by_type = \&bySizeAsc;
    $by_type = \&bySizeDsc if $reverse;

    if ($sort_by_time) {
        $by_type = \&byTimeAsc;
        $by_type = \&byTimeDsc if $reverse;
    }
  
    if ($sort_by_name) {
        $by_type = \&byNameAsc;
        $by_type = \&byNameDsc if $reverse;
    }

    ### DETERMINE MAX LENGTHS
    
    my ($max_name, $max_size, $max_date)
        = (0, length format_size($total_file_size), 0);

    my $i = 1;
    for my $file (sort $by_type keys %files) {
        my $name_length = length $file;
        my $size_length = length format_size( $files{$file}{size} );
        my $date_length = length $files{$file}{formatted_date};
        if ($name_length > $max_name) {
            $max_name = $name_length;
        }
        if ($size_length > $max_size) {
            $max_size = $size_length;
        }
        if ($date_length > $max_date) {
            $max_date = $date_length;
        }

        if ($simple) {
            print $file . "\n";
        }
    
        last if ($max_lines and $i == $max_lines);
        $i++;
    }

    exit 1 if $simple;

    my $separator = 2;
    my $name_column_length = 5;
    my $size_column_length = 4;
    my $date_column_length = 4;
    my ($min_name, $min_size, $min_date)
        = (
           $name_column_length,
           $size_column_length + $separator,
           $date_column_length + $separator
          );
    
    my $space_for_date = $max_date + $separator;
    if ($space_for_date < $min_date) {
        $space_for_date = $min_date;
    }

    my $space_for_size = $max_size + $separator;
    if ($space_for_size < $min_size) {
        $space_for_size = $min_size;
    }

    my $space_for_name = $term_width - $space_for_size - $space_for_date;  
    if ($line_size < 0) {
        $space_for_name = $max_name;
    }
    if ($space_for_name > $max_name) {
        $space_for_name = $max_name;
    }
    if ($space_for_name < $min_name) {
        $space_for_name = $min_name;
    }

    ### DISPLAY HEADER

    print (
           "\nFILES"
           . ' ' x ($space_for_name - $min_name)
           . "  SIZE" . ' ' x ($space_for_size - $min_size)
           . "  DATE\n"
          );
    print "-" x ($space_for_name + $space_for_size + $space_for_date) . "\n";
    if ( !$abs_path ) {
        print " $start_dir -->\n";
        print "-" x ($space_for_name + $space_for_size + $space_for_date) . "\n";
    }

    ### DISPLAY FILES

    my $i = 1;
    for my $file (sort $by_type keys %files) {
        my $size = format_size( $files{$file}{size} );

        printf (
                "%-${space_for_name}.${space_for_name}s%"
                . "${space_for_size}s%"
                . "${space_for_date}.${space_for_date}s\n",
                $file,
                $size,
                $files{$file}{formatted_date},
               );
        
        last if ($max_lines and $i == $max_lines);
        $i++;
    }

    ### DISPLAY FOOTER

    if ( !$abs_path ) {
        print "-" x ($space_for_name + $space_for_size + $space_for_date)
            . "\n";
        print " --> $start_dir\n";
    }
    print "-" x ($space_for_name + $space_for_size + $space_for_date) . "\n";
    print (
           "FILES"
           . ' ' x ($space_for_name - $min_name)
           . "  SIZE"
           . ' ' x ($space_for_size - $min_size)
           . "  DATE\n"
          );
    print "-" x ($space_for_name + $space_for_size + $space_for_date) . "\n";

    my $total_size_string = "TOTAL SIZE:";
    my $space_for_total_size = (
                                $space_for_name + $space_for_size
                                - length $total_size_string
                               );

    printf (
            "%s%${space_for_total_size}.${space_for_total_size}s\n\n",
            $total_size_string,
            format_size($total_file_size),
           );
    print "NUMBER OF FILES: " . $nb_files . "\n\n";
}

exit 1;

sub byNameAsc {
    $a cmp $b
}

sub byNameDsc {
    $b cmp $a
}

sub dirBySizeAsc {
    $dir_size{$a} <=> $dir_size{$b}
}

sub dirBySizeDsc {
    $dir_size{$b} <=> $dir_size{$a}
}

sub bySizeAsc {
    $files{$a}{size} <=> $files{$b}{size}
}

sub bySizeDsc {
    $files{$b}{size} <=> $files{$a}{size}
}

sub byTimeAsc {
    $files{$a}{date} <=> $files{$b}{date}
}

sub byTimeDsc {
    $files{$b}{date} <=> $files{$a}{date}
}

sub format_date {
    my $time = shift;

    #my $formatted_date = scalar localtime $time;

    my ($year, $month, $day, $hour, $min, $sec, $doy, $dow, $dst)
        = Localtime($time);

    $month = sprintf("%02d", $month);
    $day   = sprintf("%02d", $day  );
    $hour  = sprintf("%02d", $hour );
    $min   = sprintf("%02d", $min  );
    $sec   = sprintf("%02d", $sec  );
    
    my $formatted_date = "$year-$month-$day $hour:$min:$sec";

    return $formatted_date;
}

sub format_size {
    my $number = shift;

    if ($kilo) {
        $number = decimal( $number / 1024 );
        $number .= " KB";	
    }
    elsif ($mega) {
        $number = decimal( $number / (1024 * 1024) );
        $number .= " MB";
    }
    elsif ($human) {
        my @units = (' B', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB');
        my $formatted = 0;
        foreach my $i ( 0 .. $#units - 1 ) {
            if ($number > 1024) {
                $number /= 1024;
            }
            else {
                if ($i == 0) {
                    $number .= $units[$i];
                }
                else {
                    $number = decimal($number) . $units[$i];
                }
                $formatted = 1;
                last;
            }
        }
        if (!$formatted) {
            # if $number is 1 or more times the biggest unit
            # then add the biggest unit
            $number = decimal($number) . $units[$#units];
        }
    }
    else {
        $number = comma_separated($number);
    }
    return $number;
}

sub comma_separated {
    my $number = shift;
    1 while ( $number =~ s/(.*)(\d)(\d\d\d)/$1$2,$3/ );
    return $number;
}

sub decimal {
    my $number = shift;
    my $places = shift;
    if ($places =~ /^ *$/) {
        $places = 2; # Default decimal places
    }
    my ($whole, $decimal) = split /\./, $number;
    $decimal = (
                substr($decimal, 0, $places)
                . '0' x ( $places - length $decimal )
               );
    $number = comma_separated($whole) . '.' . $decimal;
    return $number;
}

sub Add_size_to_dir {
    my $dir = shift;
    my $size = shift;

    $dir_size{$dir} += $size;
    if ($parent_dir{$dir}) {
        Add_size_to_dir ($parent_dir{$dir}, $size);
    }
}

sub Scan {
    my $dir = shift;

    # exclude dirs
    return if ($exclude_dirs and $dir =~ /$exclude_dirs/);
    
    # include only dirs
    return if (
               $include_dirs
               and $dir !~ /$include_dirs/
               and (
                    $dir ne $start_dir
                    and $dir ne "$start_dir/" 
                   )
              );

    $dir =~ s/\/*$//;
  
    my $cdir = $dir;
    if ( $cdir eq '' ) {
        $cdir = '/';
    }
  
    if ( !$abs_path ) {
        $dir =~ s/^\Q$start_dir\E/ \<\-\- /;
    }

    chdir $cdir;

    if ( !opendir(DIR, $cdir) ) {
        print STDERR "Unable to open the directory '$cdir' : $!\n";
        return;
    }
  
    if ( $cdir eq '/' ) {
        $cdir = '';
    }
  
    my %dir;
    my $node;
    while ( defined( $node = readdir DIR ) ) {
        if (-d $node) {
            if ( !-l $node ) {
                # if the node is a directory and not a symbolic link
                $dir{$node} = 1;
            }
        }
        elsif (-f $node) { # if the node is an ordinary file
            
            # exclude files
            next if ($exclude_files and $node =~ /$exclude_files/);
            
            # include only files
            next if ($include_files and $node !~ /$include_files/);
            
            my $dir_node = $dir . "/" . $node;
            
            # time
            my ( $atime, $mtime, $ctime ) = ( stat $node )[8, 9, 10];
            my $time = (
                        $time_type =~ /^a/i ? $atime
                        : $time_type =~ /^c/i ? $ctime
                        : $mtime
                       );

            next if ( $min_time and $time < $min_time and !$max_time );
            next if ( $max_time and $time > $max_time and !$min_time );
            next if (
                     $min_time and $max_time
                     and (
                          (
                           $min_time > $max_time
                           and (
                                $time < $min_time
                                and $time > $max_time
                               )
                          )
                          or (
                              $min_time <= $max_time
                              and (
                                   $time < $min_time
                                   or $time > $max_time
                                  )
                             )
                         )
                    );
            
            $files{$dir_node}{date}           = $time;
            $files{$dir_node}{formatted_date} = format_date($time);

            # size
            my $file_size = ( stat $node )[7];
            $files{$dir_node}{size} = $file_size;
            $total_file_size += $file_size;
            Add_size_to_dir ($dir, $file_size);

            $nb_files++;
        }
    }
    close DIR;

    my $dir_own_size = ( stat $cdir )[7];
  
    Add_size_to_dir($dir, $dir_own_size);
    if ( $list_dirs ) {
        $total_file_size += $dir_own_size;
    }
    
    # Scan recursively all the subdirectories
    if ($recursive) {
        for my $subdir (keys %dir) {
            next if ( ($subdir eq ".") or ($subdir eq "..") );
            my $path  = $dir . "/" . $subdir;
            my $cpath = $cdir . "/" . $subdir;
            my $current_device_number = ( stat $cpath )[0];
            if ($one_file_system) {
                if ($current_device_number == $ori_device_number) {
                    $parent_dir{$path} = $dir;
                    $nb_subdirs++;
                    Scan($cpath);	  
                }
            }
            else {
                $parent_dir{$path} = $dir;
                $nb_subdirs++;
                Scan($cpath);	
            }
        }
    }
}

sub convert_date_time_to_seconds {
    my $date = shift;
    my $time = shift;
    
    $date =~ s/[-\/]//g;
    my @new_time = unpack 'a4 a2 a2', $date;
    if ( $time =~ /^ *$/ ) {
        push @new_time, 0, 0, 0;
    }
    else {
        $time =~ s/^@//;
        $time =~ s/[-:]//g;
        push @new_time, ( unpack 'a2 a2 a2', $time );
    }

    my $new_time = Mktime( @new_time );
    return $new_time;
}

sub subtract_delta_units_from_now {
    my $number = shift;
    my $unit   = shift;

    my %delta_position_for = (
                              Y => 0,
                              M => 1,
                              D => 2,
                              h => 3,
                              m => 4,
                              s => 5,
                             );

    my @now   = ( Localtime() )[0..5];
    my @delta = ( 0, 0, 0, 0, 0, 0 );
    $delta[ $delta_position_for{$unit} ] = -$number;
    @delta;
    my @new_time = Add_Delta_YMDHMS( @now, @delta );                       
    my $new_time = Mktime( @new_time );

    return $new_time;
}


=head1 VERSION

1.0

=head1 NAME

LL.pl

=head1 SYNOPSIS

LL.pl [I<OPTION>]... [I<directory>]

=head1 DESCRIPTION

By default, LL.pl will print size and modification time of each file in the I<directory> or in the current working directory if I<directory> is not specified. The default sort order is ascending by size.

Mandatory arguments to long options are mandatory for short options too.

=head1 OPTIONS

=over 8

=item B<-A, --absolute-path>

Prints a full absolute path for files/dirs.

=item B<-d, --directory>

Prints only information for directories.

=item B<-h, --help>

Prints a brief help message and exits.

=item B<-H, --human>

Prints the size in human readable format (B, KB, MB, GB, TB, PB, EB).

=item B<-I, --include-only-dirs=>I<reg-exp>

Includes only the directories that match the specified regular expression. The only restriction on the expression is that it cannot contain whitespace and should be enclosed in quotes if parentheses used.

=item B<-i, --include-only-files=>I<reg-exp>

Includes only the files that match the specified regular expression. The only restriction on the expression is that it cannot contain whitespace and should be enclosed in quotes if parentheses used.

=item B<-K>

Prints the size in KB (1024 B).

=item B<-L, --line-size=>I<N>

Sets the maximal width of a line to I<N> characters. Default is 80 characters. If the module I<Term::ReadKey> is installed, the maximal width is automatically set to terminal width. Zero (0) characters is an alias for default values. All negative numbers (ex. -1) set no limit to the maximal width of a line. However, if the width of the longest line is smaller than the maximal width specified, the latter is adjusted not to display too much whitespace.

The minimal size for a line is 30 characters if B<-d> option is used or 35 otherwise. Smaller values are ignored.

=item B<-l, --lines=>I<N>

Prints only the first I<N> files/dirs. Default is all.

=item B<-M>

Prints the size in MB (1024 KB).

=item B<-m, --man>

Prints the manual page and exits.

=item B<-n>

Sorts the output by name. Default is by size.

=item B<-o, --one-file-system>

Scans only one file system.

=item B<-R, --recursive>

Scans all sub-directories recursively.

=item B<-r, --reverse>

Sorts the output in reverse order (descending).

=item B<-s, --simple>

Prints only a full path and name of files/dirs without date or size. Useful for pipes. Implies B<-A> option.

=item B<-T> I<type>

Specifies the type of datetime displayed and/or used for sorting. The following types are possible (case insensitive).

B<a> --> use file I<access> time

B<c> --> use file I<creation> time

B<?> --> use file I<modification> time

=over 6

( ? stands for any character excluding 'c' and 'a' )

=back

=item B<-t>

Sorts the output by datetime. Default sort order is by size. Datetime type can be modified by B<-T> option. See above.

=item B<-W, --not-within-last=>I<Nunit>

Prints only files/dirs not created/modified/accessed within last I<N> time I<unit>s. The negation of B<-w> option. See below for details.

=item B<-w, --within-last=>I<Nunit>

Prints only files/dirs created/modified/accessed within last I<N> time I<unit>s ( ex. 1D (one day), 40m (forty minutes) ) or since .

B<Possible time units are :>

=over 4

Y years

M months

W weeks 

D days 

h hours

m minutes

s seconds

=back

B<Examples :>

=over 4

-w 1D (files/dirs modified within the last one day)

-w 40m (files/dirs modified within the last forty minutes

-w 2008-01-01 (files/dirs modified since January 1st 2008)

-w 2008-06-22@12-55-43 (files/dirs modified since June 22nd 2008 12:55:43)

-w 2008/06/22@12:55:43 (files/dirs modified since June 22nd 2008 12:55:43)

=back

=item B<-X, --exclude-dirs=>I<reg-exp>

Excludes the directories that match the specified regular expression. The only restriction on the expression is that it cannot contain whitespace and should be enclosed in quotes if parentheses used.

=item B<-x, --exclude-files=>I<reg-exp>

Excludes the files that match the specified regular expression. The only restriction on the expression is that it cannot contain whitespace and should be enclosed in quotes if parentheses used.

=back

=head1 AUTHOR

Written by Adrian Priscak.

=head1 REPORTING BUGS

Suggestions and bug reports should be addressed to <apriscak@gmail.com>.

=head1 COPYRIGHT

Copyright © 2006-2008 Adrian Priscak
This is free software. You may redistribute copies of it under the terms of the GNU General Public License <http://www.gnu.org/licenses/gpl.html>. There is NO WARRANTY, to the extent permitted by law.

=head1 README

Prints advanced statistics on disk space usage. Useful for users/administrators not satisfied with traditional UNIX utilities such as B<du> or B<ls>. More info is available by executing B<LL.pl -h> or B<LL.pl -m>, displaying a help and a manual page respectively.

=head1 PREREQUISITES

This script requires the following modules :

=over 8

C<strict>

C<Getopt::Long>

C<Date::Calc>

C<Pod::Usage>

C<Cwd>

=back

=head1 COREQUISITES

Term::ReadKey

=head1 OSNAMES

any

=head1 SCRIPT CATEGORIES

Unix/System_administration
CPAN/Administrative
CPAN

=cut

