move sparkline example below interrupted git group
[barcat.git] / barcat
diff --git a/barcat b/barcat
index d4a8274f6c9af8015332215138b8d46c3158a30d..dfe6a1018ecc8f55ba006db654366788795e43a5 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -8,9 +8,12 @@ use re '/msx';
 
 our $VERSION = '1.07';
 
 
 our $VERSION = '1.07';
 
-use Getopt::Long '2.33', qw( :config gnu_getopt );
 my %opt;
 my %opt;
+if (@ARGV) {
+require Getopt::Long;
+Getopt::Long->import('2.33', qw( :config gnu_getopt ));
 GetOptions(\%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 +41,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 +54,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} = {
@@ -78,30 +84,7 @@ GetOptions(\%opt,
                exit;
        },
        'usage|h' => sub {
                exit;
        },
        'usage|h' => sub {
-               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/[.,](?=\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 {
                exit;
        },
        'help|?'  => sub {
@@ -111,12 +94,14 @@ GetOptions(\%opt,
                );
        },
 ) or exit 64;  # EX_USAGE
                );
        },
 ) or exit 64;  # EX_USAGE
+}
 
 $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{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{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 +112,28 @@ $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 = (
+               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
+       );
+};
+
+
 my (@lines, @values, @order);
 
 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
 my (@lines, @values, @order);
 
 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
@@ -163,7 +170,7 @@ while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
                }
                elsif (length > $trimpos) {
                        # cut and replace (intentional lvalue for speed, contrary to PBP)
                }
                elsif (length > $trimpos) {
                        # cut and replace (intentional lvalue for speed, contrary to PBP)
-                       substr($_, $trimpos - 1) = '…';
+                       substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
                }
        }
        push @lines, $_;
                }
        }
        push @lines, $_;
@@ -183,25 +190,26 @@ 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/;
-       return 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};
-@lines or return;
+state $nr =
+       $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
+       $opt{hidemin};
 @lines > $nr or return;
 
 @lines > $nr or return;
 
+my $limit = $#lines;
+if (defined $opt{hidemax}) {
+       if ($opt{hidemin} and $opt{hidemin} < 0) {
+               $limit -= $opt{hidemax} - 1;
+       }
+       else {
+               $limit = $opt{hidemax} - 1;
+       }
+}
+
 @order = sort { $b <=> $a } @order unless tied @order;
 my $maxval = $opt{maxval} // (
 @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;
        $order[0]
 ) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
@@ -264,8 +272,7 @@ say(
        color(0),
 ) if $opt{header};
 
        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 $color = !length $val || !$opt{palette} ? undef :
        my $val = $values[$nr];
        my $rel = length $val && $range && ($val - $minval) / $range;
        my $color = !length $val || !$opt{palette} ? undef :
@@ -286,10 +293,15 @@ 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;
+       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;
        printf '%-*s', $len + length($val), $line;
        print $barmark[$_] // $opt{'graph-format'}
                for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
@@ -305,19 +317,25 @@ say $opt{palette} ? color(0) : '' if $opt{spark};
 
 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};
+               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;
        }
        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 '';
@@ -334,6 +352,37 @@ sub show_exit {
 show_exit();
 
 __END__
 show_exit();
 
 __END__
+Usage:
+  barcat [OPTIONS] [FILES|NUMBERS]
+
+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[=CHARS]      Replace lines by sparklines
+  -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 documentation
+      --version            Version information
+
 =encoding utf8
 
 =head1 NAME
 =encoding utf8
 
 =head1 NAME
@@ -361,6 +410,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.
@@ -405,9 +460,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.
 
@@ -592,16 +651,16 @@ Or the top 3 most frequent authors with statistics over all:
 
     git shortlog -sn | barcat -L3 -s
 
 
     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
 
 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 --spark= 3 1 4 1 5 0 9 2 4
+
 =head1 AUTHOR
 
 Mischa POSLAWSKY <perl@shiar.org>
 =head1 AUTHOR
 
 Mischa POSLAWSKY <perl@shiar.org>