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 'M' => 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;
61 fire => [qw( 90 31 91 33 93 97 96 )],
62 fire256=> [map {"38;5;$_"} qw(
64 202 208 214 220 226 227 228 229 230 231 159
66 whites => [qw( 1;30 0;37 1;37 )],
67 greys => [map {"38;5;$_"} 0, 232..255, 15],
68 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
69 rainbow=> [map {"38;5;$_"}
71 (map { 196 + $_*6 } 0..4), # +g
72 (map { 226 - $_*6*6 } 0..4), # -r
73 (map { 46 + $_ } 0..4), # +b
74 (map { 51 - $_*6 } 0..4), # -g
75 (map { 21 + $_*6*6 } 0..4), # +r
76 (map { 201 - $_ } 0..4), # -b
80 my @vals = split /[^0-9;]/, $_[1]
81 or die "Empty palette resulting from \"$_[1]\"\n";
90 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
91 say "barcat $mascot version $VERSION";
95 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
100 Pod::Usage::pod2usage(
101 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
104 ) or exit 64; # EX_USAGE
107 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
108 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
109 $opt{'graph-format'} //= '-';
110 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
111 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
112 if $opt{'human-readable'};
113 $opt{anchor} //= qr/\A/;
114 $opt{'value-length'} = 6 if $opt{units};
115 $opt{'value-length'} = 1 if $opt{unmodified};
116 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
117 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
118 $opt{palette} //= $opt{color} && [31, 90, 32];
119 $opt{indicators} = [split //, $opt{indicators} ||
120 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
121 ] if defined $opt{indicators} or $opt{spark};
122 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
123 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
124 and undef $opt{interval};
126 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
127 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
128 $opt{'value-format'} = $opt{units} && sub {
130 log(abs $_[0] || 1) / log(10)
131 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
132 + 1e-15 # float imprecision
134 my $decimal = ($unit % 3) == ($unit < 0);
135 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
136 $decimal = ($unit % 3) == ($unit < 0);
137 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
139 3 + ($_[0] < 0), # digits plus optional negative sign
141 $_[0] / 1000 ** int($unit/3), # number
142 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
143 $opt{units}->[$unit/3] # suffix
148 my (@lines, @values, @order);
150 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
153 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
155 $SIG{INT} = \&show_exit;
157 if (defined $opt{interval}) {
158 $opt{interval} ||= 1;
159 alarm $opt{interval} if $opt{interval} > 0;
162 require Tie::Array::Sorted;
163 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
164 } or warn $@, "Expect slowdown with large datasets!\n";
168 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
170 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
172 s/\A\h*// unless $opt{unmodified};
173 my $valnum = s/$valmatch/\n/ && $1;
174 push @values, $valnum;
175 push @order, $valnum if length $valnum;
176 if (defined $opt{trim} and defined $valnum) {
177 my $trimpos = abs $opt{trim};
178 $trimpos -= length $valnum if $opt{unmodified};
180 $_ = substr $_, 0, 2;
182 elsif (length > $trimpos) {
183 # cut and replace (intentional lvalue for speed, contrary to PBP)
184 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
188 show_lines() if defined $opt{interval} and $opt{interval} < 0
189 and $. % $opt{interval} == 0;
192 if ($opt{'zero-missing'}) {
193 push @values, (0) x 10;
196 $SIG{INT} = 'DEFAULT';
199 $opt{color} and defined $_[0] or return '';
200 return "\e[$_[0]m" if defined wantarray;
201 $_ = color(@_) . $_ . color(0) if defined;
207 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
209 @lines > $nr or return;
212 if (defined $opt{hidemax}) {
213 if ($opt{hidemin} and $opt{hidemin} < 0) {
214 $limit -= $opt{hidemax} - 1;
216 elsif ($opt{hidemax} <= $limit) {
217 $limit = $opt{hidemax} - 1;
221 @order = sort { $b <=> $a } @order unless tied @order;
222 my $maxval = $opt{maxval} // (
223 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
226 my $minval = $opt{minval} // min $order[-1] // (), 0;
227 my $range = $maxval - $minval;
228 my $lenval = $opt{'value-length'} // max map { length } @order;
229 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
230 max map { length $values[$_] && length $lines[$_] }
231 0 .. min $#lines, $opt{hidemax} || (); # left padding
232 my $size = defined $opt{width} && $range &&
233 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
236 if ($opt{markers} and $size > 0) {
237 for my $markspec (split /\h/, $opt{markers}) {
238 my ($char, $func) = split //, $markspec, 2;
240 if ($func eq 'avg') {
241 return sum(@order) / @order;
243 elsif ($func =~ /\A([0-9.]+)v\z/) {
244 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
245 my $index = $#order * $1 / 100;
246 return ($order[$index] + $order[$index + .5]) / 2;
248 elsif ($func =~ /\A-?[0-9.]+\z/) {
252 die "Unknown marker $char: $func\n";
261 color(36) for $barmark[$pos * $size] = $char;
264 state $lastmax = $maxval;
265 if ($maxval > $lastmax) {
266 print ' ' x ($lenval + $len);
269 ($lastmax - $minval) * $size + .5,
270 '-' x (($values[$nr - 1] - $minval) * $size);
272 say '+' x (($range - $lastmax) * $size + .5);
279 color(31), sprintf('%*s', $lenval, $minval),
280 color(90), '-', color(36), '+',
281 color(32), sprintf('%*s', $size * $range - 3, $maxval),
282 color(90), '-', color(36), '+',
286 while ($nr <= $limit) {
287 my $val = $values[$nr];
288 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
289 my $color = !length $val || !$opt{palette} ? undef :
290 $val == $order[0] ? $opt{palette}->[-1] : # max
291 $val == $order[-1] ? $opt{palette}->[0] : # min
292 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
293 my $indicator = $opt{indicators} && $opt{indicators}->[
294 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
295 $#{$opt{indicators}} < 2 ? 1 :
296 $val >= $order[0] ? -1 :
297 $rel * ($#{$opt{indicators}} - 1e-14) + 1
301 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
302 print color($color), $_ for $indicator;
305 print $indicator if defined $indicator;
308 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
309 sprintf "%*s", $lenval, $val;
310 color($color) for $val;
312 my $line = $lines[$nr] =~ s/\n/$val/r;
313 if (not length $val) {
317 printf '%-*s', $len + length($val), $line;
318 print $barmark[$_] // $opt{'graph-format'}
319 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
325 say $opt{palette} ? color(0) : '' if $opt{spark};
331 if ($opt{hidemin} or $opt{hidemax}) {
332 my $linemin = $opt{hidemin};
333 my $linemax = ($opt{hidemax} || @lines) - 1;
336 $linemax = @lines - $linemax;
338 printf '%.8g of ', $opt{'sum-format'}->(
339 sum(grep {length} @values[$linemin .. $linemax]) // 0
343 my $total = sum @order;
344 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
345 printf ' in %d values', scalar @order;
346 printf ' over %d lines', scalar @lines if @order != @lines;
347 printf(' (%s min, %s avg, %s max)',
348 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
349 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
350 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
359 show_stat() if $opt{stat};
360 exit 130 if @_; # 0x80+signo
368 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
371 -a, --[no-]ascii Restrict user interface to ASCII characters
372 -C, --[no-]color Force colored output of values and bar markers
373 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
375 --header Prepend a chart axis with minimum and maximum
377 -H, --human-readable Format values using SI unit prefixes
378 -t, --interval[=(N|-LINES)]
379 Output partial progress every given number of
380 seconds or input lines
381 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
382 -L, --limit[=(N|-LAST|START-[END])]
383 Stop output after a number of lines
384 --graph-format=CHAR Glyph to repeat for the graph line
385 -m, --markers=FORMAT Statistical positions to indicate on bars
386 --min=N, --max=N Bars extend from 0 or the minimum value if lower
387 --palette=(PRESET|COLORS)
388 Override colors of parsed numbers
389 -_, --spark Replace lines by sparklines
390 --indicators[=CHARS] Prefix a unicode character corresponding to each
392 -s, --stat Total statistics after all data
393 -u, --unmodified Do not reformat values, keeping leading whitespace
394 --value-length=SIZE Reserved space for numbers
395 -w, --width=COLUMNS Override the maximum number of columns to use
396 -h, --usage Overview of available options
397 --help Full pod documentation
398 -V, --version Version information
404 barcat - concatenate texts with graph to visualize values
408 B<barcat> [<options>] [<file>... | <numbers>]
412 Visualizes relative sizes of values read from input
413 (parameters, file(s) or STDIN).
414 Contents are concatenated similar to I<cat>,
415 but numbers are reformatted and a bar graph is appended to each line.
417 Don't worry, barcat does not drink and divide.
418 It can has various options for input and output (re)formatting,
419 but remains limited to one-dimensional charts.
420 For more complex graphing needs
421 you'll need a larger animal like I<gnuplot>.
427 =item -a, --[no-]ascii
429 Restrict user interface to ASCII characters,
430 replacing default UTF-8 by their closest approximation.
431 Input is always interpreted as UTF-8 and shown as is.
433 =item -C, --[no-]color
435 Force colored output of values and bar markers.
436 Defaults on if output is a tty,
437 disabled otherwise such as when piped or redirected.
438 Can also be disabled by setting I<-M>
439 or the I<NO_COLOR> environment variable.
441 =item -f, --field=(<number> | <regexp>)
443 Compare values after a given number of whitespace separators,
444 or matching a regular expression.
446 Unspecified or I<-f0> means values are at the start of each line.
447 With I<-f1> the second word is taken instead.
448 A string can indicate the starting position of a value
449 (such as I<-f:> if preceded by colons),
450 or capture the numbers itself,
451 for example I<-f'(\d+)'> for the first digits anywhere.
455 Prepend a chart axis with minimum and maximum values labeled.
457 =item -H, --human-readable
459 Format values using SI unit prefixes,
460 turning long numbers like I<12356789> into I<12.4M>.
461 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
462 Short integers are aligned but kept without decimal point.
464 =item -t, --interval[=(<seconds> | -<lines>)]
466 Output partial progress every given number of seconds or input lines.
467 An update can also be forced by sending a I<SIGALRM> alarm signal.
469 =item -l, --length=[-]<size>[%]
471 Trim line contents (between number and bars)
472 to a maximum number of characters.
473 The exceeding part is replaced by an abbreviation sign,
474 unless C<--length=0>.
476 Prepend a dash (i.e. make negative) to enforce padding
477 regardless of encountered contents.
479 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
481 Stop output after a number of lines.
482 A single value indicates the last line number (like C<head>),
483 or first line counting from the bottom if negative (like C<tail>).
484 A specific range can be given by two values.
486 All input is still counted and analyzed for statistics,
487 but disregarded for padding and bar size.
489 =item --graph-format=<character>
491 Glyph to repeat for the graph line.
492 Defaults to a dash C<->.
494 =item -m, --markers=<format>
496 Statistical positions to indicate on bars.
497 A single indicator glyph precedes each position:
503 Exact value to match on the axis.
504 A vertical bar at the zero crossing is displayed by I<|0>
506 For example I<:3.14> would show a colon at pi.
508 =item <percentage>I<v>
510 Ranked value at the given percentile.
511 The default shows I<+> at I<50v> for the mean or median;
512 the middle value or average between middle values.
513 One standard deviation right of the mean is at about I<68.3v>.
514 The default includes I<< >31.73v <68.27v >>
515 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
520 the sum of all values divided by the number of counted lines.
521 Indicated by default as I<=>.
525 =item --min=<number>, --max=<number>
527 Bars extend from 0 or the minimum value if lower,
528 to the largest value encountered.
529 These options can be set to customize this range.
531 =item --palette=(<preset> | <color>...)
533 Override colors of parsed numbers.
534 Can be any CSI escape, such as I<90> for default dark grey,
535 or alternatively I<1;30> for bright black.
537 In case of additional colors,
538 the last is used for values equal to the maximum, the first for minima.
539 If unspecified, these are green and red respectively (I<31 90 32>).
540 Multiple intermediate colors will be distributed
541 relative to the size of values.
543 Predefined color schemes are named I<whites> and I<fire>,
544 or I<greys> and I<fire256> for 256-color variants.
548 Replace lines by I<sparklines>,
549 single characters (configured by C<--indicators>)
550 corresponding to input values.
552 =item --indicators[=<characters>]
554 Prefix a unicode character corresponding to each value.
555 The first specified character will be used for non-values,
556 the remaining sequence will be distributed over the range of values.
557 Unspecified, block fill glyphs U+2581-2588 will be used.
561 Total statistics after all data.
563 =item -u, --unmodified
565 Do not reformat values, keeping leading whitespace.
566 Keep original value alignment, which may be significant in some programs.
568 =item --value-length=<size>
570 Reserved space for numbers.
572 =item -w, --width=<columns>
574 Override the maximum number of columns to use.
575 Appended graphics will extend to fill up the entire screen.
579 Overview of available options.
583 Full pod documentation
584 as rendered by perldoc.
596 seq 30 | awk '{print sin($1/10)}' | barcat
598 Compare file sizes (with human-readable numbers):
600 du -d0 -b * | barcat -H
602 Memory usage of user processes with long names truncated:
604 ps xo rss,pid,cmd | barcat -l40
606 Monitor network latency from prefixed results:
608 ping google.com | barcat -f'time=\K' -t
610 Commonly used after counting, for example users on the current server:
612 users | tr ' ' '\n' | sort | uniq -c | barcat
614 Letter frequencies in text files:
616 cat /usr/share/games/fortunes/*.u8 |
617 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
618 sort | uniq -c | barcat
620 Number of HTTP requests per day:
622 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
624 Any kind of database query with counts, preserving returned alignment:
626 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
629 In PostgreSQL from within the client:
631 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
633 Earthquakes worldwide magnitude 1+ in the last 24 hours:
635 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
636 column -tns, | barcat -f4 -u -l80%
638 External datasets, like movies per year:
640 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
641 jq .[].year | uniq -c | barcat
643 Pokémon height comparison:
645 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
646 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
648 USD/EUR exchange rate from CSV provided by the ECB:
650 curl https://sdw.ecb.europa.eu/export.do \
651 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
652 barcat -f',\K' --value-length=7
654 Total population history in XML from the World Bank:
656 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
657 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
660 And of course various Git statistics, such commit count by year:
662 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
664 Or the top 3 most frequent authors with statistics over all:
666 git shortlog -sn | barcat -L3 -s
668 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
670 ( git log --pretty=%ci --since=30day | cut -b-10
671 seq 0 30 | xargs -i date +%F -d-{}day ) |
672 sort | uniq -c | awk '$1--' | barcat --spark
674 Sparkline graphics of simple input given as inline parameters:
676 barcat -_ 3 1 4 1 5 0 9 2 4
678 Misusing the spark functionality to draw a lolcat line:
680 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
684 Mischa POSLAWSKY <perl@shiar.org>