#!/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