5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
18 'C' => sub { $opt{color} = 0 },
22 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
27 'trim|length|l=s' => sub {
28 my ($optname, $optval) = @_;
29 $optval =~ s/%$// and $opt{trimpct}++;
30 $optval =~ m/\A-?[0-9]+\z/ or die(
31 "Value \"$optval\" invalid for option $optname",
32 " (number or percentage expected)\n"
42 my ($optname, $optval) = @_;
44 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
45 ($opt{hidemin}, $opt{hidemax}) =
46 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
47 "Value \"$optval\" invalid for option limit",
53 'graph-format=s' => sub {
54 $opt{'graph-format'} = substr $_[1], 0, 1;
57 $opt{spark} = [split //,
58 $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
63 fire => [qw( 90 31 91 33 93 97 96 )],
64 fire88 => [map {"38;5;$_"} qw(
65 80 32 48 64 68 72 76 77 78 79 47
67 fire256=> [map {"38;5;$_"} qw(
69 202 208 214 220 226 227 228 229 230 231 159
71 ramp88 => [map {"38;5;$_"} qw(
72 64 65 66 67 51 35 39 23 22 26 25 28
74 whites => [qw( 1;30 0;37 1;37 )],
75 greys => [map {"38;5;$_"} 52, 235..255, 47],
76 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
83 say "barcat version $VERSION";
87 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
92 Pod::Usage::pod2usage(
93 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
96 ) or exit 64; # EX_USAGE
99 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
100 $opt{color} //= -t *STDOUT; # enable on tty
101 $opt{'graph-format'} //= '-';
102 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
103 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
104 if $opt{'human-readable'};
105 $opt{anchor} //= qr/\A/;
106 $opt{'value-length'} = 6 if $opt{units};
107 $opt{'value-length'} = 1 if $opt{unmodified};
108 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
109 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
110 $opt{palette} //= $opt{color} && [31, 90, 32];
111 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
112 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
113 and undef $opt{interval};
115 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
116 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
117 $opt{'value-format'} = $opt{units} && sub {
119 log(abs $_[0] || 1) / log(10)
120 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
121 + 1e-15 # float imprecision
123 my $decimal = ($unit % 3) == ($unit < 0);
124 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
125 $decimal = ($unit % 3) == ($unit < 0);
126 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
128 3 + ($_[0] < 0), # digits plus optional negative sign
130 $_[0] / 1000 ** int($unit/3), # number
131 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
132 $opt{units}->[$unit/3] # suffix
137 my (@lines, @values, @order);
139 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
142 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
144 $SIG{INT} = \&show_exit;
146 if (defined $opt{interval}) {
147 $opt{interval} ||= 1;
148 alarm $opt{interval} if $opt{interval} > 0;
151 require Tie::Array::Sorted;
152 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
153 } or warn $@, "Expect slowdown with large datasets!\n";
157 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
159 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
161 s/\A\h*// unless $opt{unmodified};
162 my $valnum = s/$valmatch/\n/ && $1;
163 push @values, $valnum;
164 push @order, $valnum if length $valnum;
165 if (defined $opt{trim} and defined $valnum) {
166 my $trimpos = abs $opt{trim};
167 $trimpos -= length $valnum if $opt{unmodified};
169 $_ = substr $_, 0, 2;
171 elsif (length > $trimpos) {
172 # cut and replace (intentional lvalue for speed, contrary to PBP)
173 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
177 show_lines() if defined $opt{interval} and $opt{interval} < 0
178 and $. % $opt{interval} == 0;
181 if ($opt{'zero-missing'}) {
182 push @values, (0) x 10;
185 $SIG{INT} = 'DEFAULT';
188 $opt{color} and defined $_[0] or return '';
189 return "\e[$_[0]m" if defined wantarray;
190 $_ = color(@_) . $_ . color(0) if defined;
196 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
198 @lines > $nr or return;
201 if (defined $opt{hidemax}) {
202 if ($opt{hidemin} and $opt{hidemin} < 0) {
203 $limit -= $opt{hidemax} - 1;
206 $limit = $opt{hidemax} - 1;
210 @order = sort { $b <=> $a } @order unless tied @order;
211 my $maxval = $opt{maxval} // (
212 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
215 my $minval = $opt{minval} // min $order[-1] // (), 0;
216 my $range = $maxval - $minval;
217 my $lenval = $opt{'value-length'} // max map { length } @order;
218 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
219 max map { length $values[$_] && length $lines[$_] }
220 0 .. min $#lines, $opt{hidemax} || (); # left padding
221 my $size = defined $opt{width} && $range &&
222 ($opt{width} - $lenval - $len) / $range; # bar multiplication
225 if ($opt{markers} and $size > 0) {
226 for my $markspec (split /\h/, $opt{markers}) {
227 my ($char, $func) = split //, $markspec, 2;
229 if ($func eq 'avg') {
230 return sum(@order) / @order;
232 elsif ($func =~ /\A([0-9.]+)v\z/) {
233 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
234 my $index = $#order * $1 / 100;
235 return ($order[$index] + $order[$index + .5]) / 2;
237 elsif ($func =~ /\A-?[0-9.]+\z/) {
241 die "Unknown marker $char: $func\n";
250 color(36) for $barmark[$pos * $size] = $char;
253 state $lastmax = $maxval;
254 if ($maxval > $lastmax) {
255 print ' ' x ($lenval + $len);
258 ($lastmax - $minval) * $size + .5,
259 '-' x (($values[$nr - 1] - $minval) * $size);
261 say '+' x (($range - $lastmax) * $size + .5);
268 color(31), sprintf('%*s', $lenval, $minval),
269 color(90), '-', color(36), '+',
270 color(32), sprintf('%*s', $size * $range - 3, $maxval),
271 color(90), '-', color(36), '+',
275 while ($nr <= $limit) {
276 my $val = $values[$nr];
277 my $rel = length $val && $range && ($val - $minval) / $range;
278 my $color = !length $val || !$opt{palette} ? undef :
279 $val == $order[0] ? $opt{palette}->[-1] : # max
280 $val == $order[-1] ? $opt{palette}->[0] : # min
281 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
284 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
285 print color($color), $opt{spark}->[
286 !$val || !$#{$opt{spark}} ? 0 : # blank
287 $val == $order[0] ? -1 : # max
288 $val == $order[-1] ? 1 : # min
289 $#{$opt{spark}} < 3 ? 1 :
290 $rel * ($#{$opt{spark}} - 3) + 2.5
296 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
297 sprintf "%*s", $lenval, $val;
298 color($color) for $val;
300 my $line = $lines[$nr] =~ s/\n/$val/r;
301 if (not length $val) {
305 printf '%-*s', $len + length($val), $line;
306 print $barmark[$_] // $opt{'graph-format'}
307 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
313 say $opt{palette} ? color(0) : '' if $opt{spark};
319 if ($opt{hidemin} or $opt{hidemax}) {
320 my $linemin = $opt{hidemin};
321 my $linemax = ($opt{hidemax} || @lines) - 1;
324 $linemax = @lines - $linemax;
326 printf '%.8g of ', $opt{'sum-format'}->(
327 sum(grep {length} @values[$linemin .. $linemax]) // 0
331 my $total = sum @order;
332 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
333 printf ' in %d values', scalar @order;
334 printf ' over %d lines', scalar @lines if @order != @lines;
335 printf(' (%s min, %s avg, %s max)',
336 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
337 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
338 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
347 show_stat() if $opt{stat};
348 exit 130 if @_; # 0x80+signo
356 barcat [OPTIONS] [FILES|NUMBERS]
359 -a, --[no-]ascii Restrict user interface to ASCII characters
360 -c, --[no-]color Force colored output of values and bar markers
361 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
363 --header Prepend a chart axis with minimum and maximum
365 -H, --human-readable Format values using SI unit prefixes
366 -t, --interval[=(N|-LINES)]
367 Output partial progress every given number of
368 seconds or input lines
369 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
370 -L, --limit[=(N|-LAST|START-[END])]
371 Stop output after a number of lines
372 --graph-format=CHAR Glyph to repeat for the graph line
373 -m, --markers=FORMAT Statistical positions to indicate on bars
374 --min=N, --max=N Bars extend from 0 or the minimum value if lower
375 --palette=(PRESET|COLORS)
376 Override colors of parsed numbers
377 --spark[=CHARS] Replace lines by sparklines
378 -s, --stat Total statistics after all data
379 -u, --unmodified Do not reformat values, keeping leading whitespace
380 --value-length=SIZE Reserved space for numbers
381 -w, --width=COLUMNS Override the maximum number of columns to use
382 -h, --usage Overview of available options
383 --help Full documentation
384 --version Version information
390 barcat - graph to visualize input values
394 B<barcat> [<options>] [<file>... | <numbers>]
398 Visualizes relative sizes of values read from input
399 (parameters, file(s) or STDIN).
400 Contents are concatenated similar to I<cat>,
401 but numbers are reformatted and a bar graph is appended to each line.
403 Don't worry, barcat does not drink and divide.
404 It can has various options for input and output (re)formatting,
405 but remains limited to one-dimensional charts.
406 For more complex graphing needs
407 you'll need a larger animal like I<gnuplot>.
413 =item -a, --[no-]ascii
415 Restrict user interface to ASCII characters,
416 replacing default UTF-8 by their closest approximation.
417 Input is always interpreted as UTF-8 and shown as is.
419 =item -c, --[no-]color
421 Force colored output of values and bar markers.
422 Defaults on if output is a tty,
423 disabled otherwise such as when piped or redirected.
425 =item -f, --field=(<number> | <regexp>)
427 Compare values after a given number of whitespace separators,
428 or matching a regular expression.
430 Unspecified or I<-f0> means values are at the start of each line.
431 With I<-f1> the second word is taken instead.
432 A string can indicate the starting position of a value
433 (such as I<-f:> if preceded by colons),
434 or capture the numbers itself,
435 for example I<-f'(\d+)'> for the first digits anywhere.
439 Prepend a chart axis with minimum and maximum values labeled.
441 =item -H, --human-readable
443 Format values using SI unit prefixes,
444 turning long numbers like I<12356789> into I<12.4M>.
445 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
446 Short integers are aligned but kept without decimal point.
448 =item -t, --interval[=(<seconds> | -<lines>)]
450 Output partial progress every given number of seconds or input lines.
451 An update can also be forced by sending a I<SIGALRM> alarm signal.
453 =item -l, --length=[-]<size>[%]
455 Trim line contents (between number and bars)
456 to a maximum number of characters.
457 The exceeding part is replaced by an abbreviation sign,
458 unless C<--length=0>.
460 Prepend a dash (i.e. make negative) to enforce padding
461 regardless of encountered contents.
463 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
465 Stop output after a number of lines.
466 A single value indicates the last line number (like C<head>),
467 or first line counting from the bottom if negative (like C<tail>).
468 A specific range can be given by two values.
470 All input is still counted and analyzed for statistics,
471 but disregarded for padding and bar size.
473 =item --graph-format=<character>
475 Glyph to repeat for the graph line.
476 Defaults to a dash C<->.
478 =item -m, --markers=<format>
480 Statistical positions to indicate on bars.
481 A single indicator glyph precedes each position:
487 Exact value to match on the axis.
488 A vertical bar at the zero crossing is displayed by I<|0>
490 For example I<:3.14> would show a colon at pi.
492 =item <percentage>I<v>
494 Ranked value at the given percentile.
495 The default shows I<+> at I<50v> for the mean or median;
496 the middle value or average between middle values.
497 One standard deviation right of the mean is at about I<68.3v>.
498 The default includes I<< >31.73v <68.27v >>
499 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
504 the sum of all values divided by the number of counted lines.
505 Indicated by default as I<=>.
509 =item --min=<number>, --max=<number>
511 Bars extend from 0 or the minimum value if lower,
512 to the largest value encountered.
513 These options can be set to customize this range.
515 =item --palette=(<preset> | <color>...)
517 Override colors of parsed numbers.
518 Can be any CSI escape, such as I<90> for default dark grey,
519 or alternatively I<1;30> for bright black.
521 In case of additional colors,
522 the last is used for values equal to the maximum, the first for minima.
523 If unspecified, these are green and red respectively (I<31 90 32>).
524 Multiple intermediate colors will be distributed
525 relative to the size of values.
527 Predefined color schemes are named I<whites> and I<fire>,
528 or I<greys> and I<fire256> for 256-color variants.
530 =item --spark[=<characters>]
532 Replace lines by I<sparklines>,
533 single characters corresponding to input values.
534 A specified sequence of unicode characters will be used for
535 Of a specified sequence of unicode characters,
536 the first one will be used for non-values,
537 the last one for the maximum,
538 the second (if any) for the minimum,
539 and any remaining will be distributed over the range of values.
540 Unspecified, block fill glyphs U+2581-2588 will be used.
544 Total statistics after all data.
546 =item -u, --unmodified
548 Do not reformat values, keeping leading whitespace.
549 Keep original value alignment, which may be significant in some programs.
551 =item --value-length=<size>
553 Reserved space for numbers.
555 =item -w, --width=<columns>
557 Override the maximum number of columns to use.
558 Appended graphics will extend to fill up the entire screen.
562 Overview of available options.
579 seq 30 | awk '{print sin($1/10)}' | barcat
581 Compare file sizes (with human-readable numbers):
583 du -d0 -b * | barcat -H
585 Memory usage of user processes with long names truncated:
587 ps xo %mem,pid,cmd | barcat -l40
589 Monitor network latency from prefixed results:
591 ping google.com | barcat -f'time=\K' -t
593 Commonly used after counting, for example users on the current server:
595 users | tr ' ' '\n' | sort | uniq -c | barcat
597 Letter frequencies in text files:
599 cat /usr/share/games/fortunes/*.u8 |
600 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
601 sort | uniq -c | barcat
603 Number of HTTP requests per day:
605 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
607 Any kind of database query with counts, preserving returned alignment:
609 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
612 In PostgreSQL from within the client:
614 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
616 Earthquakes worldwide magnitude 1+ in the last 24 hours:
618 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
619 column -tns, | barcat -f4 -u -l80%
621 External datasets, like movies per year:
623 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
624 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
626 But please get I<jq> to process JSON
627 and replace the manual selection by C<< jq '.[].year' >>.
629 Pokémon height comparison:
631 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
632 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
634 USD/EUR exchange rate from CSV provided by the ECB:
636 curl https://sdw.ecb.europa.eu/export.do \
637 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
638 grep '^[12]' | barcat -f',\K' --value-length=7
640 Total population history in XML from the World Bank:
642 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
643 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
644 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
646 And of course various Git statistics, such commit count by year:
648 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
650 Or the top 3 most frequent authors with statistics over all:
652 git shortlog -sn | barcat -L3 -s
654 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
656 ( git log --pretty=%ci --since=30day | cut -b-10
657 seq 0 30 | xargs -i date +%F -d-{}day ) |
658 sort | uniq -c | awk '$1--' | barcat --spark
660 Sparkline graphics of simple input given as inline parameters:
662 barcat --spark= 3 1 4 1 5 0 9 2 4
666 Mischa POSLAWSKY <perl@shiar.org>