swap -C to enable color, -M to disable
[barcat.git] / barcat
diff --git a/barcat b/barcat
index cc3f607a990787bdb985d9735aeb7a23079c45a3..c0292257676ade42c1a2b5c7a9d429d092bd92f7 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -6,14 +6,16 @@ use List::Util qw( min max sum );
 use open qw( :std :utf8 );
 use re '/msx';
 
-our $VERSION = '1.07';
+our $VERSION = '1.08';
 
-use Getopt::Long '2.33', qw( :config gnu_getopt );
 my %opt;
+if (@ARGV) {
+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];
@@ -39,8 +41,9 @@ GetOptions(\%opt,
        'limit|L:s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
+               $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
                ($opt{hidemin}, $opt{hidemax}) =
-               $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/ or die(
+               $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
                        "Value \"$optval\" invalid for option limit",
                        " (range expected)\n"
                );
@@ -50,64 +53,46 @@ GetOptions(\%opt,
        'graph-format=s' => sub {
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
-       'spark:s' => sub {
-               $opt{spark} = [split //,
-                       $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
-               ];
-       },
+       'spark|_!',
+       'indicators:s',
        'palette=s' => sub {
                $opt{palette} = {
+                       ''     => [],
                        fire   => [qw( 90 31 91 33 93 97 96 )],
-                       fire88 => [map {"38;5;$_"} qw(
-                               80  32 48 64  68 72 76  77 78 79  47
-                       )],
                        fire256=> [map {"38;5;$_"} qw(
                                235  52 88 124 160 196
                                202 208 214 220 226  227 228 229 230 231  159
                        )],
-                       ramp88 => [map {"38;5;$_"} qw(
-                               64 65 66 67 51 35 39 23 22 26 25 28
-                       )],
                        whites => [qw( 1;30 0;37 1;37 )],
-                       greys  => [map {"38;5;$_"} 52, 235..255, 47],
-               }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
+                       greys  => [map {"38;5;$_"} 0, 232..255, 15],
+                       random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
+                       rainbow=> [map {"38;5;$_"}
+                               196, # r
+                               (map { 196 + $_*6   } 0..4), # +g
+                               (map { 226 - $_*6*6 } 0..4), # -r
+                               (map {  46 + $_     } 0..4), # +b
+                               (map {  51 - $_*6   } 0..4), # -g
+                               (map {  21 + $_*6*6 } 0..4), # +r
+                               (map { 201 - $_     } 0..4), # -b
+                               196,
+                       ],
+               }->{$_[1]} // do {
+                       my @vals = split /[^0-9;]/, $_[1]
+                               or die "Empty palette resulting from \"$_[1]\"\n";
+                       \@vals;
+               };
        },
        'stat|s!',
        'signal-stat=s',
        'unmodified|u!',
        'width|w=i',
-       'version' => sub {
-               say "barcat version $VERSION";
+       'version|V' => sub {
+               my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
+               say "barcat $mascot version $VERSION";
                exit;
        },
        'usage|h' => sub {
-               local $/ = undef;  # slurp
-               my $pod = readline *DATA;
-               $pod =~ s/^=over\K/ 25/;  # indent options list
-               $pod =~ s{
-                       ^=item \h \N*\n\n \N*\n \K  # first line
-                       (?: (?: ^=over .*? ^=back\n )? (?!=) \N*\n )*
-               }{\n}g;  # abbreviate options
-               $pod =~ s/[.,](?=\n)//g;  # trailing punctuation
-               $pod =~ s/^=item\ \K(?=--)/____/g;  # 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(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____/    /g;  # nbsp substitute
-               print $contents;
+               /^=/ ? last : print for readline *DATA;  # text between __END__ and pod
                exit;
        },
        'help|?'  => sub {
@@ -117,9 +102,10 @@ GetOptions(\%opt,
                );
        },
 ) or exit 64;  # EX_USAGE
+}
 
 $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']
@@ -130,6 +116,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{indicators} = [split //, $opt{indicators} ||
+       ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
+] if defined $opt{indicators} or $opt{spark};
 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
        and undef $opt{interval};
