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
52 ($opt{hidemin}, $opt{hidemax}) =
53 $optval =~ m/\A (?: (-? [0-9]+)? - )? (-? [0-9]+)? \z/ or die(
54 "Value \"$optval\" invalid for option limit",
57 s/\A-0*\z// and $_ ||= undef for $opt{hidemax} // ();
62 'graph-format=s' => sub {
63 $opt{'graph-format'} = substr $_[1], 0, 1;
70 fire => [qw( 90 31 91 33 93 97 96 )],
71 fire256=> [map {"38;5;$_"} qw(
73 202 208 214 220 226 227 228 229 230 231 159
75 whites => [qw( 1;30 0;37 1;37 )],
76 greys => [map {"38;5;$_"} 0, 232..255, 15],
77 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
78 rainbow=> [map {"38;5;$_"}
80 (map { 196 + $_*6 } 0..4), # +g
81 (map { 226 - $_*6*6 } 0..4), # -r
82 (map { 46 + $_ } 0..4), # +b
83 (map { 51 - $_*6 } 0..4), # -g
84 (map { 21 + $_*6*6 } 0..4), # +r
85 (map { 201 - $_ } 0..4), # -b
89 my @vals = split /[^0-9;]/, $_[1]
90 or die "Empty palette resulting from \"$_[1]\"\n";
100 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
101 say "barcat $mascot version $VERSION";
105 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
110 Pod::Usage::pod2usage(
111 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
114 ) or exit 64; # EX_USAGE
117 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
118 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
119 $opt{'graph-format'} //= '-';
120 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
121 $opt{units} = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
122 if $opt{'human-readable'};
123 $opt{anchor} //= qr/\A/;
124 $opt{'value-length'} = 4 if $opt{units};
125 $opt{'value-length'} = 1 if $opt{unmodified};
126 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
127 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
128 $opt{report} //= join(', ',
129 '${min; color(31)} min',
130 '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg',
131 '${max; color(32)} max',
133 $opt{palette} //= $opt{color} && [31, 90, 32];
134 $opt{indicators} = [split //, $opt{indicators} ||
135 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
136 ] if defined $opt{indicators} or $opt{spark};
137 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
138 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
139 and undef $opt{interval};
141 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
142 $opt{'value-format'} = $opt{sexagesimal} ? sub {
143 my $s = abs($_[0]) + .5;
144 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
145 } : $opt{units} && sub {
147 log(abs $_[0] || 1) / log(10)
148 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
149 + 1e-15 # float imprecision
151 my $decimal = ($unit % 3) == ($unit < 0);
152 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
153 $decimal = ($unit % 3) == ($unit < 0);
154 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
156 3 + ($_[0] < 0), # digits plus optional negative sign
158 $_[0] / 1000 ** int($unit/3), # number
159 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
160 $opt{units}->[$unit/3] # suffix
162 } and $opt{reformat}++;
163 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
166 my (@lines, @values, @order);
168 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
171 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
173 $SIG{INT} = \&show_exit;
175 if (defined $opt{interval}) {
176 $opt{interval} ||= 1;
177 alarm $opt{interval} if $opt{interval} > 0;
180 require Tie::Array::Sorted;
181 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
182 } or warn $@, "Expect slowdown with large datasets!\n";
185 my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish
186 my $valmatch = qr< $opt{anchor} ( \h* -? $float |) >x;
187 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
189 s/\A\h*// unless $opt{unmodified};
190 my $valnum = s/$valmatch/\n/ && $1;
191 push @values, $valnum;
192 push @order, $valnum if length $valnum;
193 if (defined $opt{trim} and defined $valnum) {
194 my $trimpos = abs $opt{trim};
195 $trimpos -= length $valnum if $opt{unmodified};
197 $_ = substr $_, 0, 2;
199 elsif (length > $trimpos) {
200 # cut and replace (intentional lvalue for speed, contrary to PBP)
201 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
205 show_lines() if defined $opt{interval} and $opt{interval} < 0
206 and $. % $opt{interval} == 0;
209 $SIG{INT} = 'DEFAULT';
212 $opt{color} and defined $_[0] or return '';
213 return "\e[$_[0]m" if defined wantarray;
214 $_ = color(@_) . $_ . color(0) if defined;
220 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
222 @lines > $nr or return;
225 if (defined $opt{hidemax}) {
226 if ($opt{hidemax} < 0) {
227 $limit = $nr - $opt{hidemax} - 1;
229 elsif ($opt{hidemin} and $opt{hidemin} < 0) {
230 $limit -= $opt{hidemax} - 1;
232 elsif ($opt{hidemax} <= $limit) {
233 $limit = $opt{hidemax} - 1;
237 @order = sort { $b <=> $a } @order unless tied @order;
238 my $maxval = $opt{maxval} // (
239 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
242 my $minval = $opt{minval} // min $order[-1] // (), 0;
243 my $range = $maxval - $minval;
244 $range &&= log $range if $opt{log};
245 my $lenval = $opt{'value-length'} // max map { length } @order;
246 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
247 max(map { length $values[$_] && length $lines[$_] } $nr .. $limit)
249 my $size = defined $opt{width} && $range &&
250 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
253 if ($opt{markers} and $size > 0) {
254 for my $markspec (split /\h/, $opt{markers}) {
255 my ($char, $func) = split //, $markspec, 2;
257 if ($func eq 'avg') {
258 return sum(@order) / @order;
260 elsif ($func =~ /\A([0-9.]+)v\z/) {
262 "Invalid marker $char: percentile $1 out of bounds\n"
264 my $index = $#order * $1 / 100;
265 return ($order[$index] + $order[$index + .5]) / 2;
267 elsif ($func =~ /\A-?[0-9.]+\z/) {
270 elsif ($func =~ /\A\/($float)\z/) {
271 my @range = my $multiple = my $next = $1;
272 while ($next < $maxval) {
273 $multiple *= 10 if $opt{log};
274 push @range, $next += $multiple;
279 die "Unknown marker $char: $func\n";
288 $pos &&= log $pos if $opt{log};
290 color(36) for $barmark[$pos / $range * $size] = $char;
294 state $lastmax = $maxval;
295 if ($maxval > $lastmax) {
296 print ' ' x ($lenval + $len);
299 ($lastmax - $minval) * $size / $range + .5,
300 '-' x (($values[$nr - 1] - $minval) * $size / $range);
302 say '+' x (($range - $lastmax) * $size / $range + .5);
309 color(31), sprintf('%*s', $lenval, $minval),
310 color(90), '-', color(36), '+',
311 color(32), sprintf('%*s', $size - 3, $maxval),
312 color(90), '-', color(36), '+',
316 while ($nr <= $limit) {
317 my $val = $values[$nr];
320 $rel = $val - $minval;
321 $rel &&= log $rel if $opt{log};
322 $rel = min(1, $rel / $range) if $range; # 0..1
324 my $color = !length $val || !$opt{palette} ? undef :
325 $val == $order[0] ? $opt{palette}->[-1] : # max
326 $val == $order[-1] ? $opt{palette}->[0] : # min
327 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
328 my $indicator = $opt{indicators} && $opt{indicators}->[
329 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
330 $#{$opt{indicators}} < 2 ? 1 :
331 $val >= $order[0] ? -1 :
332 $rel * ($#{$opt{indicators}} - 1e-14) + 1
336 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
337 print color($color), $_ for $indicator;
340 print $indicator if defined $indicator;
343 $val = sprintf("%*s", $lenval,
344 $opt{reformat} ? $opt{'value-format'}->($val) : $val
346 color($color) for $val;
348 my $line = $lines[$nr] =~ s/\n/$val/r;
349 if (not length $val) {
353 printf '%-*s', $len + length($val), $line;
354 if ($rel and $size) {
355 print $barmark[$_] // $opt{'graph-format'}
356 for 1 .. $rel * $size + .5;
363 say $opt{palette} ? color(0) : '' if $opt{spark};
369 if ($opt{hidemin} or $opt{hidemax}) {
370 my $linemin = $opt{hidemin};
371 my $linemax = ($opt{hidemax} || @lines) - 1;
374 $linemax = @lines - $linemax;
376 print varfmt('${sum+} of ', {
377 lines => $linemax - $linemin + 1,
378 sum => sum(0, grep {length} @values[$linemin .. $linemax]),
382 my $total = sum @order;
383 my $fmt = '${sum+;color(1)} total in ${count#} values';
384 $fmt .= ' over ${lines#} lines' if @order != @lines;
385 $fmt .= " ($_)" for $opt{report} || ();
392 avg => $total / @order,
400 my ($fmt, $vars) = @_;
401 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
402 my ($name, $cmd) = split /\s*;/, $1, 2;
403 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
404 local $_ = $vars->{$name};
406 $_ = $opt{'value-format'}->($_) if $format;
409 warn "Error in \$$name report: $@" if $@;
414 warn "Unknown variable \$$name in report\n";
423 show_stat() if $opt{stat};
424 exit 130 if @_; # 0x80+signo
432 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
435 -a, --[no-]ascii Restrict user interface to ASCII characters
436 -C, --[no-]color Force colored output of values and bar markers
437 -f, --field=([+]N|REGEXP)
438 Compare values after a given number of whitespace
440 --header Prepend a chart axis with minimum and maximum
442 -H, --human-readable Format values using SI unit prefixes
443 --sexagesimal Convert seconds to HH:MM:SS time format
444 -t, --interval[=(N|-LINES)]
445 Output partial progress every given number of
446 seconds or input lines
447 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
448 -L, --limit=[N|[-]START(-[END]|+N)]
449 Select a range of lines to display
450 -e, --log Logarithmic (exponential) scale instead of linear
451 --graph-format=CHAR Glyph to repeat for the graph line
452 -m, --markers=FORMAT Statistical positions to indicate on bars
453 --min=N, --max=N Bars extend from 0 or the minimum value if lower
454 --palette=(PRESET|COLORS)
455 Override colors of parsed numbers
456 -_, --spark Replace lines by sparklines
457 --indicators[=CHARS] Prefix a unicode character corresponding to each
459 -s, --stat Total statistics after all data
460 -u, --unmodified Do not reformat values, keeping leading whitespace
461 --value-length=SIZE Reserved space for numbers
462 -w, --width=COLUMNS Override the maximum number of columns to use
463 -h, --usage Overview of available options
464 --help Full pod documentation
465 -V, --version Version information
471 barcat - concatenate texts with graph to visualize values
475 B<barcat> [<options>] [<file>... | <numbers>]
479 Visualizes relative sizes of values read from input
480 (parameters, file(s) or STDIN).
481 Contents are concatenated similar to I<cat>,
482 but numbers are reformatted and a bar graph is appended to each line.
484 Don't worry, barcat does not drink and divide.
485 It can has various options for input and output (re)formatting,
486 but remains limited to one-dimensional charts.
487 For more complex graphing needs
488 you'll need a larger animal like I<gnuplot>.
494 =item -a, --[no-]ascii
496 Restrict user interface to ASCII characters,
497 replacing default UTF-8 by their closest approximation.
498 Input is always interpreted as UTF-8 and shown as is.
500 =item -C, --[no-]color
502 Force colored output of values and bar markers.
503 Defaults on if output is a tty,
504 disabled otherwise such as when piped or redirected.
505 Can also be disabled by setting I<-M>
506 or the I<NO_COLOR> environment variable.
508 =item -f, --field=([+]<number> | <regexp>)
510 Compare values after a given number of whitespace separators,
511 or matching a regular expression.
513 Unspecified or I<-f0> means values are at the start of each line.
514 With I<-f1> the second word is taken instead.
515 A string can indicate the starting position of a value
516 (such as I<-f:> if preceded by colons),
517 or capture the numbers itself,
518 for example I<-f'(\d+)'> for the first digits anywhere.
519 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
523 Prepend a chart axis with minimum and maximum values labeled.
525 =item -H, --human-readable
527 Format values using SI unit prefixes,
528 turning long numbers like I<12356789> into I<12.4M>.
529 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
530 Short integers are aligned but kept without decimal point.
534 Convert seconds to HH:MM:SS time format.
536 =item -t, --interval[=(<seconds> | -<lines>)]
538 Output partial progress every given number of seconds or input lines.
539 An update can also be forced by sending a I<SIGALRM> alarm signal.
541 =item -l, --length=[-]<size>[%]
543 Trim line contents (between number and bars)
544 to a maximum number of characters.
545 The exceeding part is replaced by an abbreviation sign,
546 unless C<--length=0>.
548 Prepend a dash (i.e. make negative) to enforce padding
549 regardless of encountered contents.
551 =item -L, --limit=[<count> | [-]<start>(-[<end>] | +<count>)]
553 Select a range of lines to display.
554 A single integer indicates the last line number (like C<head>),
555 or first line counting from the bottom if negative (like C<tail>).
557 A range consists of a starting line number followed by either
558 a dash C<-> to an optional end, or plus sign C<+> with count.
560 All hidden input is still counted and analyzed for statistics,
561 but disregarded for padding and bar size.
565 Logarithmic (I<e>xponential) scale instead of linear
566 to compare orders of magnitude.
568 =item --graph-format=<character>
570 Glyph to repeat for the graph line.
571 Defaults to a dash C<->.
573 =item -m, --markers=<format>
575 Statistical positions to indicate on bars.
576 A single indicator glyph precedes each position:
582 Exact value to match on the axis.
583 A vertical bar at the zero crossing is displayed by I<|0>
585 For example I<π3.14> would locate pi.
589 Repeated at every multiple of a number.
590 For example I<:/1> for a grid at every integer.
592 =item <percentage>I<v>
594 Ranked value at the given percentile.
595 The default shows I<+> at I<50v> for the mean or median;
596 the middle value or average between middle values.
597 One standard deviation right of the mean is at about I<68.3v>.
598 The default includes I<< >31.73v <68.27v >>
599 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
604 the sum of all values divided by the number of counted lines.
605 Indicated by default as I<=>.
609 =item --min=<number>, --max=<number>
611 Bars extend from 0 or the minimum value if lower,
612 to the largest value encountered.
613 These options can be set to customize this range.
615 =item --palette=(<preset> | <color>...)
617 Override colors of parsed numbers.
618 Can be any CSI escape, such as I<90> for default dark grey,
619 or alternatively I<1;30> for bright black.
621 In case of additional colors,
622 the last is used for values equal to the maximum, the first for minima.
623 If unspecified, these are green and red respectively (I<31 90 32>).
624 Multiple intermediate colors will be distributed
625 relative to the size of values.
627 Predefined color schemes are named I<whites> and I<fire>,
628 or I<greys> and I<fire256> for 256-color variants.
632 Replace lines by I<sparklines>,
633 single characters (configured by C<--indicators>)
634 corresponding to input values.
636 =item --indicators[=<characters>]
638 Prefix a unicode character corresponding to each value.
639 The first specified character will be used for non-values,
640 the remaining sequence will be distributed over the range of values.
641 Unspecified, block fill glyphs U+2581-2588 will be used.
645 Total statistics after all data.
647 =item -u, --unmodified
649 Do not reformat values, keeping leading whitespace.
650 Keep original value alignment, which may be significant in some programs.
652 =item --value-length=<size>
654 Reserved space for numbers.
656 =item -w, --width=<columns>
658 Override the maximum number of columns to use.
659 Appended graphics will extend to fill up the entire screen,
660 otherwise determined by the environment variable I<COLUMNS>
661 or by running the C<tput> command.
665 Overview of available options.
669 Full pod documentation
670 as rendered by perldoc.
682 seq 30 | awk '{print sin($1/10)}' | barcat
684 Compare file sizes (with human-readable numbers):
686 du -d0 -b * | barcat -H
688 Same from formatted results, selecting the first numeric value:
690 tree -s --noreport | barcat -H -f+
692 Compare media metadata, like image size or play time:
694 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
696 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
698 find -type f -print0 | xargs -0 -L1 \
699 ffprobe -show_format -of json -v error |
700 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
702 Memory usage of user processes with long names truncated:
704 ps xo rss,pid,cmd | barcat -l40
706 Monitor network latency from prefixed results:
708 ping google.com | barcat -f'time=\K' -t
710 Commonly used after counting, for example users on the current server:
712 users | tr ' ' '\n' | sort | uniq -c | barcat
714 Letter frequencies in text files:
716 cat /usr/share/games/fortunes/*.u8 |
717 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
718 sort | uniq -c | barcat
720 Number of HTTP requests per day:
722 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
724 Any kind of database query results, preserving returned alignment:
726 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
729 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
731 > SELECT schemaname, relname, pg_total_relation_size(relid)
732 FROM pg_statio_user_tables ORDER BY idx_blks_hit
735 Same thing in SQLite (requires the sqlite3 client):
738 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
740 Earthquakes worldwide magnitude 1+ in the last 24 hours:
742 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
743 column -ts, -n | barcat -f4 -u -l80%
745 External datasets, like movies per year:
747 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
748 jq .[].year | uniq -c | barcat
750 Pokémon height comparison:
752 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
753 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
755 USD/EUR exchange rate from CSV provided by the ECB:
757 curl https://sdw.ecb.europa.eu/export.do \
758 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
759 barcat -f',\K' --value-length=7
761 Total population history in XML from the World Bank:
763 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
764 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
765 barcat -f1 -H --markers=+/1e9
767 Population and other information for all countries:
769 curl http://download.geonames.org/export/dump/countryInfo.txt |
770 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
772 And of course various Git statistics, such commit count by year:
774 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
776 Or the top 3 most frequent authors with statistics over all:
778 git shortlog -sn | barcat -L3 -s
780 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
782 ( git log --pretty=%ci --since=30day | cut -b-10
783 seq 0 30 | xargs -i date +%F -d-{}day ) |
784 sort | uniq -c | awk '$1--' | barcat --spark
786 Sparkline graphics of simple input given as inline parameters:
788 barcat -_ 3 1 4 1 5 0 9 2 4
790 Misusing the spark functionality to draw a lolcat line:
792 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
796 Mischa POSLAWSKY <perl@shiar.org>