keep valueless lines as is
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 838c89f95fe14c41a5e95329b669a264db7f4e5b..54761d9aaa1835512f2ba4fb8111caf86ed7b2c9 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} = {
@@ -78,33 +82,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 \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 {
                exit;
        },
        'help|?'  => sub {
@@ -119,7 +97,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};
@@ -133,7 +112,12 @@ $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 {
 $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 $unit = int(
+               log(abs $_[0] || 1) / log(10)
+               - 3 * ($_[0] < .9995)   # shift to smaller unit if below 1
+               - log(.9995) / log(10)  # 3 digits rounding up
+               + 1e-15  # float imprecision
+       );
        my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
        sprintf('%3.*f%1s',
                $float && ($unit % 3) == ($unit < 0),  # tenths
        my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
        sprintf('%3.*f%1s',
                $float && ($unit % 3) == ($unit < 0),  # tenths
@@ -179,7 +163,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, $_;
@@ -201,13 +185,24 @@ sub color {
 
 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;
@@ -270,8 +265,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 :
@@ -297,6 +291,10 @@ while ($nr <= $#lines) {
                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;
@@ -312,9 +310,15 @@ 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 '%.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;
        }
        if (@order) {
                my $total = sum @order;
@@ -322,9 +326,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)',
                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 '';
                );
        }
        say '';
@@ -341,6 +345,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
@@ -368,6 +403,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.
@@ -412,9 +453,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.