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 s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/;
24 (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)'
26 $opt{anchor} = qr/$_/;
27 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
33 'trim|length|l=s' => sub {
34 my ($optname, $optval) = @_;
35 $optval =~ s/%$// and $opt{trimpct}++;
36 $optval =~ m/\A-?[0-9]+\z/ or die(
37 "Value \"$optval\" invalid for option $optname",
38 " (number or percentage expected)\n"
48 my ($optname, $optval) = @_;
50 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
51 ($opt{hidemin}, $opt{hidemax}) =
52 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
53 "Value \"$optval\" invalid for option limit",
60 'graph-format=s' => sub {
61 $opt{'graph-format'} = substr $_[1], 0, 1;
68 fire => [qw( 90 31 91 33 93 97 96 )],
69 fire256=> [map {"38;5;$_"} qw(
71 202 208 214 220 226 227 228 229 230 231 159
73 whites => [qw( 1;30 0;37 1;37 )],
74 greys => [map {"38;5;$_"} 0, 232..255, 15],
75 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
76 rainbow=> [map {"38;5;$_"}
78 (map { 196 + $_*6 } 0..4), # +g
79 (map { 226 - $_*6*6 } 0..4), # -r
80 (map { 46 + $_ } 0..4), # +b
81 (map { 51 - $_*6 } 0..4), # -g
82 (map { 21 + $_*6*6 } 0..4), # +r
83 (map { 201 - $_ } 0..4), # -b
87 my @vals = split /[^0-9;]/, $_[1]
88 or die "Empty palette resulting from \"$_[1]\"\n";
97 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
98 say "barcat $mascot version $VERSION";
102 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
107 Pod::Usage::pod2usage(
108 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
111 ) or exit 64; # EX_USAGE
114 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
115 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
116 $opt{'graph-format'} //= '-';
117 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
118 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
119 if $opt{'human-readable'};
120 $opt{anchor} //= qr/\A/;
121 $opt{'value-length'} = 4 if $opt{units};
122 $opt{'value-length'} = 1 if $opt{unmodified};
123 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
124 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
125 $opt{palette} //= $opt{color} && [31, 90, 32];
126 $opt{indicators} = [split //, $opt{indicators} ||
127 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
128 ] if defined $opt{indicators} or $opt{spark};
129 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
130 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
131 and undef $opt{interval};
133 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
134 $opt{'value-format'} = $opt{sexagesimal} ? sub {
135 my $s = abs($_[0]) + .5;
136 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
137 } : $opt{units} && sub {
139 log(abs $_[0] || 1) / log(10)
140 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
141 + 1e-15 # float imprecision
143 my $decimal = ($unit % 3) == ($unit < 0);
144 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
145 $decimal = ($unit % 3) == ($unit < 0);
146 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
148 3 + ($_[0] < 0), # digits plus optional negative sign
150 $_[0] / 1000 ** int($unit/3), # number
151 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
152 $opt{units}->[$unit/3] # suffix
154 } and $opt{reformat}++;
155 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
158 my (@lines, @values, @order);
160 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
163 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
165 $SIG{INT} = \&show_exit;
167 if (defined $opt{interval}) {
168 $opt{interval} ||= 1;
169 alarm $opt{interval} if $opt{interval} > 0;
172 require Tie::Array::Sorted;
173 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
174 } or warn $@, "Expect slowdown with large datasets!\n";
178 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
180 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
182 s/\A\h*// unless $opt{unmodified};
183 my $valnum = s/$valmatch/\n/ && $1;
184 push @values, $valnum;
185 push @order, $valnum if length $valnum;
186 if (defined $opt{trim} and defined $valnum) {
187 my $trimpos = abs $opt{trim};
188 $trimpos -= length $valnum if $opt{unmodified};
190 $_ = substr $_, 0, 2;
192 elsif (length > $trimpos) {
193 # cut and replace (intentional lvalue for speed, contrary to PBP)
194 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
198 show_lines() if defined $opt{interval} and $opt{interval} < 0
199 and $. % $opt{interval} == 0;
202 $SIG{INT} = 'DEFAULT';
205 $opt{color} and defined $_[0] or return '';
206 return "\e[$_[0]m" if defined wantarray;
207 $_ = color(@_) . $_ . color(0) if defined;
213 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
215 @lines > $nr or return;
218 if (defined $opt{hidemax}) {
219 if ($opt{hidemin} and $opt{hidemin} < 0) {
220 $limit -= $opt{hidemax} - 1;
222 elsif ($opt{hidemax} <= $limit) {
223 $limit = $opt{hidemax} - 1;
227 @order = sort { $b <=> $a } @order unless tied @order;
228 my $maxval = $opt{maxval} // (
229 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
232 my $minval = $opt{minval} // min $order[-1] // (), 0;
233 my $range = $maxval - $minval;
234 $range &&= log $range if $opt{log};
235 my $lenval = $opt{'value-length'} // max map { length } @order;
236 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
237 max map { length $values[$_] && length $lines[$_] }
238 0 .. min $#lines, $opt{hidemax} || (); # left padding
239 my $size = defined $opt{width} && $range &&
240 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
243 if ($opt{markers} and $size > 0) {
244 for my $markspec (split /\h/, $opt{markers}) {
245 my ($char, $func) = split //, $markspec, 2;
247 if ($func eq 'avg') {
248 return sum(@order) / @order;
250 elsif ($func =~ /\A([0-9.]+)v\z/) {
252 "Invalid marker $char: percentile $1 out of bounds\n"
254 my $index = $#order * $1 / 100;
255 return ($order[$index] + $order[$index + .5]) / 2;
257 elsif ($func =~ /\A-?[0-9.]+\z/) {
261 die "Unknown marker $char: $func\n";
269 $pos &&= log $pos if $opt{log};
271 color(36) for $barmark[$pos / $range * $size] = $char;
274 state $lastmax = $maxval;
275 if ($maxval > $lastmax) {
276 print ' ' x ($lenval + $len);
279 ($lastmax - $minval) * $size / $range + .5,
280 '-' x (($values[$nr - 1] - $minval) * $size / $range);
282 say '+' x (($range - $lastmax) * $size / $range + .5);
289 color(31), sprintf('%*s', $lenval, $minval),
290 color(90), '-', color(36), '+',
291 color(32), sprintf('%*s', $size - 3, $maxval),
292 color(90), '-', color(36), '+',
296 while ($nr <= $limit) {
297 my $val = $values[$nr];
300 $rel = $val - $minval;
301 $rel &&= log $rel if $opt{log};
302 $rel = min(1, $rel / $range) if $range; # 0..1
304 my $color = !length $val || !$opt{palette} ? undef :
305 $val == $order[0] ? $opt{palette}->[-1] : # max
306 $val == $order[-1] ? $opt{palette}->[0] : # min
307 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
308 my $indicator = $opt{indicators} && $opt{indicators}->[
309 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
310 $#{$opt{indicators}} < 2 ? 1 :
311 $val >= $order[0] ? -1 :
312 $rel * ($#{$opt{indicators}} - 1e-14) + 1
316 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
317 print color($color), $_ for $indicator;
320 print $indicator if defined $indicator;
323 $val = sprintf("%*s", $lenval,
324 $opt{reformat} ? $opt{'value-format'}->($val) : $val
326 color($color) for $val;
328 my $line = $lines[$nr] =~ s/\n/$val/r;
329 if (not length $val) {
333 printf '%-*s', $len + length($val), $line;
334 if ($rel and $size) {
335 print $barmark[$_] // $opt{'graph-format'}
336 for 1 .. $rel * $size + .5;
343 say $opt{palette} ? color(0) : '' if $opt{spark};
349 if ($opt{hidemin} or $opt{hidemax}) {
350 my $linemin = $opt{hidemin};
351 my $linemax = ($opt{hidemax} || @lines) - 1;
354 $linemax = @lines - $linemax;
356 printf '%.8g of ', $opt{'value-format'}->(
357 sum(grep {length} @values[$linemin .. $linemax]) // 0
361 my $total = sum @order;
362 printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
363 printf ' in %d values', scalar @order;
364 printf ' over %d lines', scalar @lines if @order != @lines;
365 printf(' (%s min, %s avg, %s max)',
366 color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
367 color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
368 color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
377 show_stat() if $opt{stat};
378 exit 130 if @_; # 0x80+signo
386 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
389 -a, --[no-]ascii Restrict user interface to ASCII characters
390 -C, --[no-]color Force colored output of values and bar markers
391 -f, --field=([+]N|REGEXP)
392 Compare values after a given number of whitespace
394 --header Prepend a chart axis with minimum and maximum
396 -H, --human-readable Format values using SI unit prefixes
397 --sexagesimal Convert seconds to HH:MM:SS time format
398 -t, --interval[=(N|-LINES)]
399 Output partial progress every given number of
400 seconds or input lines
401 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
402 -L, --limit[=(N|-LAST|START-[END])]
403 Stop output after a number of lines
404 -e, --log Logarithmic (exponential) scale instead of linear
405 --graph-format=CHAR Glyph to repeat for the graph line
406 -m, --markers=FORMAT Statistical positions to indicate on bars
407 --min=N, --max=N Bars extend from 0 or the minimum value if lower
408 --palette=(PRESET|COLORS)
409 Override colors of parsed numbers
410 -_, --spark Replace lines by sparklines
411 --indicators[=CHARS] Prefix a unicode character corresponding to each
413 -s, --stat Total statistics after all data
414 -u, --unmodified Do not reformat values, keeping leading whitespace
415 --value-length=SIZE Reserved space for numbers
416 -w, --width=COLUMNS Override the maximum number of columns to use
417 -h, --usage Overview of available options
418 --help Full pod documentation
419 -V, --version Version information
425 barcat - concatenate texts with graph to visualize values
429 B<barcat> [<options>] [<file>... | <numbers>]
433 Visualizes relative sizes of values read from input
434 (parameters, file(s) or STDIN).
435 Contents are concatenated similar to I<cat>,
436 but numbers are reformatted and a bar graph is appended to each line.
438 Don't worry, barcat does not drink and divide.
439 It can has various options for input and output (re)formatting,
440 but remains limited to one-dimensional charts.
441 For more complex graphing needs
442 you'll need a larger animal like I<gnuplot>.
448 =item -a, --[no-]ascii
450 Restrict user interface to ASCII characters,
451 replacing default UTF-8 by their closest approximation.
452 Input is always interpreted as UTF-8 and shown as is.
454 =item -C, --[no-]color
456 Force colored output of values and bar markers.
457 Defaults on if output is a tty,
458 disabled otherwise such as when piped or redirected.
459 Can also be disabled by setting I<-M>
460 or the I<NO_COLOR> environment variable.
462 =item -f, --field=([+]<number> | <regexp>)
464 Compare values after a given number of whitespace separators,
465 or matching a regular expression.
467 Unspecified or I<-f0> means values are at the start of each line.
468 With I<-f1> the second word is taken instead.
469 A string can indicate the starting position of a value
470 (such as I<-f:> if preceded by colons),
471 or capture the numbers itself,
472 for example I<-f'(\d+)'> for the first digits anywhere.
473 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
477 Prepend a chart axis with minimum and maximum values labeled.
479 =item -H, --human-readable
481 Format values using SI unit prefixes,
482 turning long numbers like I<12356789> into I<12.4M>.
483 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
484 Short integers are aligned but kept without decimal point.
488 Convert seconds to HH:MM:SS time format.
490 =item -t, --interval[=(<seconds> | -<lines>)]
492 Output partial progress every given number of seconds or input lines.
493 An update can also be forced by sending a I<SIGALRM> alarm signal.
495 =item -l, --length=[-]<size>[%]
497 Trim line contents (between number and bars)
498 to a maximum number of characters.
499 The exceeding part is replaced by an abbreviation sign,
500 unless C<--length=0>.
502 Prepend a dash (i.e. make negative) to enforce padding
503 regardless of encountered contents.
505 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
507 Stop output after a number of lines.
508 A single value indicates the last line number (like C<head>),
509 or first line counting from the bottom if negative (like C<tail>).
510 A specific range can be given by two values.
512 All input is still counted and analyzed for statistics,
513 but disregarded for padding and bar size.
517 Logarithmic (I<e>xponential) scale instead of linear
518 to compare orders of magnitude.
520 =item --graph-format=<character>
522 Glyph to repeat for the graph line.
523 Defaults to a dash C<->.
525 =item -m, --markers=<format>
527 Statistical positions to indicate on bars.
528 A single indicator glyph precedes each position:
534 Exact value to match on the axis.
535 A vertical bar at the zero crossing is displayed by I<|0>
537 For example I<:3.14> would show a colon at pi.
539 =item <percentage>I<v>
541 Ranked value at the given percentile.
542 The default shows I<+> at I<50v> for the mean or median;
543 the middle value or average between middle values.
544 One standard deviation right of the mean is at about I<68.3v>.
545 The default includes I<< >31.73v <68.27v >>
546 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
551 the sum of all values divided by the number of counted lines.
552 Indicated by default as I<=>.
556 =item --min=<number>, --max=<number>
558 Bars extend from 0 or the minimum value if lower,
559 to the largest value encountered.
560 These options can be set to customize this range.
562 =item --palette=(<preset> | <color>...)
564 Override colors of parsed numbers.
565 Can be any CSI escape, such as I<90> for default dark grey,
566 or alternatively I<1;30> for bright black.
568 In case of additional colors,
569 the last is used for values equal to the maximum, the first for minima.
570 If unspecified, these are green and red respectively (I<31 90 32>).
571 Multiple intermediate colors will be distributed
572 relative to the size of values.
574 Predefined color schemes are named I<whites> and I<fire>,
575 or I<greys> and I<fire256> for 256-color variants.
579 Replace lines by I<sparklines>,
580 single characters (configured by C<--indicators>)
581 corresponding to input values.
583 =item --indicators[=<characters>]
585 Prefix a unicode character corresponding to each value.
586 The first specified character will be used for non-values,
587 the remaining sequence will be distributed over the range of values.
588 Unspecified, block fill glyphs U+2581-2588 will be used.
592 Total statistics after all data.
594 =item -u, --unmodified
596 Do not reformat values, keeping leading whitespace.
597 Keep original value alignment, which may be significant in some programs.
599 =item --value-length=<size>
601 Reserved space for numbers.
603 =item -w, --width=<columns>
605 Override the maximum number of columns to use.
606 Appended graphics will extend to fill up the entire screen,
607 otherwise determined by the environment variable I<COLUMNS>
608 or by running the C<tput> command.
612 Overview of available options.
616 Full pod documentation
617 as rendered by perldoc.
629 seq 30 | awk '{print sin($1/10)}' | barcat
631 Compare file sizes (with human-readable numbers):
633 du -d0 -b * | barcat -H
635 Same from formatted results, selecting the first numeric value:
637 tree -s --noreport | barcat -H -f+
639 Compare media metadata, like image size or play time:
641 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
643 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
645 find -type f -print0 | xargs -0 -L1 \
646 ffprobe -show_format -of json -v error |
647 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
649 Memory usage of user processes with long names truncated:
651 ps xo rss,pid,cmd | barcat -l40
653 Monitor network latency from prefixed results:
655 ping google.com | barcat -f'time=\K' -t
657 Commonly used after counting, for example users on the current server:
659 users | tr ' ' '\n' | sort | uniq -c | barcat
661 Letter frequencies in text files:
663 cat /usr/share/games/fortunes/*.u8 |
664 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
665 sort | uniq -c | barcat
667 Number of HTTP requests per day:
669 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
671 Any kind of database query results, preserving returned alignment:
673 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
676 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
678 > SELECT schemaname, relname, pg_total_relation_size(relid)
679 FROM pg_statio_user_tables ORDER BY idx_blks_hit
682 Same thing in SQLite (requires the sqlite3 client):
685 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
687 Earthquakes worldwide magnitude 1+ in the last 24 hours:
689 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
690 column -ts, -n | barcat -f4 -u -l80%
692 External datasets, like movies per year:
694 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
695 jq .[].year | uniq -c | barcat
697 Pokémon height comparison:
699 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
700 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
702 USD/EUR exchange rate from CSV provided by the ECB:
704 curl https://sdw.ecb.europa.eu/export.do \
705 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
706 barcat -f',\K' --value-length=7
708 Total population history in XML from the World Bank:
710 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
711 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
714 Population and other information for all countries:
716 curl http://download.geonames.org/export/dump/countryInfo.txt |
717 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
719 And of course various Git statistics, such commit count by year:
721 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
723 Or the top 3 most frequent authors with statistics over all:
725 git shortlog -sn | barcat -L3 -s
727 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
729 ( git log --pretty=%ci --since=30day | cut -b-10
730 seq 0 30 | xargs -i date +%F -d-{}day ) |
731 sort | uniq -c | awk '$1--' | barcat --spark
733 Sparkline graphics of simple input given as inline parameters:
735 barcat -_ 3 1 4 1 5 0 9 2 4
737 Misusing the spark functionality to draw a lolcat line:
739 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
743 Mischa POSLAWSKY <perl@shiar.org>