stable enough version 1.07
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 2acdaddfca5c4cf232a67cb35375742fcfaddea4..da07ea706c7e149754edafc6465251b3f1592554 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -1,12 +1,11 @@
 #!/usr/bin/perl -CA
-use 5.018;
+use 5.014;
 use warnings;
 use utf8;
 use List::Util qw( min max sum );
 use open qw( :std :utf8 );
-use experimental qw( lexical_subs );
 
-our $VERSION = '1.06';
+our $VERSION = '1.07';
 
 use Getopt::Long '2.33', qw( :config gnu_getopt );
 my %opt;
@@ -35,7 +34,7 @@ GetOptions(\%opt,
        'hidemax=i',
        'minval=f',
        'maxval=f',
-       'limit|L=s' => sub {
+       'limit|L:s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
                ($opt{hidemin}, $opt{hidemax}) =
@@ -50,7 +49,7 @@ GetOptions(\%opt,
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
        'spark:s' => sub {
-               $opt{spark} = [split //, $_[1] || '▁▂▃▄▅▆▇█'];
+               $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
        },
        'palette=s' => sub {
                $opt{palette} = {
@@ -80,16 +79,27 @@ GetOptions(\%opt,
        'usage|h' => sub {
                local $/;
                my $pod = readline *DATA;
-               $pod =~ s/^=over\K/ 22/m;  # indent options list
+               $pod =~ s/^=over\K/ 25/m;  # indent options list
                $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
+               $pod =~ s/[.,](?=\n)//g;  # trailing punctuation
+               $pod =~ s/^=item \K(?=--)/____/gm;  # align long options
+               # abbreviate <variable> indicators
+               $pod =~ s/\Q>.../s>/g;
+               $pod =~ s/<(?:number|count|seconds)>/N/g;
+               $pod =~ s/<character(s?)>/\Uchar$1/g;
+               $pod =~ s/\Q | /|/g;
+               $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g;  # uppercase
 
                require Pod::Usage;
-               my $parser = Pod::Usage->new;
+               my $parser = Pod::Usage->new(USAGE_OPTIONS => {
+                       -indent => 2, -width => 78,
+               });
                $parser->select('SYNOPSIS', 'OPTIONS');
                $parser->output_string(\my $contents);
                $parser->parse_string_document($pod);
 
                $contents =~ s/\n(?=\n\h)//msg;  # strip space between items
+               $contents =~ s/^  \K____/    /gm;  # nbsp substitute
                print $contents;
                exit;
        },
@@ -101,7 +111,7 @@ GetOptions(\%opt,
        },
 ) or exit 64;  # EX_USAGE
 
-$opt{width} ||= $ENV{COLUMNS} || 80;
+$opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
 $opt{color} //= -t *STDOUT;  # enable on tty
 $opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
@@ -112,6 +122,9 @@ $opt{'value-length'} = 1 if $opt{unmodified};
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
 $opt{palette} //= $opt{color} && [31, 90, 32];
+$opt{hidemin} = ($opt{hidemin} || 1) - 1;
+$opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
+       and undef $opt{interval};
 
 my (@lines, @values, @order);
 
@@ -132,8 +145,10 @@ if (defined $opt{interval}) {
        } or warn $@, "Expect slowdown with large datasets!\n";
 }
 
-my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
-while (readline) {
+my $valmatch = qr<
+       $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
+>x;
+while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
        s/\r?\n\z//;
        s/^\h*// unless $opt{unmodified};
        push @values, s/$valmatch/\n/ && $1;
@@ -153,6 +168,10 @@ while (readline) {
                and $. % $opt{interval} == 0;
 }
 
+if ($opt{'zero-missing'}) {
+       push @values, (0) x 10;
+}
+
 $SIG{INT} = 'DEFAULT';
 
 sub color {
@@ -161,22 +180,35 @@ sub color {
        $_ = color(@_) . $_ . color(0) if defined;
 }
 
+sub sival {
+       my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
+       my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
+       sprintf('%3.*f%1s',
+               $float && ($unit % 3) == ($unit < 0),  # tenths
+               $_[0] / 1000 ** int($unit/3),   # number
+               $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
+       );
+}
+
 sub show_lines {
 
-state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
-@lines and @lines > $nr or return;
+state $nr = $opt{hidemin};
 @lines or return;
-@lines > $nr or return unless $opt{hidemin};
+@lines > $nr or return;
 
 @order = sort { $b <=> $a } @order unless tied @order;
-my $maxval = $opt{maxval} // ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
+my $maxval = $opt{maxval} // (
+       $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
+       $order[0]
+) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
+my $range = $maxval - $minval;
 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[$_] }
                0 .. min $#lines, $opt{hidemax} || ();  # left padding
-my $size   = ($maxval - $minval) &&
-       ($opt{width} - $lenval - $len) / ($maxval - $minval);  # bar multiplication
+my $size   = $range &&
+       ($opt{width} - $lenval - $len) / $range;  # bar multiplication
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
@@ -206,28 +238,16 @@ if ($opt{markers} and $size > 0) {
                        ($lastmax - $minval) * $size + .5,
                        '-' x (($values[$nr - 1] - $minval) * $size);
                print color(92);
-               say '+' x (($maxval - $lastmax - $minval) * $size + .5);
+               say '+' x (($range - $lastmax) * $size + .5);
                print color(0);
                $lastmax = $maxval;
        }
 }
 
-@lines > $nr or return if $opt{hidemin};
-
-sub sival {
-       my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
-       my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
-       sprintf('%3.*f%1s',
-               $float && ($unit % 3) == ($unit < 0),  # tenths
-               $_[0] / 1000 ** int($unit/3),   # number
-               $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
-       );
-}
-
 say(
        color(31), sprintf('%*s', $lenval, $minval),
        color(90), '-', color(36), '+',
-       color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
+       color(32), sprintf('%*s', $size * $range - 3, $maxval),
        color(90), '-', color(36), '+',
        color(0),
 ) if $opt{header};
@@ -235,25 +255,32 @@ say(
 while ($nr <= $#lines) {
        $nr >= $opt{hidemax} and last if defined $opt{hidemax};
        my $val = $values[$nr];
-       my $rel = length $val && ($val - $minval) / ($maxval - $minval);
+       my $rel = length $val && $range && ($val - $minval) / $range;
+       my $color = !length $val || !$opt{palette} ? undef :
+               $val == $order[0] ? $opt{palette}->[-1] : # max
+               $val == $order[-1] ? $opt{palette}->[0] : # min
+               $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
 
        if ($opt{spark}) {
-               print color($opt{palette}->[ $rel * $#{$opt{palette}} ]) if $opt{palette};
-               print $opt{spark}->[ $rel * $#{$opt{spark}} ];
+               say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
+               print color($color), $opt{spark}->[
+                       !$val ? 0 : # blank
+                       $val == $order[0] ? -1 : # max
+                       $val == $order[-1] ? 1 : # min
+                       $#{$opt{spark}} < 3 ? 1 :
+                       $rel * ($#{$opt{spark}} - 3) + 2.5
+               ];
                next;
        }
 
        if (length $val) {
-               my $color = !$opt{palette} ? undef :
-                       $val == $order[0] ? $opt{palette}->[-1] : # max
-                       $val == $order[-1] ? $opt{palette}->[0] : # min
-                       $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
                $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
        printf '%-*s', $len + length($val), $line;
-       print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
+       print $barmark[$_] // $opt{'graph-format'}
+               for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
        say '';
 }
 continue {
@@ -265,14 +292,15 @@ say $opt{palette} ? color(0) : '' if $opt{spark};
 
 sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
-               $opt{hidemin} ||= 1;
-               $opt{hidemax} ||= @lines;
-               printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
+               printf '%s of ', sum(grep { length }
+                       @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
+               ) // 0;
        }
        if (@order) {
                my $total = sum @order;
-               printf '%s total', color(1) . $total . color(0);
-               printf ' in %d values', scalar @values;
+               printf '%s total', color(1) . sprintf('%.8g', $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) . $order[-1] . color(0),
                        color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
@@ -300,11 +328,12 @@ barcat - graph to visualize input values
 
 =head1 SYNOPSIS
 
-B<barcat> [<options>] [<input>]
+B<barcat> [<options>] [<file>... | <numbers>]
 
 =head1 DESCRIPTION
 
-Visualizes relative sizes of values read from input (file(s) or STDIN).
+Visualizes relative sizes of values read from input
+(parameters, file(s) or STDIN).
 Contents are concatenated similar to I<cat>,
 but numbers are reformatted and a bar graph is appended to each line.
 
@@ -324,7 +353,7 @@ Force colored output of values and bar markers.
 Defaults on if output is a tty,
 disabled otherwise such as when piped or redirected.
 
-=item -f, --field=(<number>|<regexp>)
+=item -f, --field=(<number> | <regexp>)
 
 Compare values after a given number of whitespace separators,
 or matching a regular expression.
@@ -347,7 +376,7 @@ 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 -t, --interval[=(<seconds>|-<lines>)]
+=item -t, --interval[=(<seconds> | -<lines>)]
 
 Output partial progress every given number of seconds or input lines.
 An update can also be forced by sending a I<SIGALRM> alarm signal.
@@ -362,7 +391,7 @@ unless C<--length=0>.
 Prepend a dash (i.e. make negative) to enforce padding
 regardless of encountered contents.
 
-=item -L, --limit=(<count>|<start>-[<end>])
+=item -L, --limit[=(<count> | <start>-[<end>])]
 
 Stop output after a number of lines.
 All input is still counted and analyzed for statistics,
@@ -414,13 +443,18 @@ These options can be set to customize this range.
 
 Override colors of parsed numbers.
 Can be any CSI escape, such as I<90> for default dark grey,
-or alternatively I<1;30> for bold black.
+or alternatively I<1;30> for bright black.
 
 In case of additional colors,
 the last is used for values equal to the maximum, the first for minima.
 If unspecified, these are green and red respectively (I<31 90 32>).
+Multiple intermediate colors will be distributed
+relative to the size of values.
 
-=item --spark[=<glyphs>]
+Predefined color schemes are named I<whites> and I<fire>,
+or I<greys> and I<fire256> for 256-color variants.
+
+=item --spark[=<characters>]
 
 Replace lines by I<sparklines>,
 single characters corresponding to input values.
@@ -485,7 +519,7 @@ Monitor network latency from prefixed results:
 
 Commonly used after counting, for example users on the current server:
 
-    users | sed 's/ /\n/g' | sort | uniq -c | barcat
+    users | tr ' ' '\n' | sort | uniq -c | barcat
 
 Letter frequencies in text files:
 
@@ -502,10 +536,14 @@ Any kind of database query with counts, preserving returned alignment:
     echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
     psql -t | barcat -u
 
+In PostgreSQL from within the client:
+
+       postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
+
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
     https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
-    column -tns, | graph -f4 -u -l80%
+    column -tns, | barcat -f4 -u -l80%
 
 External datasets, like movies per year:
 
@@ -526,8 +564,7 @@ USD/EUR exchange rate from CSV provided by the ECB:
          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
     grep '^[12]' | barcat -f',\K' --value-length=7
 
-Total population history from the World Bank dataset (XML):
-External datasets, like total population in XML from the World Bank:
+Total population history in XML from the World Bank:
 
     curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
@@ -541,11 +578,15 @@ Or the top 3 most frequent authors with statistics over all:
 
     git shortlog -sn | barcat -L3 -s
 
-Activity of the last days (substitute date C<-v-{}d> on BSD):
+Sparkline graphics of simple input given as inline parameters:
+
+       barcat --spark= 3 1 4 1 5 0 9 2 4
+
+Activity graph of the last days (substitute date C<-v-{}d> on BSD):
 
     ( git log --pretty=%ci --since=30day | cut -b-10
       seq 0 30 | xargs -i date +%F -d-{}day ) |
-    sort | uniq -c | awk '$1--' | graph --spark
+    sort | uniq -c | awk '$1--' | barcat --spark
 
 =head1 AUTHOR