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