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;
60 fire => [qw( 90 31 91 33 93 97 96 )],
61 fire88 => [map {"38;5;$_"} qw(
62 80 32 48 64 68 72 76 77 78 79 47
64 fire256=> [map {"38;5;$_"} qw(
66 202 208 214 220 226 227 228 229 230 231 159
68 ramp88 => [map {"38;5;$_"} qw(
69 64 65 66 67 51 35 39 23 22 26 25 28
71 whites => [qw( 1;30 0;37 1;37 )],
72 greys => [map {"38;5;$_"} 52, 235..255, 47],
73 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
80 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
81 say "barcat $mascot version $VERSION";
85 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
90 Pod::Usage::pod2usage(
91 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
94 ) or exit 64; # EX_USAGE
97 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
98 $opt{color} //= -t *STDOUT; # enable on tty
99 $opt{'graph-format'} //= '-';
100 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
101 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
102 if $opt{'human-readable'};
103 $opt{anchor} //= qr/\A/;
104 $opt{'value-length'} = 6 if $opt{units};
105 $opt{'value-length'} = 1 if $opt{unmodified};
106 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
107 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
108 $opt{palette} //= $opt{color} && [31, 90, 32];
109 $opt{indicators} = [split //,
110 $opt{indicators} || ($opt{ascii} ? ' .oO' : ' ▁▂▃▄▅▆▇█')
111 ] if defined $opt{indicators} or $opt{spark};
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 ? max(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;
206 elsif ($opt{hidemax} <= $limit) {
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 - !!$opt{indicators}) / $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 ];
283 my $indicator = $opt{indicators} && $opt{indicators}->[
284 !$val || !$#{$opt{indicators}} ? 0 : # blank
285 $#{$opt{indicators}} < 2 ? 1 :
286 $val >= $order[0] ? -1 :
287 $rel * ($#{$opt{indicators}} - 1e-14) + 1
291 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
292 print color($color), $_ for $indicator;
295 print $indicator if defined $indicator;
298 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
299 sprintf "%*s", $lenval, $val;
300 color($color) for $val;
302 my $line = $lines[$nr] =~ s/\n/$val/r;
303 if (not length $val) {
307 printf '%-*s', $len + length($val), $line;
308 print $barmark[$_] // $opt{'graph-format'}
309 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
315 say $opt{palette} ? color(0) : '' if $opt{spark};
321 if ($opt{hidemin} or $opt{hidemax}) {
322 my $linemin = $opt{hidemin};
323 my $linemax = ($opt{hidemax} || @lines) - 1;
326 $linemax = @lines - $linemax;
328 printf '%.8g of ', $opt{'sum-format'}->(
329 sum(grep {length} @values[$linemin .. $linemax]) // 0
333 my $total = sum @order;
334 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
335 printf ' in %d values', scalar @order;
336 printf ' over %d lines', scalar @lines if @order != @lines;
337 printf(' (%s min, %s avg, %s max)',
338 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
339 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
340 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
349 show_stat() if $opt{stat};
350 exit 130 if @_; # 0x80+signo
358 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
361 -a, --[no-]ascii Restrict user interface to ASCII characters
362 -c, --[no-]color Force colored output of values and bar markers
363 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
365 --header Prepend a chart axis with minimum and maximum
367 -H, --human-readable Format values using SI unit prefixes
368 -t, --interval[=(N|-LINES)]
369 Output partial progress every given number of
370 seconds or input lines
371 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
372 -L, --limit[=(N|-LAST|START-[END])]
373 Stop output after a number of lines
374 --graph-format=CHAR Glyph to repeat for the graph line
375 -m, --markers=FORMAT Statistical positions to indicate on bars
376 --min=N, --max=N Bars extend from 0 or the minimum value if lower
377 --palette=(PRESET|COLORS)
378 Override colors of parsed numbers
379 -_, --spark Replace lines by sparklines
380 --indicators[=CHARS] Prefix a unicode character corresponding to each
382 -s, --stat Total statistics after all data
383 -u, --unmodified Do not reformat values, keeping leading whitespace
384 --value-length=SIZE Reserved space for numbers
385 -w, --width=COLUMNS Override the maximum number of columns to use
386 -h, --usage Overview of available options
387 --help Full pod documentation
388 -V, --version Version information
394 barcat - concatenate texts with graph to visualize values
398 B<barcat> [<options>] [<file>... | <numbers>]
402 Visualizes relative sizes of values read from input
403 (parameters, file(s) or STDIN).
404 Contents are concatenated similar to I<cat>,
405 but numbers are reformatted and a bar graph is appended to each line.
407 Don't worry, barcat does not drink and divide.
408 It can has various options for input and output (re)formatting,
409 but remains limited to one-dimensional charts.
410 For more complex graphing needs
411 you'll need a larger animal like I<gnuplot>.
417 =item -a, --[no-]ascii
419 Restrict user interface to ASCII characters,
420 replacing default UTF-8 by their closest approximation.
421 Input is always interpreted as UTF-8 and shown as is.
423 =item -c, --[no-]color
425 Force colored output of values and bar markers.
426 Defaults on if output is a tty,
427 disabled otherwise such as when piped or redirected.
429 =item -f, --field=(<number> | <regexp>)
431 Compare values after a given number of whitespace separators,
432 or matching a regular expression.
434 Unspecified or I<-f0> means values are at the start of each line.
435 With I<-f1> the second word is taken instead.
436 A string can indicate the starting position of a value
437 (such as I<-f:> if preceded by colons),
438 or capture the numbers itself,
439 for example I<-f'(\d+)'> for the first digits anywhere.
443 Prepend a chart axis with minimum and maximum values labeled.
445 =item -H, --human-readable
447 Format values using SI unit prefixes,
448 turning long numbers like I<12356789> into I<12.4M>.
449 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
450 Short integers are aligned but kept without decimal point.
452 =item -t, --interval[=(<seconds> | -<lines>)]
454 Output partial progress every given number of seconds or input lines.
455 An update can also be forced by sending a I<SIGALRM> alarm signal.
457 =item -l, --length=[-]<size>[%]
459 Trim line contents (between number and bars)
460 to a maximum number of characters.
461 The exceeding part is replaced by an abbreviation sign,
462 unless C<--length=0>.
464 Prepend a dash (i.e. make negative) to enforce padding
465 regardless of encountered contents.
467 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
469 Stop output after a number of lines.
470 A single value indicates the last line number (like C<head>),
471 or first line counting from the bottom if negative (like C<tail>).
472 A specific range can be given by two values.
474 All input is still counted and analyzed for statistics,
475 but disregarded for padding and bar size.
477 =item --graph-format=<character>
479 Glyph to repeat for the graph line.
480 Defaults to a dash C<->.
482 =item -m, --markers=<format>
484 Statistical positions to indicate on bars.
485 A single indicator glyph precedes each position:
491 Exact value to match on the axis.
492 A vertical bar at the zero crossing is displayed by I<|0>
494 For example I<:3.14> would show a colon at pi.
496 =item <percentage>I<v>
498 Ranked value at the given percentile.
499 The default shows I<+> at I<50v> for the mean or median;
500 the middle value or average between middle values.
501 One standard deviation right of the mean is at about I<68.3v>.
502 The default includes I<< >31.73v <68.27v >>
503 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
508 the sum of all values divided by the number of counted lines.
509 Indicated by default as I<=>.
513 =item --min=<number>, --max=<number>
515 Bars extend from 0 or the minimum value if lower,
516 to the largest value encountered.
517 These options can be set to customize this range.
519 =item --palette=(<preset> | <color>...)
521 Override colors of parsed numbers.
522 Can be any CSI escape, such as I<90> for default dark grey,
523 or alternatively I<1;30> for bright black.
525 In case of additional colors,
526 the last is used for values equal to the maximum, the first for minima.
527 If unspecified, these are green and red respectively (I<31 90 32>).
528 Multiple intermediate colors will be distributed
529 relative to the size of values.
531 Predefined color schemes are named I<whites> and I<fire>,
532 or I<greys> and I<fire256> for 256-color variants.
536 Replace lines by I<sparklines>,
537 single characters (configured by C<--indicators>)
538 corresponding to input values.
540 =item --indicators[=<characters>]
542 Prefix a unicode character corresponding to each value.
543 The first specified character will be used for non-values,
544 the remaining sequence will be distributed over the range of values.
545 Unspecified, block fill glyphs U+2581-2588 will be used.
549 Total statistics after all data.
551 =item -u, --unmodified
553 Do not reformat values, keeping leading whitespace.
554 Keep original value alignment, which may be significant in some programs.
556 =item --value-length=<size>
558 Reserved space for numbers.
560 =item -w, --width=<columns>
562 Override the maximum number of columns to use.
563 Appended graphics will extend to fill up the entire screen.
567 Overview of available options.
571 Full pod documentation
572 as rendered by perldoc.
584 seq 30 | awk '{print sin($1/10)}' | barcat
586 Compare file sizes (with human-readable numbers):
588 du -d0 -b * | barcat -H
590 Memory usage of user processes with long names truncated:
592 ps xo rss,pid,cmd | barcat -l40
594 Monitor network latency from prefixed results:
596 ping google.com | barcat -f'time=\K' -t
598 Commonly used after counting, for example users on the current server:
600 users | tr ' ' '\n' | sort | uniq -c | barcat
602 Letter frequencies in text files:
604 cat /usr/share/games/fortunes/*.u8 |
605 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
606 sort | uniq -c | barcat
608 Number of HTTP requests per day:
610 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
612 Any kind of database query with counts, preserving returned alignment:
614 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
617 In PostgreSQL from within the client:
619 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
621 Earthquakes worldwide magnitude 1+ in the last 24 hours:
623 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
624 column -tns, | barcat -f4 -u -l80%
626 External datasets, like movies per year:
628 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
629 jq .[].year | uniq -c | barcat
631 Pokémon height comparison:
633 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
634 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
636 USD/EUR exchange rate from CSV provided by the ECB:
638 curl https://sdw.ecb.europa.eu/export.do \
639 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
640 barcat -f',\K' --value-length=7
642 Total population history in XML from the World Bank:
644 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
645 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
648 And of course various Git statistics, such commit count by year:
650 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
652 Or the top 3 most frequent authors with statistics over all:
654 git shortlog -sn | barcat -L3 -s
656 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
658 ( git log --pretty=%ci --since=30day | cut -b-10
659 seq 0 30 | xargs -i date +%F -d-{}day ) |
660 sort | uniq -c | awk '$1--' | barcat --spark
662 Sparkline graphics of simple input given as inline parameters:
664 barcat -_ 3 1 4 1 5 0 9 2 4
668 Mischa POSLAWSKY <perl@shiar.org>