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} ? ' .oO' : ' ▁▂▃▄▅▆▇█')
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 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
84 say "barcat $mascot version $VERSION";
88 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
93 Pod::Usage::pod2usage(
94 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
97 ) or exit 64; # EX_USAGE
100 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
101 $opt{color} //= -t *STDOUT; # enable on tty
102 $opt{'graph-format'} //= '-';
103 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
104 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
105 if $opt{'human-readable'};
106 $opt{anchor} //= qr/\A/;
107 $opt{'value-length'} = 6 if $opt{units};
108 $opt{'value-length'} = 1 if $opt{unmodified};
109 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
110 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
111 $opt{palette} //= $opt{color} && [31, 90, 32];
112 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
113 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
114 and undef $opt{interval};
116 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
117 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
118 $opt{'value-format'} = $opt{units} && sub {
120 log(abs $_[0] || 1) / log(10)
121 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
122 + 1e-15 # float imprecision
124 my $decimal = ($unit % 3) == ($unit < 0);
125 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
126 $decimal = ($unit % 3) == ($unit < 0);
127 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
129 3 + ($_[0] < 0), # digits plus optional negative sign
131 $_[0] / 1000 ** int($unit/3), # number
132 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
133 $opt{units}->[$unit/3] # suffix
138 my (@lines, @values, @order);
140 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
143 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
145 $SIG{INT} = \&show_exit;
147 if (defined $opt{interval}) {
148 $opt{interval} ||= 1;
149 alarm $opt{interval} if $opt{interval} > 0;
152 require Tie::Array::Sorted;
153 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
154 } or warn $@, "Expect slowdown with large datasets!\n";
158 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
160 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
162 s/\A\h*// unless $opt{unmodified};
163 my $valnum = s/$valmatch/\n/ && $1;
164 push @values, $valnum;
165 push @order, $valnum if length $valnum;
166 if (defined $opt{trim} and defined $valnum) {
167 my $trimpos = abs $opt{trim};
168 $trimpos -= length $valnum if $opt{unmodified};
170 $_ = substr $_, 0, 2;
172 elsif (length > $trimpos) {
173 # cut and replace (intentional lvalue for speed, contrary to PBP)
174 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
178 show_lines() if defined $opt{interval} and $opt{interval} < 0
179 and $. % $opt{interval} == 0;
182 if ($opt{'zero-missing'}) {
183 push @values, (0) x 10;
186 $SIG{INT} = 'DEFAULT';
189 $opt{color} and defined $_[0] or return '';
190 return "\e[$_[0]m" if defined wantarray;
191 $_ = color(@_) . $_ . color(0) if defined;
197 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
199 @lines > $nr or return;
202 if (defined $opt{hidemax}) {
203 if ($opt{hidemin} and $opt{hidemin} < 0) {
204 $limit -= $opt{hidemax} - 1;
207 $limit = $opt{hidemax} - 1;
211 @order = sort { $b <=> $a } @order unless tied @order;
212 my $maxval = $opt{maxval} // (
213 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
216 my $minval = $opt{minval} // min $order[-1] // (), 0;
217 my $range = $maxval - $minval;
218 my $lenval = $opt{'value-length'} // max map { length } @order;
219 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
220 max map { length $values[$_] && length $lines[$_] }
221 0 .. min $#lines, $opt{hidemax} || (); # left padding
222 my $size = defined $opt{width} && $range &&
223 ($opt{width} - $lenval - $len) / $range; # bar multiplication
226 if ($opt{markers} and $size > 0) {
227 for my $markspec (split /\h/, $opt{markers}) {
228 my ($char, $func) = split //, $markspec, 2;
230 if ($func eq 'avg') {
231 return sum(@order) / @order;
233 elsif ($func =~ /\A([0-9.]+)v\z/) {
234 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
235 my $index = $#order * $1 / 100;
236 return ($order[$index] + $order[$index + .5]) / 2;
238 elsif ($func =~ /\A-?[0-9.]+\z/) {
242 die "Unknown marker $char: $func\n";
251 color(36) for $barmark[$pos * $size] = $char;
254 state $lastmax = $maxval;
255 if ($maxval > $lastmax) {
256 print ' ' x ($lenval + $len);
259 ($lastmax - $minval) * $size + .5,
260 '-' x (($values[$nr - 1] - $minval) * $size);
262 say '+' x (($range - $lastmax) * $size + .5);
269 color(31), sprintf('%*s', $lenval, $minval),
270 color(90), '-', color(36), '+',
271 color(32), sprintf('%*s', $size * $range - 3, $maxval),
272 color(90), '-', color(36), '+',
276 while ($nr <= $limit) {
277 my $val = $values[$nr];
278 my $rel = length $val && $range && ($val - $minval) / $range;
279 my $color = !length $val || !$opt{palette} ? undef :
280 $val == $order[0] ? $opt{palette}->[-1] : # max
281 $val == $order[-1] ? $opt{palette}->[0] : # min
282 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
285 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
286 print color($color), $opt{spark}->[
287 !$val || !$#{$opt{spark}} ? 0 : # blank
288 $#{$opt{spark}} < 2 ? 1 :
289 $val >= $order[0] ? -1 :
290 $rel * ($#{$opt{spark}} - 1e-14) + 1
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 pod documentation
384 -V, --version Version information
390 barcat - concatenate texts with graph to visualize 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 Of a specified sequence of unicode characters,
535 the first one will be used for non-values,
536 the remainder will be distributed over the range of values.
537 Unspecified, block fill glyphs U+2581-2588 will be used.
541 Total statistics after all data.
543 =item -u, --unmodified
545 Do not reformat values, keeping leading whitespace.
546 Keep original value alignment, which may be significant in some programs.
548 =item --value-length=<size>
550 Reserved space for numbers.
552 =item -w, --width=<columns>
554 Override the maximum number of columns to use.
555 Appended graphics will extend to fill up the entire screen.
559 Overview of available options.
563 Full pod documentation
564 as rendered by perldoc.
576 seq 30 | awk '{print sin($1/10)}' | barcat
578 Compare file sizes (with human-readable numbers):
580 du -d0 -b * | barcat -H
582 Memory usage of user processes with long names truncated:
584 ps xo %mem,pid,cmd | barcat -l40
586 Monitor network latency from prefixed results:
588 ping google.com | barcat -f'time=\K' -t
590 Commonly used after counting, for example users on the current server:
592 users | tr ' ' '\n' | sort | uniq -c | barcat
594 Letter frequencies in text files:
596 cat /usr/share/games/fortunes/*.u8 |
597 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
598 sort | uniq -c | barcat
600 Number of HTTP requests per day:
602 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
604 Any kind of database query with counts, preserving returned alignment:
606 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
609 In PostgreSQL from within the client:
611 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
613 Earthquakes worldwide magnitude 1+ in the last 24 hours:
615 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
616 column -tns, | barcat -f4 -u -l80%
618 External datasets, like movies per year:
620 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
621 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
623 But please get I<jq> to process JSON
624 and replace the manual selection by C<< jq '.[].year' >>.
626 Pokémon height comparison:
628 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
629 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
631 USD/EUR exchange rate from CSV provided by the ECB:
633 curl https://sdw.ecb.europa.eu/export.do \
634 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
635 grep '^[12]' | barcat -f',\K' --value-length=7
637 Total population history in XML from the World Bank:
639 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
640 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
641 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
643 And of course various Git statistics, such commit count by year:
645 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
647 Or the top 3 most frequent authors with statistics over all:
649 git shortlog -sn | barcat -L3 -s
651 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
653 ( git log --pretty=%ci --since=30day | cut -b-10
654 seq 0 30 | xargs -i date +%F -d-{}day ) |
655 sort | uniq -c | awk '$1--' | barcat --spark
657 Sparkline graphics of simple input given as inline parameters:
659 barcat --spark= 3 1 4 1 5 0 9 2 4
663 Mischa POSLAWSKY <perl@shiar.org>