@@ -137,12 +126,21 @@ $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
 $opt{'value-format'} = $opt{units} && sub {
-       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]
+       my $unit = (
+               log(abs $_[0] || 1) / log(10)
+               - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
+               + 1e-15  # float imprecision
+       );
+       my $decimal = ($unit % 3) == ($unit < 0);
+       $unit -= log($decimal ? .995 : .9995) / log(10);  # rounded
+       $decimal = ($unit % 3) == ($unit < 0);
+       $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/;  # integer 0..999
+       sprintf('%*.*f%1s',
+               3 + ($_[0] < 0), # digits plus optional negative sign
+               $decimal,  # tenths
+               $_[0] / 1000 ** int($unit/3),  # number
+               $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
+                       $opt{units}->[$unit/3]  # suffix
        );
 };
 
@@ -205,13 +203,24 @@ sub color {
 
 sub show_lines {
 
-state $nr = $opt{hidemin};
-@lines or return;
+state $nr =
+       $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
+       $opt{hidemin};
 @lines > $nr or return;
 
+my $limit = $#lines;
+if (defined $opt{hidemax}) {
+       if ($opt{hidemin} and $opt{hidemin} < 0) {
+               $limit -= $opt{hidemax} - 1;
+       }
+       elsif ($opt{hidemax} <= $limit) {
+               $limit = $opt{hidemax} - 1;
+       }
+}
+
 @order = sort { $b <=> $a } @order unless tied @order;
 my $maxval = $opt{maxval} // (
-       $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
+       $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
        $order[0]
 ) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
@@ -221,7 +230,7 @@ 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   = defined $opt{width} && $range &&
-       ($opt{width} - $lenval - $len) / $range;  # bar multiplication
+       ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range;  # bar multiplication
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
@@ -274,26 +283,26 @@ say(
        color(0),
 ) if $opt{header};
 
-while ($nr <= $#lines) {
-       $nr >= $opt{hidemax} and last if defined $opt{hidemax};
+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
                $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
+       my $indicator = $opt{indicators} && $opt{indicators}->[
+               !length($val) || !$#{$opt{indicators}} ? 0 : # blank
+               $#{$opt{indicators}} < 2 ? 1 :
+               $val >= $order[0] ? -1 :
+               $rel * ($#{$opt{indicators}} - 1e-14) + 1
+       ];
 
        if ($opt{spark}) {
                say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
-               print color($color), $opt{spark}->[
-                       !$val || !$#{$opt{spark}} ? 0 : # blank
-                       $val == $order[0] ? -1 : # max
-                       $val == $order[-1] ? 1 : # min
-                       $#{$opt{spark}} < 3 ? 1 :
-                       $rel * ($#{$opt{spark}} - 3) + 2.5
-               ];
+               print color($color), $_ for $indicator;
                next;
        }
+       print $indicator if defined $indicator;
 
        if (length $val) {
                $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
@@ -301,6 +310,10 @@ while ($nr <= $#lines) {
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
+       if (not length $val) {
+               say $line;
+               next;
+       }
        printf '%-*s', $len + length($val), $line;
        print $barmark[$_] // $opt{'graph-format'}
                for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
@@ -316,9 +329,15 @@ say $opt{palette} ? color(0) : '' if $opt{spark};
 
 sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
-               printf '%.8g of ', $opt{'sum-format'}->(sum(grep { length }
-                       @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
-               ) // 0);
+               my $linemin = $opt{hidemin};
+               my $linemax = ($opt{hidemax} || @lines) - 1;
+               if ($linemin < 0) {
+                       $linemin += @lines;
+                       $linemax = @lines - $linemax;
+               }
+               printf '%.8g of ', $opt{'sum-format'}->(
+                       sum(grep {length} @values[$linemin .. $linemax]) // 0
+               );
        }
        if (@order) {
                my $total = sum @order;
@@ -326,9 +345,9 @@ sub show_stat {
                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) . $opt{'calc-format'}->($total / @order) . color(0),
-                       color(32) . $order[0] . color(0),
+                       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),
                );
        }
        say '';
@@ -345,11 +364,44 @@ sub show_exit {
 show_exit();
 
 __END__
+Usage:                                               /\_/\
+  barcat [OPTIONS] [FILES|NUMBERS]                  (=•.•=)
+                                                    (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
+                           separators
+      --header             Prepend a chart axis with minimum and maximum
+                           values labeled
+  -H, --human-readable     Format values using SI unit prefixes
+  -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
+      --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
+      --palette=(PRESET|COLORS)
+                           Override colors of parsed numbers
+  -_, --spark              Replace lines by sparklines
+      --indicators[=CHARS] Prefix a unicode character corresponding to each
+                           value
+  -s, --stat               Total statistics after all data
+  -u, --unmodified         Do not reformat values, keeping leading whitespace
+      --value-length=SIZE  Reserved space for numbers
+  -w, --width=COLUMNS      Override the maximum number of columns to use
+  -h, --usage              Overview of available options
+      --help               Full pod documentation
+  -V, --version            Version information
+
 =encoding utf8
 
 =head1 NAME
 
-barcat - graph to visualize input values
+barcat - concatenate texts with graph to visualize values
 
 =head1 SYNOPSIS
 
@@ -378,11 +430,13 @@ 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>)
 
@@ -422,9 +476,13 @@ 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> | -<last> | <start>-[<end>])]
 
 Stop output after a number of lines.
+A single value indicates the last line number (like C<head>),
+or first line counting from the bottom if negative (like C<tail>).
+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.
 
@@ -485,16 +543,17 @@ relative to the size of values.
 Predefined color schemes are named I<whites> and I<fire>,
 or I<greys> and I<fire256> for 256-color variants.
 
-=item --spark[=<characters>]
+=item -_, --spark
 
 Replace lines by I<sparklines>,
-single characters corresponding to input values.
-A specified sequence of unicode characters will be used for
-Of a specified sequence of unicode characters,
-the first one will be used for non-values,
-the last one for the maximum,
-the second (if any) for the minimum,
-and any remaining will be distributed over the range of values.
+single characters (configured by C<--indicators>)
+corresponding to input values.
+
+=item --indicators[=<characters>]
+
+Prefix a unicode character corresponding to each value.
+The first specified character will be used for non-values,
+the remaining sequence will be distributed over the range of values.
 Unspecified, block fill glyphs U+2581-2588 will be used.
 
 =item -s, --stat
@@ -521,10 +580,10 @@ Overview of available options.
 
 =item --help
 
-Full documentation
-rendered by perldoc.
+Full pod documentation
+as rendered by perldoc.
 
-=item --version
+=item -V, --version
 
 Version information.
 
@@ -542,7 +601,7 @@ Compare file sizes (with human-readable numbers):
 
 Memory usage of user processes with long names truncated:
 
-    ps xo %mem,pid,cmd | barcat -l40
+    ps xo rss,pid,cmd | barcat -l40
 
 Monitor network latency from prefixed results:
 
@@ -560,7 +619,7 @@ Letter frequencies in text files:
 
 Number of HTTP requests per day:
 
-    cat log/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
+    cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
 Any kind of database query with counts, preserving returned alignment:
 
@@ -569,7 +628,7 @@ Any kind of database query with counts, preserving returned alignment:
 
 In PostgreSQL from within the client:
 
-       postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
+    > SELECT sin(generate_series(0, 3, .1)) \g |barcat
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
@@ -579,10 +638,7 @@ Earthquakes worldwide magnitude 1+ in the last 24 hours:
 External datasets, like movies per year:
 
     curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
-    perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
-
-But please get I<jq> to process JSON
-and replace the manual selection by C<< jq '.[].year' >>.
+    jq .[].year | uniq -c | barcat
 
 Pokémon height comparison:
 
@@ -593,13 +649,13 @@ USD/EUR exchange rate from CSV provided by the ECB:
 
     curl https://sdw.ecb.europa.eu/export.do \
          -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
-    grep '^[12]' | barcat -f',\K' --value-length=7
+    barcat -f',\K' --value-length=7
 
 Total population history in XML from the World Bank:
 
-    curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
-    xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
-    sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
+    curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
+    xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
+    barcat -f1 -H
 
 And of course various Git statistics, such commit count by year:
 
@@ -609,16 +665,20 @@ Or the top 3 most frequent authors with statistics over all:
 
     git shortlog -sn | barcat -L3 -s
 
-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--' | barcat --spark
 
+Sparkline graphics of simple input given as inline parameters:
+
+    barcat -_ 3 1 4 1 5 0 9 2 4
+
+Misusing the spark functionality to draw a lolcat line:
+
+    seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
+
 =head1 AUTHOR
 
 Mischa POSLAWSKY <perl@shiar.org>