negative limit counts from last line
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 7a0bf878fab83ce4c5b06d2364685be35d4f9649..5d0f85c287b571a726a6426ae027406b3b8cb0ef 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -11,6 +11,7 @@ our $VERSION = '1.07';
 use Getopt::Long '2.33', qw( :config gnu_getopt );
 my %opt;
 GetOptions(\%opt,
 use Getopt::Long '2.33', qw( :config gnu_getopt );
 my %opt;
 GetOptions(\%opt,
+       'ascii|a!',
        'color|c!',
        'C' => sub { $opt{color} = 0 },
        'field|f=s' => sub {
        'color|c!',
        'C' => sub { $opt{color} = 0 },
        'field|f=s' => sub {
@@ -38,8 +39,9 @@ GetOptions(\%opt,
        'limit|L:s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
        'limit|L:s' => sub {
                my ($optname, $optval) = @_;
                $optval ||= 0;
+               $optval =~ /\A-[0-9]+\z/ and $optval .= '-';  # tail shorthand
                ($opt{hidemin}, $opt{hidemax}) =
                ($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"
                );
                        "Value \"$optval\" invalid for option limit",
                        " (range expected)\n"
                );
@@ -50,7 +52,9 @@ GetOptions(\%opt,
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
        'spark:s' => sub {
                $opt{'graph-format'} = substr $_[1], 0, 1;
        },
        'spark:s' => sub {
-               $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
+               $opt{spark} = [split //,
+                       $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
+               ];
        },
        'palette=s' => sub {
                $opt{palette} = {
        },
        'palette=s' => sub {
                $opt{palette} = {
@@ -81,7 +85,10 @@ GetOptions(\%opt,
                local $/ = undef;  # slurp
                my $pod = readline *DATA;
                $pod =~ s/^=over\K/ 25/;  # indent options list
                local $/ = undef;  # slurp
                my $pod = readline *DATA;
                $pod =~ s/^=over\K/ 25/;  # indent options list
-               $pod =~ s/^=item\ \N*\n\n\N*\n\K (?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/g;
+               $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/[.,](?=\n)//g;  # trailing punctuation
                $pod =~ s/^=item\ \K(?=--)/____/g;  # align long options
                # abbreviate <variable> indicators
@@ -116,7 +123,8 @@ $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};
 $opt{color} //= -t *STDOUT;  # enable on tty
 $opt{'graph-format'} //= '-';
 $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
-$opt{units}   = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
+$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'} = 1 if $opt{unmodified};
 $opt{anchor} //= qr/\A/;
 $opt{'value-length'} = 6 if $opt{units};
 $opt{'value-length'} = 1 if $opt{unmodified};
@@ -127,6 +135,19 @@ $opt{hidemin} = ($opt{hidemin} || 1) - 1;
 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
        and undef $opt{interval};
 
 $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 {
+       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 (@lines, @values, @order);
 
 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
 my (@lines, @values, @order);
 
 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
@@ -162,7 +183,8 @@ while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
                        $_ = substr $_, 0, 2;
                }
                elsif (length > $trimpos) {
                        $_ = substr $_, 0, 2;
                }
                elsif (length > $trimpos) {
-                       substr($_, $trimpos - 1) = '…';
+                       # cut and replace (intentional lvalue for speed, contrary to PBP)
+                       substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
                }
        }
        push @lines, $_;
                }
        }
        push @lines, $_;
@@ -182,19 +204,11 @@ sub color {
        $_ = color(@_) . $_ . color(0) if defined;
 }
 
        $_ = color(@_) . $_ . color(0) if defined;
 }
 
-sub sival {
-       my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
-       my $float = $_[0] !~ /\A0*[-0-9]{1,3}\z/;
-       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 {
 
 sub show_lines {
 
-state $nr = $opt{hidemin};
+state $nr =
+       $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
+       $opt{hidemin};
 @lines or return;
 @lines > $nr or return;
 
 @lines or return;
 @lines > $nr or return;
 
@@ -209,7 +223,7 @@ 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 $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   = $range &&
+my $size   = defined $opt{width} && $range &&
        ($opt{width} - $lenval - $len) / $range;  # bar multiplication
 
 my @barmark;
        ($opt{width} - $lenval - $len) / $range;  # bar multiplication
 
 my @barmark;
@@ -221,13 +235,22 @@ if ($opt{markers} and $size > 0) {
                                return sum(@order) / @order;
                        }
                        elsif ($func =~ /\A([0-9.]+)v\z/) {
                                return sum(@order) / @order;
                        }
                        elsif ($func =~ /\A([0-9.]+)v\z/) {
+                               die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
                                my $index = $#order * $1 / 100;
                                return ($order[$index] + $order[$index + .5]) / 2;
                        }
                                my $index = $#order * $1 / 100;
                                return ($order[$index] + $order[$index + .5]) / 2;
                        }
-                       else {
+                       elsif ($func =~ /\A-?[0-9.]+\z/) {
                                return $func;
                        }
                                return $func;
                        }
-               } - $minval;
+                       else {
+                               die "Unknown marker $char: $func\n";
+                       }
+               };
+               defined $pos or do {
+                       warn $@ if $@;
+                       next;
+               };
+               $pos -= $minval;
                $pos >= 0 or next;
                color(36) for $barmark[$pos * $size] = $char;
        }
                $pos >= 0 or next;
                color(36) for $barmark[$pos * $size] = $char;
        }
@@ -266,7 +289,7 @@ while ($nr <= $#lines) {
        if ($opt{spark}) {
                say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
                print color($color), $opt{spark}->[
        if ($opt{spark}) {
                say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
                print color($color), $opt{spark}->[
-                       !$val ? 0 : # blank
+                       !$val || !$#{$opt{spark}} ? 0 : # blank
                        $val == $order[0] ? -1 : # max
                        $val == $order[-1] ? 1 : # min
                        $#{$opt{spark}} < 3 ? 1 :
                        $val == $order[0] ? -1 : # max
                        $val == $order[-1] ? 1 : # min
                        $#{$opt{spark}} < 3 ? 1 :
@@ -276,7 +299,8 @@ while ($nr <= $#lines) {
        }
 
        if (length $val) {
        }
 
        if (length $val) {
-               $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
+               $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
+                       sprintf "%*s", $lenval, $val;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
@@ -290,26 +314,31 @@ continue {
 }
 say $opt{palette} ? color(0) : '' if $opt{spark};
 
 }
 say $opt{palette} ? color(0) : '' if $opt{spark};
 
+       return $nr;
 }
 
 sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
 }
 
 sub show_stat {
        if ($opt{hidemin} or $opt{hidemax}) {
-               printf '%s of ', sum(grep { length }
-                       @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
-               ) // 0;
+               my $linemin = $opt{hidemin};
+               $linemin += @lines if $linemin < 0;
+               my $linemax = ($opt{hidemax} || @lines) - 1;
+               printf '%.8g of ', $opt{'sum-format'}->(
+                       sum(grep {length} @values[$linemin .. $linemax]) // 0
+               );
        }
        if (@order) {
                my $total = sum @order;
        }
        if (@order) {
                my $total = sum @order;
-               printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
+               printf '%s total', color(1) . $opt{'sum-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)',
                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),
-                       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 '';
                );
        }
        say '';
+       return 1;
 }
 
 sub show_exit {
 }
 
 sub show_exit {
@@ -349,6 +378,12 @@ you'll need a larger animal like I<gnuplot>.
 
 =over
 
 
 =over
 
+=item -a, --[no-]ascii
+
+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
 
 Force colored output of values and bar markers.
 =item -c, --[no-]color
 
 Force colored output of values and bar markers.
@@ -393,9 +428,13 @@ unless C<--length=0>.
 Prepend a dash (i.e. make negative) to enforce padding
 regardless of encountered contents.
 
 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.
 
 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.
 
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
@@ -544,12 +583,12 @@ In PostgreSQL from within the client:
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
-    https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
+    curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
     column -tns, | barcat -f4 -u -l80%
 
 External datasets, like movies per year:
 
     column -tns, | barcat -f4 -u -l80%
 
 External datasets, like movies per year:
 
-    curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
+    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
     perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
 
 But please get I<jq> to process JSON
@@ -557,7 +596,7 @@ and replace the manual selection by C<< jq '.[].year' >>.
 
 Pokémon height comparison:
 
 
 Pokémon height comparison:
 
-    curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
+    curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
 
 USD/EUR exchange rate from CSV provided by the ECB:
     jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
 
 USD/EUR exchange rate from CSV provided by the ECB:
@@ -568,7 +607,7 @@ USD/EUR exchange rate from CSV provided by the ECB:
 
 Total population history 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 |
+    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
 
     xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
     sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H