logarithmic scale option
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 4d3205ff3aec082d3741083e8c438902e45aeb52..b3d9ce9a9c7e7fa784e07d25a384b5d8b4710104 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -6,7 +6,7 @@ use List::Util qw( min max sum );
 use open qw( :std :utf8 );
 use re '/msx';
 
-our $VERSION = '1.08';
+our $VERSION = '1.09';
 
 my %opt;
 if (@ARGV) {
@@ -14,15 +14,21 @@ require Getopt::Long;
 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
 GetOptions(\%opt,
        'ascii|a!',
-       'color|c!',
-       'C' => sub { $opt{color} = 0 },
+       'color|C!',
+       'M' => sub { $opt{color} = 0 },
        'field|f=s' => sub {
                eval {
                        local $_ = $_[1];
-                       $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
+                       s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/;
+                       s{\A[+]([0-9]*)\z}{
+                               (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)'
+                       }e;
+                       $opt{anchor} = qr/$_/;
                } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
        },
        'human-readable|H!',
+       'sexagesimal!',
+       'reformat!',
        'interval|t:i',
        'trim|length|l=s' => sub {
                my ($optname, $optval) = @_;
@@ -48,6 +54,7 @@ GetOptions(\%opt,
                        " (range expected)\n"
                );
        },
+       'log|e!',
        'header!',
        'markers|m=s',
        'graph-format=s' => sub {
@@ -105,13 +112,13 @@ GetOptions(\%opt,
 }
 
 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
-$opt{color} //= -t *STDOUT;  # enable on tty
+$opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT;  # enable on tty
 $opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
        if $opt{'human-readable'};
 $opt{anchor} //= qr/\A/;
-$opt{'value-length'} = 6 if $opt{units};
+$opt{'value-length'} = 4 if $opt{units};
 $opt{'value-length'} = 1 if $opt{unmodified};
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
@@ -123,9 +130,11 @@ $opt{hidemin} = ($opt{hidemin} || 1) - 1;
 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
        and undef $opt{interval};
 
-$opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
-$opt{'value-format'} = $opt{units} && sub {
+$opt{'value-format'} = $opt{sexagesimal} ? sub {
+       my $s = abs($_[0]) + .5;
+       sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
+} : $opt{units} && sub {
        my $unit = (
                log(abs $_[0] || 1) / log(10)
                - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
@@ -142,7 +151,8 @@ $opt{'value-format'} = $opt{units} && sub {
                $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
                        $opt{units}->[$unit/3]  # suffix
        );
-};
+} and $opt{reformat}++;
+$opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
 
 
 my (@lines, @values, @order);
@@ -189,10 +199,6 @@ while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
                and $. % $opt{interval} == 0;
 }
 
-if ($opt{'zero-missing'}) {
-       push @values, (0) x 10;
-}
-
 $SIG{INT} = 'DEFAULT';
 
 sub color {
@@ -225,6 +231,7 @@ my $maxval = $opt{maxval} // (
 ) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
 my $range = $maxval - $minval;
+$range &&= log $maxval if $opt{log};
 my $lenval = $opt{'value-length'} // max map { length } @order;
 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
        max map { length $values[$_] && length $lines[$_] }
@@ -241,7 +248,9 @@ if ($opt{markers} and $size > 0) {
                                return sum(@order) / @order;
                        }
                        elsif ($func =~ /\A([0-9.]+)v\z/) {
-                               die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
+                               $1 <= 100 or die(
+                                       "Invalid marker $char: percentile $1 out of bounds\n"
+                               );
                                my $index = $#order * $1 / 100;
                                return ($order[$index] + $order[$index + .5]) / 2;
                        }
@@ -257,6 +266,7 @@ if ($opt{markers} and $size > 0) {
                        next;
                };
                $pos -= $minval;
+               $pos &&= log $pos if $opt{log};
                $pos >= 0 or next;
                color(36) for $barmark[$pos * $size] = $char;
        }
@@ -285,7 +295,7 @@ say(
 
 while ($nr <= $limit) {
        my $val = $values[$nr];
-       my $rel = length $val && $range && ($val - $minval) / $range;
+       my $rel = length $val && $range && min(1, ($val - $minval) / $range);
        my $color = !length $val || !$opt{palette} ? undef :
                $val == $order[0] ? $opt{palette}->[-1] : # max
                $val == $order[-1] ? $opt{palette}->[0] : # min
@@ -305,8 +315,9 @@ while ($nr <= $limit) {
        print $indicator if defined $indicator;
 
        if (length $val) {
-               $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
-                       sprintf "%*s", $lenval, $val;
+               $val = sprintf("%*s", $lenval,
+                       $opt{reformat} ? $opt{'value-format'}->($val) : $val
+               );
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
@@ -315,8 +326,10 @@ while ($nr <= $limit) {
                next;
        }
        printf '%-*s', $len + length($val), $line;
+       my $barlen = $values[$nr] || 0;
+       $barlen &&= log $barlen if $opt{log};
        print $barmark[$_] // $opt{'graph-format'}
-               for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
+               for 1 .. $size && ($barlen - $minval) * $size + .5;
        say '';
 }
 continue {
@@ -335,19 +348,19 @@ sub show_stat {
                        $linemin += @lines;
                        $linemax = @lines - $linemax;
                }
-               printf '%.8g of ', $opt{'sum-format'}->(
+               printf '%.8g of ', $opt{'value-format'}->(
                        sum(grep {length} @values[$linemin .. $linemax]) // 0
                );
        }
        if (@order) {
                my $total = sum @order;
-               printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
+               printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
                printf ' in %d values', scalar @order;
                printf ' over %d lines', scalar @lines if @order != @lines;
                printf(' (%s min, %s avg, %s max)',
-                       color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
-                       color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
-                       color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
+                       color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
+                       color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
+                       color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
                );
        }
        say '';
@@ -369,18 +382,21 @@ Usage:                                               /\_/\
                                                     (u   u)
 Options:
   -a, --[no-]ascii         Restrict user interface to ASCII characters
-  -c, --[no-]color         Force colored output of values and bar markers
-  -f, --field=(N|REGEXP)   Compare values after a given number of whitespace
+  -C, --[no-]color         Force colored output of values and bar markers
+  -f, --field=([+]N|REGEXP)
+                           Compare values after a given number of whitespace
                            separators
       --header             Prepend a chart axis with minimum and maximum
                            values labeled
   -H, --human-readable     Format values using SI unit prefixes
+      --sexagesimal        Convert seconds to HH:MM:SS time format
   -t, --interval[=(N|-LINES)]
                            Output partial progress every given number of
                            seconds or input lines
   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
   -L, --limit[=(N|-LAST|START-[END])]
                            Stop output after a number of lines
+  -e, --log                Logarithmic (exponential) scale instead of linear
       --graph-format=CHAR  Glyph to repeat for the graph line
   -m, --markers=FORMAT     Statistical positions to indicate on bars
       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
@@ -430,13 +446,15 @@ Restrict user interface to ASCII characters,
 replacing default UTF-8 by their closest approximation.
 Input is always interpreted as UTF-8 and shown as is.
 
-=item -c, --[no-]color
+=item -C, --[no-]color
 
 Force colored output of values and bar markers.
 Defaults on if output is a tty,
 disabled otherwise such as when piped or redirected.
+Can also be disabled by setting I<-M>
+or the I<NO_COLOR> environment variable.
 
-=item -f, --field=(<number> | <regexp>)
+=item -f, --field=([+]<number> | <regexp>)
 
 Compare values after a given number of whitespace separators,
 or matching a regular expression.
@@ -447,6 +465,7 @@ A string can indicate the starting position of a value
 (such as I<-f:> if preceded by colons),
 or capture the numbers itself,
 for example I<-f'(\d+)'> for the first digits anywhere.
+A shorthand for this is I<+0>, or I<+N> to find the Nth number.
 
 =item --header
 
@@ -459,6 +478,10 @@ turning long numbers like I<12356789> into I<12.4M>.
 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
 Short integers are aligned but kept without decimal point.
 
+=item --sexagesimal
+
+Convert seconds to HH:MM:SS time format.
+
 =item -t, --interval[=(<seconds> | -<lines>)]
 
 Output partial progress every given number of seconds or input lines.
@@ -484,6 +507,11 @@ A specific range can be given by two values.
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
+=item -e, --log
+
+Logarithmic (I<e>xponential) scale instead of linear
+to compare orders of magnitude.
+
 =item --graph-format=<character>
 
 Glyph to repeat for the graph line.
@@ -570,7 +598,9 @@ Reserved space for numbers.
 =item -w, --width=<columns>
 
 Override the maximum number of columns to use.
-Appended graphics will extend to fill up the entire screen.
+Appended graphics will extend to fill up the entire screen,
+otherwise determined by the environment variable I<COLUMNS>
+or by running the C<tput> command.
 
 =item -h, --usage
 
@@ -597,6 +627,20 @@ Compare file sizes (with human-readable numbers):
 
     du -d0 -b * | barcat -H
 
+Same from formatted results, selecting the first numeric value:
+
+    tree -s --noreport | barcat -H -f+
+
+Compare media metadata, like image size or play time:
+
+    exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
+
+    exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
+
+    find -type f -print0 | xargs -0 -L1 \
+    ffprobe -show_format -of json -v error |
+    jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
+
 Memory usage of user processes with long names truncated:
 
     ps xo rss,pid,cmd | barcat -l40
@@ -619,19 +663,26 @@ Number of HTTP requests per day:
 
     cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
-Any kind of database query with counts, preserving returned alignment:
+Any kind of database query results, preserving returned alignment:
 
-    echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
+    echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
     psql -t | barcat -u
 
-In PostgreSQL from within the client:
+In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
 
-    > SELECT sin(generate_series(0, 3, .1)) \g |barcat
+    > SELECT schemaname, relname, pg_total_relation_size(relid)
+      FROM pg_statio_user_tables ORDER BY idx_blks_hit
+      \g |barcat -uHf+
+
+Same thing in SQLite (requires the sqlite3 client):
+
+    > .once |barcat -Hf+
+    > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
-    column -tns, | barcat -f4 -u -l80%
+    column -ts, -n | barcat -f4 -u -l80%
 
 External datasets, like movies per year:
 
@@ -655,6 +706,11 @@ Total population history in XML from the World Bank:
     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
     barcat -f1 -H
 
+Population and other information for all countries:
+
+    curl http://download.geonames.org/export/dump/countryInfo.txt |
+    grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
+
 And of course various Git statistics, such commit count by year:
 
     git log --pretty=%ci | cut -b-4 | uniq -c | barcat