X-Git-Url: http://git.shiar.nl/barcat.git/blobdiff_plain/2f3c208306a5cd89299468fcf4372719c3d87693..261e0bd8de1f84ceeaa417c9aed73c9862419416:/barcat diff --git a/barcat b/barcat index 7fc7f66..3eb004e 100755 --- a/barcat +++ b/barcat @@ -6,7 +6,7 @@ use List::Util qw( min max sum ); use open qw( :std :utf8 ); use re '/msx'; -our $VERSION = '1.08'; +our $VERSION = '1.09'; my %opt; if (@ARGV) { @@ -40,20 +40,44 @@ GetOptions(\%opt, $opt{trim} = $optval; }, 'value-length=i', - 'hidemin=i', - 'hidemax=i', 'minval=f', 'maxval=f', '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 =~ s/[+]/--/; + my ($start, $end) = + $optval =~ m/\A (?: (-? [0-9]+)? - )? (-? [0-9]+)? \z/ or die( "Value \"$optval\" invalid for option limit", " (range expected)\n" ); + $start ||= 1; + $start--; + s/\A-0*\z// and $_ ||= undef for $end // (); + + $opt{hidemin} = sub { + my ($lines) = @_; + if ($start < 0) { + return max(0, $lines + $start + 2); + } + return $start; + } if $start; + $opt{hidemax} = sub { + my ($limit, $offset) = @_; + if ($end < 0) { + return $offset - $end - 1; # count + } + elsif ($start < 0) { + return $limit - $end + 1; # bottom + } + elsif ($end <= $limit) { + return $end - 1; # less + } + return $limit; + } if defined $end; }, + 'log|e!', 'header!', 'markers|m=s', 'graph-format=s' => sub { @@ -89,6 +113,7 @@ GetOptions(\%opt, }; }, 'stat|s!', + 'report=s', 'signal-stat=s', 'unmodified|u!', 'width|w=i', @@ -114,18 +139,28 @@ $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark}; $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'] +$opt{units} = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m'] if $opt{'human-readable'}; $opt{anchor} //= qr/\A/; $opt{'value-length'} = 4 if $opt{units}; $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{report} //= join('', + '${partsum+; $_ .= " of "}', + '${sum+; color(1); $_ .= " total in "}', + '${count#} values', + '${lines#; $_ = $_ != @order && " over $_ lines"}', + sprintf('${count: (%s)}', join ', ', + '${min; color(31)} min', + '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg', + '${max; color(32)} max', + ), +); $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}; @@ -173,9 +208,8 @@ 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; +my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish +my $valmatch = qr< $opt{anchor} ( \h* -? $float |) >x; while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) { s/\r?\n\z//; s/\A\h*// unless $opt{unmodified}; @@ -208,20 +242,10 @@ sub color { sub show_lines { -state $nr = - $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) : - $opt{hidemin}; +state $nr = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0; @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; - } -} +my $limit = $opt{hidemax} ? $opt{hidemax}->($#lines, $nr) : $#lines; @order = sort { $b <=> $a } @order unless tied @order; my $maxval = $opt{maxval} // ( @@ -230,51 +254,65 @@ my $maxval = $opt{maxval} // ( ) // 0; my $minval = $opt{minval} // min $order[-1] // (), 0; my $range = $maxval - $minval; +$range &&= log $range if $opt{log}; 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 + max(map { length $values[$_] && length $lines[$_] } $nr .. $limit) + // 0; # left padding my $size = defined $opt{width} && $range && - ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication + ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication my @barmark; if ($opt{markers} and $size > 0) { for my $markspec (split /\h/, $opt{markers}) { my ($char, $func) = split //, $markspec, 2; - my $pos = eval { + my @pos = eval { if ($func eq 'avg') { return sum(@order) / @order; } elsif ($func =~ /\A([0-9.]+)v\z/) { - die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100; + $1 <= 100 or die( + "Invalid marker $char: percentile $1 out of bounds\n" + ); my $index = $#order * $1 / 100; return ($order[$index] + $order[$index + .5]) / 2; } elsif ($func =~ /\A-?[0-9.]+\z/) { return $func; } + elsif ($func =~ /\A\/($float)\z/) { + my @range = my $multiple = my $next = $1; + while ($next < $maxval) { + $multiple *= 10 if $opt{log}; + push @range, $next += $multiple; + } + return @range; + } else { die "Unknown marker $char: $func\n"; } }; - defined $pos or do { + @pos or do { warn $@ if $@; next; }; - $pos -= $minval; - $pos >= 0 or next; - color(36) for $barmark[$pos * $size] = $char; + for my $pos (@pos) { + $pos -= $minval; + $pos &&= log $pos if $opt{log}; + $pos >= 0 or next; + color(36) for $barmark[$pos / $range * $size] = $char; + } } state $lastmax = $maxval; if ($maxval > $lastmax) { print ' ' x ($lenval + $len); - printf color(90); + print color(90); printf '%-*s', - ($lastmax - $minval) * $size + .5, - '-' x (($values[$nr - 1] - $minval) * $size); + ($lastmax - $minval) * $size / $range + .5, + '-' x (($values[$nr - 1] - $minval) * $size / $range); print color(92); - say '+' x (($range - $lastmax) * $size + .5); + say '+' x (($range - $lastmax) * $size / $range + .5); print color(0); $lastmax = $maxval; } @@ -283,14 +321,19 @@ if ($opt{markers} and $size > 0) { say( color(31), sprintf('%*s', $lenval, $minval), color(90), '-', color(36), '+', - color(32), sprintf('%*s', $size * $range - 3, $maxval), + color(32), sprintf('%*s', $size - 3, $maxval), color(90), '-', color(36), '+', color(0), ) if $opt{header}; while ($nr <= $limit) { my $val = $values[$nr]; - my $rel = length $val && $range && min(1, ($val - $minval) / $range); + my $rel; + if (length $val) { + $rel = $val - $minval; + $rel &&= log $rel if $opt{log}; + $rel = min(1, $rel / $range) if $range; # 0..1 + } my $color = !length $val || !$opt{palette} ? undef : $val == $order[0] ? $opt{palette}->[-1] : # max $val == $order[-1] ? $opt{palette}->[0] : # min @@ -321,8 +364,10 @@ while ($nr <= $limit) { next; } printf '%-*s', $len + length($val), $line; - print $barmark[$_] // $opt{'graph-format'} - for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5; + if ($rel and $size) { + print $barmark[$_] // $opt{'graph-format'} + for 1 .. $rel * $size + .5; + } say ''; } continue { @@ -334,32 +379,49 @@ say $opt{palette} ? color(0) : '' if $opt{spark}; } sub show_stat { - if ($opt{hidemin} or $opt{hidemax}) { - my $linemin = $opt{hidemin}; - my $linemax = ($opt{hidemax} || @lines) - 1; - if ($linemin < 0) { - $linemin += @lines; - $linemax = @lines - $linemax; - } - printf '%.8g of ', $opt{'value-format'}->( - sum(grep {length} @values[$linemin .. $linemax]) // 0 - ); - } + my %vars = ( + count => int @order, + lines => int @lines, + ); + my $linemin = !$opt{hidemin} ? 0 : + ($vars{start} = $opt{hidemin}->($#lines)); + my $linemax = !$opt{hidemax} ? $#lines : + ($vars{end} = $opt{hidemax}->($#lines, $vars{start})); if (@order) { - my $total = sum @order; - printf '%s total', color(1) . $opt{'value-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)', - color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0), - color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0), - color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0), + $vars{partsum} = sum(0, grep {length} @values[$linemin .. $linemax]) + if $linemin <= $linemax and ($opt{hidemin} or $opt{hidemax}); + %vars = (%vars, + sum => sum(@order), + min => $order[-1], + max => $order[0], ); + $vars{avg} = $vars{sum} / @order; } - say ''; + say varfmt($opt{report}, \%vars); return 1; } +sub varfmt { + my ($fmt, $vars) = @_; + $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{ + my ($name, $op, $cmd) = split /\s*([;:])/, $1, 2; + my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat}; + local $_ = $vars->{$name}; + defined && do { + $_ = $opt{'value-format'}->($_) if $format; + if ($cmd and $op eq ':') { + $_ = varfmt($cmd, $vars); + } + elsif ($cmd) { + eval $cmd; + warn "Error in \$$name report: $@" if $@; + } + $_; + } + }eg; + return $fmt; +} + sub show_exit { show_lines(); show_stat() if $opt{stat}; @@ -387,8 +449,9 @@ Options: 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 + -L, --limit=[N|[-]START(-[END]|+N)] + Select a range of lines to display + -e, --log Logarithmic (exponential) scale instead of linear --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 @@ -489,16 +552,23 @@ unless C<--length=0>. Prepend a dash (i.e. make negative) to enforce padding regardless of encountered contents. -=item -L, --limit[=( | - | -[])] +=item -L, --limit=[ | [-](-[] | +)] -Stop output after a number of lines. -A single value indicates the last line number (like C), +Select a range of lines to display. +A single integer indicates the last line number (like C), or first line counting from the bottom if negative (like C). -A specific range can be given by two values. -All input is still counted and analyzed for statistics, +A range consists of a starting line number followed by either +a dash C<-> to an optional end, or plus sign C<+> with count. + +All hidden input is still counted and analyzed for statistics, but disregarded for padding and bar size. +=item -e, --log + +Logarithmic (Ixponential) scale instead of linear +to compare orders of magnitude. + =item --graph-format= Glyph to repeat for the graph line. @@ -516,7 +586,12 @@ A single indicator glyph precedes each position: Exact value to match on the axis. A vertical bar at the zero crossing is displayed by I<|0> for negative values. -For example I<:3.14> would show a colon at pi. +For example I<π3.14> would locate pi. + +=item I + +Repeated at every multiple of a number. +For example I<:/1> for a grid at every integer. =item I @@ -573,6 +648,10 @@ Unspecified, block fill glyphs U+2581-2588 will be used. Total statistics after all data. +While processing (possibly a neverending pipe), +intermediate results are also shown on signal I if available (control+t on BSDs) +or I otherwise (ctrl+\ on linux). + =item -u, --unmodified Do not reformat values, keeping leading whitespace. @@ -691,12 +770,12 @@ Total population history in XML from the World Bank: 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 + barcat -f1 -H --markers=+/1e9 Population and other information for all countries: curl http://download.geonames.org/export/dump/countryInfo.txt | - grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -u -l150 -s + grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s And of course various Git statistics, such commit count by year: