xdf
#!/usr/bin/perl -w
use strict;

# ==============================================================================
# xdf - eXtended Disk Free
# ==============================================================================
# Collect df information and format identically on all supported platforms,
# particularly adding -h support ("human-readable output") to platforms
# that do not have this in their native df command. Also, xdf only reports on
# local filesystems, except temp filesystems are excluded by default. Sorts
# by mountpoint, but other sort options are available.
# -----------------------------------------------------------------------------
# Author: Jeremy Holland
# -----------------------------------------------------------------------------
# Usage: xdf [--csv] [--swap] [--sort=mode]
#   --csv: report output in minimal CSV format, for use with other scripts
#   --sort: sort output by size, used, free, device, full, or mountpoint
#   --swap: include swap partitions and temp filesystems (normally excluded)
# -----------------------------------------------------------------------------
# Rev History
#   2012-02-02 - J. Holland - Initial release
#   2012-02-07 - J. Holland - Added ZFS reporting for Solaris 10+
#   2012-06-09 - J. Holland - Added CSV mode for use with Big Brother monitor
#   2012-09-12 - J. Holland - Added sort modes
#
# Known Bugs/Feature Requests
#   - Add support for a directory argument (as with df) that only reports on
#     the filesystem containing that directory
# -----------------------------------------------------------------------------

use Getopt::Long;
use Switch;

# ------------------------------------------------------------------------------
# PROCESS COMMAND LINE AND CHECK ARGUMENTS
# ------------------------------------------------------------------------------

sub USAGE { 
  "USAGE: xdf [--csv] [--swap] [--sort=mode]\n";
}
my $csv_output = 0;
my $include_swap = 0;
my $sort_mode = "mountpoint";
GetOptions(
  "csv"      => \$csv_output,
  "swap"     => \$include_swap,
  "sort=s"   => \$sort_mode,
) or die USAGE;
die USAGE if @ARGV;

my %sort_comparators = (
  mountpoint => sub { $a->{mountpoint}{SORT} cmp $b->{mountpoint}{SORT} }, # ascending
  size       => sub { $b->{size}{SORT}       <=> $a->{size}{SORT}       }, # descending
  used       => sub { $b->{used}{SORT}       <=> $a->{used}{SORT}       }, # descending
  free       => sub { $b->{free}{SORT}       <=> $a->{free}{SORT}       }, # descending
  full       => sub { $b->{full}{SORT}       <=> $a->{full}{SORT}       }, # descending
  device     => sub { $a->{device}{SORT}     cmp $b->{device}{SORT}     }, # ascending
);
my $sort_comparator = $sort_comparators{lc $sort_mode};
if (not defined $sort_comparator) {
  die "Error: valid sort modes are: " . (join ", ", sort keys %sort_comparators) . "\n";
}

# Storage for output variables
my $output = [ ];
my $mountpoint_maxlength = 0;

# ------------------------------------------------------------------------------
# SET UP BASED ON ENVIRONMENT
# ------------------------------------------------------------------------------

# All supported O/S's df commands output in this format (so far):
my $RGXCAP_df_output_general = qr{
  ^
  (\S+)   # CAP $1: device
  \s+
  (\d+)   # CAP $2: size
  \s+
  (\d+)   # CAP $3: used
  \s+
  (\d+)   # CAP $4: free
  \s+
  (\d+)%  # CAP $5: use %
  \s+
  (\/.*)  # CAP $6: mount point
  $
}x;

# Set external commands and output-matching regexes based on O/S
my (@df_cmd, @mount_cmd, $RGX_local_partition, $RGX_swap_partition, $RGXCAP_df_output);
my @zpool_cmd = qw(/sbin/zpool list -H);
# --- SOLARIS ------------------------------------------------------------------
if ("SOLARIS" eq uc $^O) {
  @df_cmd = qw(/bin/df -kl);
  @mount_cmd = qw(/sbin/mount);
  $RGX_local_partition = qr{
    ^
    /dev/dsk/ c\d+ t\d+ d\d+ s\d+
      |
    /dev/md/dsk/ d \d{1,3}
      |
    swap
  }x;
  $RGX_swap_partition = qr{
    ^
    (?: swap )
  }x;
  $RGXCAP_df_output = $RGXCAP_df_output_general;
}
# --- SUNOS --------------------------------------------------------------------
elsif ("SUNOS" eq uc $^O) {
  @df_cmd = qw(/bin/df -t 4.2);
  @mount_cmd = qw(/usr/etc/mount);
  $RGX_local_partition = qr{
    ^
    /dev/sd \d[a-z]
  }x;
  undef $RGX_swap_partition;
  $RGXCAP_df_output = $RGXCAP_df_output_general;
}
# --- LINUX --------------------------------------------------------------------
elsif ("LINUX" eq uc $^O) {
  @df_cmd = qw(/bin/df -klP);
  @mount_cmd = qw(/bin/mount);
  $RGX_local_partition = qr{
    ^
    /dev/.*?
      |
    tmpfs
      |
    LABEL=/tmp
  }x;
  $RGX_swap_partition = qr{
    ^
    tmpfs
      |
    LABEL=/tmp
  }x;
  $RGXCAP_df_output = $RGXCAP_df_output_general;
}
# --- UNKNOWN ------------------------------------------------------------------
else {
  die "Operating system $^O is not supported!\n";
}

# ------------------------------------------------------------------------------
# PROCESS DF OUTPUT
# ------------------------------------------------------------------------------

my @df_output = ext_command(@df_cmd);
# Filter out any non-local partitions
my @local_partitions = grep { /$RGX_local_partition/ } @df_output;
# Remove swap partitions unless running with --swap
if (defined $RGX_swap_partition and not $include_swap) {
  @local_partitions = grep { $_ !~ $RGX_swap_partition } @local_partitions;
}
# Remove duplicates (Linux reports the /tmp partition twice)
@local_partitions = keys %{{ map { $_ => 1 } @local_partitions }};

# Process df output and store formatted output and sort fields.
foreach my $partition (@local_partitions) {
  if ($partition =~ m/$RGXCAP_df_output/) {
    my ($device, $size, $used, $free, $full, $mountpoint) = ($1, $2, $3, $4, $5, $6);
    push @{$output}, {
      device     => { SORT => $device,     DISPLAY => $device               },
      size       => { SORT => $size,       DISPLAY => kb_to_readable($size) },
      used       => { SORT => $used,       DISPLAY => kb_to_readable($used) },
      free       => { SORT => $free,       DISPLAY => kb_to_readable($free) },
      full       => { SORT => $full,       DISPLAY => "$full%"              },
      mountpoint => { SORT => $mountpoint, DISPLAY => $mountpoint           },
    };
    if ($mountpoint_maxlength < length $mountpoint) {
      $mountpoint_maxlength = length $mountpoint;
    }
  }
  else {
    die "Could not parse: \"$partition\"\n";
  }
}

# ------------------------------------------------------------------------------
# PROCESS ZFS POOLS (IF SUPPORTED)
# ------------------------------------------------------------------------------

if (-x $zpool_cmd[0]) {
  my @zpool_output = ext_command(@zpool_cmd);
  if (@zpool_output > 1 or $zpool_output[0] !~ m/no pools/) {
    my @mount_output = ext_command(@mount_cmd);
    foreach my $zpool_line (@zpool_output) {
      # zpool output looks like (fields are tab-delimited):
      # pool_name   X.XXG  X.XXG  X.XXG   XX%  <more fields>
      # size fields are total, used, free, used%
      my ($device, $size, $used, $free, $full) = split m/\t/, $zpool_line;
      my $mountpoint = "unknown";
      foreach my $mount_line (@mount_output) {
        if ($mount_line =~ m/(\/\S*)[ ]on[ ]$device[ ]/) {
          $mountpoint = $1;
          last;
        }
      }
      foreach ($size, $used, $free) {
        s/(\d)([A-Z])/$1 $2b/;
      }
      (my $full_sort = $full) =~ s/%$//;
      push @{$output}, {
        device     => { SORT => "ZPOOL:$device",       DISPLAY => "ZPOOL:$device" },
        size       => { SORT => readable_to_kb($size), DISPLAY => $size           },
        used       => { SORT => readable_to_kb($used), DISPLAY => $used           },
        free       => { SORT => readable_to_kb($free), DISPLAY => $free           },
        full       => { SORT => $full_sort,            DISPLAY => $full           },
        mountpoint => { SORT => $mountpoint,           DISPLAY => $mountpoint     },
      };
      if ($mountpoint_maxlength < length $mountpoint) {
        $mountpoint_maxlength = length $mountpoint;
      }
    }
  }
}

# ------------------------------------------------------------------------------
# SORT AND PRINT THE OUTPUT
# ------------------------------------------------------------------------------

my $format = "%-${mountpoint_maxlength}s  %8s  %8s  %8s  %4s  %s\n";
if ($csv_output) {
  foreach my $output_item (sort $sort_comparator @{$output}) {
    print join ",",
      map { $_->{DISPLAY} }
          @{$output_item}{qw(mountpoint size used free full device)};
    print "\n";
  }
}
else {
  print "\n";
  printf $format, qw(mountpoint size used free full device);
  print "-" x 80, "\n";
  foreach my $output_item (sort $sort_comparator @{$output}) {
    printf $format,
      map { $_->{DISPLAY} }
          @{$output_item}{qw(mountpoint size used free full device)};
  }
  print "\n";
}

exit;

# ===== SUBROUTINES ==========================================================

# Convert kilobytes to "human readable" appropriate units
sub kb_to_readable {
  my $kb = $_[0];
  if ($kb < 900) {
    return "$kb Kb";
  } elsif ($kb < 900_000) {
    return sprintf "%1.1f Mb", $kb / 1024;
  } elsif ($kb < 900_000_000) {
    return sprintf "%1.1f Gb", $kb / 1024**2;
  } else {
    return sprintf "%1.1f Tb", $kb / 1024**3;
  }
}

# Convert human readable units to raw Kb (for sorting)
sub readable_to_kb {
  my $size_str = uc $_[0];
  my ($num, $unit) = ($size_str =~ m/^([\d.]+)[ ]*(?:([BKMGTP])B?)?$/);
  $unit = "" if not $unit;
  switch ($unit) {
    case ""  { return $num;           }
    case "B" { return $num / 1024;    }
    case "K" { return $num;           }
    case "M" { return $num * 1024;    }
    case "G" { return $num * 1024**2; }
    case "T" { return $num * 1024**3; }
    case "P" { return $num * 1024**4; }
  }
  die "Invalid strings passed to readable_to_kb()\n";
}

# Execute an external command and return the ouput
sub ext_command {
  my @command_and_args = @_;
  open COMMAND, "-|", @command_and_args
    or die "Could not execute $command_and_args[0]: $!\n";
  my @output_lines = <COMMAND>;
  close COMMAND;
  my $retval = $? >> 8;
  if ($retval != 0) {
    die "$command_and_args[0] returned nonzero exit code ($retval)\n";
  }
  return @output_lines;
}

    
Download this file
Jeremy Holland - Code Portfolio
Contact Me