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",
59 'graph-format=s' => sub {
60 $opt{'graph-format'} = substr $_[1], 0, 1;
67 fire => [qw( 90 31 91 33 93 97 96 )],
68 fire256=> [map {"38;5;$_"} qw(
70 202 208 214 220 226 227 228 229 230 231 159
72 whites => [qw( 1;30 0;37 1;37 )],
73 greys => [map {"38;5;$_"} 0, 232..255, 15],
74 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
75 rainbow=> [map {"38;5;$_"}
77 (map { 196 + $_*6 } 0..4), # +g
78 (map { 226 - $_*6*6 } 0..4), # -r
79 (map { 46 + $_ } 0..4), # +b
80 (map { 51 - $_*6 } 0..4), # -g
81 (map { 21 + $_*6*6 } 0..4), # +r
82 (map { 201 - $_ } 0..4), # -b
86 my @vals = split /[^0-9;]/, $_[1]
87 or die "Empty palette resulting from \"$_[1]\"\n";
96 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
97 say "barcat $mascot version $VERSION";
101 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
106 Pod::Usage::pod2usage(
107 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
110 ) or exit 64; # EX_USAGE
113 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
114 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
115 $opt{'graph-format'} //= '-';
116 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
117 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
118 if $opt{'human-readable'};
119 $opt{anchor} //= qr/\A/;
120 $opt{'value-length'} = 4 if $opt{units};
121 $opt{'value-length'} = 1 if $opt{unmodified};
122 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
123 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
124 $opt{palette} //= $opt{color} && [31, 90, 32];
125 $opt{indicators} = [split //, $opt{indicators} ||
126 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
127 ] if defined $opt{indicators} or $opt{spark};
128 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
129 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
130 and undef $opt{interval};
132 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
133 $opt{'value-format'} = $opt{sexagesimal} ? sub {
134 my $s = abs($_[0]) + .5;
135 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
136 } : $opt{units} && sub {
138 log(abs $_[0] || 1) / log(10)
139 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
140 + 1e-15 # float imprecision
142 my $decimal = ($unit % 3) == ($unit < 0);
143 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
144 $decimal = ($unit % 3) == ($unit < 0);
145 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
147 3 + ($_[0] < 0), # digits plus optional negative sign
149 $_[0] / 1000 ** int($unit/3), # number
150 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
151 $opt{units}->[$unit/3] # suffix
153 } and $opt{reformat}++;
154 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
157 my (@lines, @values, @order);
159 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
162 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
164 $SIG{INT} = \&show_exit;
166 if (defined $opt{interval}) {
167 $opt{interval} ||= 1;
168 alarm $opt{interval} if $opt{interval} > 0;
171 require Tie::Array::Sorted;
172 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
173 } or warn $@, "Expect slowdown with large datasets!\n";
177 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
179 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
181 s/\A\h*// unless $opt{unmodified};
182 my $valnum = s/$valmatch/\n/ && $1;
183 push @values, $valnum;
184 push @order, $valnum if length $valnum;
185 if (defined $opt{trim} and defined $valnum) {
186 my $trimpos = abs $opt{trim};
187 $trimpos -= length $valnum if $opt{unmodified};
189 $_ = substr $_, 0, 2;
191 elsif (length > $trimpos) {
192 # cut and replace (intentional lvalue for speed, contrary to PBP)
193 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
197 show_lines() if defined $opt{interval} and $opt{interval} < 0
198 and $. % $opt{interval} == 0;
201 $SIG{INT} = 'DEFAULT';
204 $opt{color} and defined $_[0] or return '';
205 return "\e[$_[0]m" if defined wantarray;
206 $_ = color(@_) . $_ . color(0) if defined;
212 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
214 @lines > $nr or return;
217 if (defined $opt{hidemax}) {
218 if ($opt{hidemin} and $opt{hidemin} < 0) {
219 $limit -= $opt{hidemax} - 1;
221 elsif ($opt{hidemax} <= $limit) {
222 $limit = $opt{hidemax} - 1;
226 @order = sort { $b <=> $a } @order unless tied @order;
227 my $maxval = $opt{maxval} // (
228 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
231 my $minval = $opt{minval} // min $order[-1] // (), 0;
232 my $range = $maxval - $minval;
233 my $lenval = $opt{'value-length'} // max map { length } @order;
234 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
235 max map { length $values[$_] && length $lines[$_] }
236 0 .. min $#lines, $opt{hidemax} || (); # left padding
237 my $size = defined $opt{width} && $range &&
238 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
241 if ($opt{markers} and $size > 0) {
242 for my $markspec (split /\h/, $opt{markers}) {
243 my ($char, $func) = split //, $markspec, 2;
245 if ($func eq 'avg') {
246 return sum(@order) / @order;
248 elsif ($func =~ /\A([0-9.]+)v\z/) {
250 "Invalid marker $char: percentile $1 out of bounds\n"
252 my $index = $#order * $1 / 100;
253 return ($order[$index] + $order[$index + .5]) / 2;
255 elsif ($func =~ /\A-?[0-9.]+\z/) {
259 die "Unknown marker $char: $func\n";
268 color(36) for $barmark[$pos * $size] = $char;
271 state $lastmax = $maxval;
272 if ($maxval > $lastmax) {
273 print ' ' x ($lenval + $len);
276 ($lastmax - $minval) * $size + .5,
277 '-' x (($values[$nr - 1] - $minval) * $size);
279 say '+' x (($range - $lastmax) * $size + .5);
286 color(31), sprintf('%*s', $lenval, $minval),
287 color(90), '-', color(36), '+',
288 color(32), sprintf('%*s', $size * $range - 3, $maxval),
289 color(90), '-', color(36), '+',
293 while ($nr <= $limit) {
294 my $val = $values[$nr];
295 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
296 my $color = !length $val || !$opt{palette} ? undef :
297 $val == $order[0] ? $opt{palette}->[-1] : # max
298 $val == $order[-1] ? $opt{palette}->[0] : # min
299 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
300 my $indicator = $opt{indicators} && $opt{indicators}->[
301 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
302 $#{$opt{indicators}} < 2 ? 1 :
303 $val >= $order[0] ? -1 :
304 $rel * ($#{$opt{indicators}} - 1e-14) + 1
308 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
309 print color($color), $_ for $indicator;
312 print $indicator if defined $indicator;
315 $val = sprintf("%*s", $lenval,
316 $opt{reformat} ? $opt{'value-format'}->($val) : $val
318 color($color) for $val;
320 my $line = $lines[$nr] =~ s/\n/$val/r;
321 if (not length $val) {
325 printf '%-*s', $len + length($val), $line;
326 print $barmark[$_] // $opt{'graph-format'}
327 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
333 say $opt{palette} ? color(0) : '' if $opt{spark};
339 if ($opt{hidemin} or $opt{hidemax}) {
340 my $linemin = $opt{hidemin};
341 my $linemax = ($opt{hidemax} || @lines) - 1;
344 $linemax = @lines - $linemax;
346 printf '%.8g of ', $opt{'value-format'}->(
347 sum(grep {length} @values[$linemin .. $linemax]) // 0
351 my $total = sum @order;
352 printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
353 printf ' in %d values', scalar @order;
354 printf ' over %d lines', scalar @lines if @order != @lines;
355 printf(' (%s min, %s avg, %s max)',
356 color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
357 color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
358 color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
367 show_stat() if $opt{stat};
368 exit 130 if @_; # 0x80+signo
376 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
379 -a, --[no-]ascii Restrict user interface to ASCII characters
380 -C, --[no-]color Force colored output of values and bar markers
381 -f, --field=([+]N|REGEXP)
382 Compare values after a given number of whitespace
384 --header Prepend a chart axis with minimum and maximum
386 -H, --human-readable Format values using SI unit prefixes
387 --sexagesimal Convert seconds to HH:MM:SS time format
388 -t, --interval[=(N|-LINES)]
389 Output partial progress every given number of
390 seconds or input lines
391 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
392 -L, --limit[=(N|-LAST|START-[END])]
393 Stop output after a number of lines
394 --graph-format=CHAR Glyph to repeat for the graph line
395 -m, --markers=FORMAT Statistical positions to indicate on bars
396 --min=N, --max=N Bars extend from 0 or the minimum value if lower
397 --palette=(PRESET|COLORS)
398 Override colors of parsed numbers
399 -_, --spark Replace lines by sparklines
400 --indicators[=CHARS] Prefix a unicode character corresponding to each
402 -s, --stat Total statistics after all data
403 -u, --unmodified Do not reformat values, keeping leading whitespace
404 --value-length=SIZE Reserved space for numbers
405 -w, --width=COLUMNS Override the maximum number of columns to use
406 -h, --usage Overview of available options
407 --help Full pod documentation
408 -V, --version Version information
414 barcat - concatenate texts with graph to visualize values
418 B<barcat> [<options>] [<file>... | <numbers>]
422 Visualizes relative sizes of values read from input
423 (parameters, file(s) or STDIN).
424 Contents are concatenated similar to I<cat>,
425 but numbers are reformatted and a bar graph is appended to each line.
427 Don't worry, barcat does not drink and divide.
428 It can has various options for input and output (re)formatting,
429 but remains limited to one-dimensional charts.
430 For more complex graphing needs
431 you'll need a larger animal like I<gnuplot>.
437 =item -a, --[no-]ascii
439 Restrict user interface to ASCII characters,
440 replacing default UTF-8 by their closest approximation.
441 Input is always interpreted as UTF-8 and shown as is.
443 =item -C, --[no-]color
445 Force colored output of values and bar markers.
446 Defaults on if output is a tty,
447 disabled otherwise such as when piped or redirected.
448 Can also be disabled by setting I<-M>
449 or the I<NO_COLOR> environment variable.
451 =item -f, --field=([+]<number> | <regexp>)
453 Compare values after a given number of whitespace separators,
454 or matching a regular expression.
456 Unspecified or I<-f0> means values are at the start of each line.
457 With I<-f1> the second word is taken instead.
458 A string can indicate the starting position of a value
459 (such as I<-f:> if preceded by colons),
460 or capture the numbers itself,
461 for example I<-f'(\d+)'> for the first digits anywhere.
462 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
466 Prepend a chart axis with minimum and maximum values labeled.
468 =item -H, --human-readable
470 Format values using SI unit prefixes,
471 turning long numbers like I<12356789> into I<12.4M>.
472 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
473 Short integers are aligned but kept without decimal point.
477 Convert seconds to HH:MM:SS time format.
479 =item -t, --interval[=(<seconds> | -<lines>)]
481 Output partial progress every given number of seconds or input lines.
482 An update can also be forced by sending a I<SIGALRM> alarm signal.
484 =item -l, --length=[-]<size>[%]
486 Trim line contents (between number and bars)
487 to a maximum number of characters.
488 The exceeding part is replaced by an abbreviation sign,
489 unless C<--length=0>.
491 Prepend a dash (i.e. make negative) to enforce padding
492 regardless of encountered contents.
494 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
496 Stop output after a number of lines.
497 A single value indicates the last line number (like C<head>),
498 or first line counting from the bottom if negative (like C<tail>).
499 A specific range can be given by two values.
501 All input is still counted and analyzed for statistics,
502 but disregarded for padding and bar size.
504 =item --graph-format=<character>
506 Glyph to repeat for the graph line.
507 Defaults to a dash C<->.
509 =item -m, --markers=<format>
511 Statistical positions to indicate on bars.
512 A single indicator glyph precedes each position:
518 Exact value to match on the axis.
519 A vertical bar at the zero crossing is displayed by I<|0>
521 For example I<:3.14> would show a colon at pi.
523 =item <percentage>I<v>
525 Ranked value at the given percentile.
526 The default shows I<+> at I<50v> for the mean or median;
527 the middle value or average between middle values.
528 One standard deviation right of the mean is at about I<68.3v>.
529 The default includes I<< >31.73v <68.27v >>
530 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
535 the sum of all values divided by the number of counted lines.
536 Indicated by default as I<=>.
540 =item --min=<number>, --max=<number>
542 Bars extend from 0 or the minimum value if lower,
543 to the largest value encountered.
544 These options can be set to customize this range.
546 =item --palette=(<preset> | <color>...)
548 Override colors of parsed numbers.
549 Can be any CSI escape, such as I<90> for default dark grey,
550 or alternatively I<1;30> for bright black.
552 In case of additional colors,
553 the last is used for values equal to the maximum, the first for minima.
554 If unspecified, these are green and red respectively (I<31 90 32>).
555 Multiple intermediate colors will be distributed
556 relative to the size of values.
558 Predefined color schemes are named I<whites> and I<fire>,
559 or I<greys> and I<fire256> for 256-color variants.
563 Replace lines by I<sparklines>,
564 single characters (configured by C<--indicators>)
565 corresponding to input values.
567 =item --indicators[=<characters>]
569 Prefix a unicode character corresponding to each value.
570 The first specified character will be used for non-values,
571 the remaining sequence will be distributed over the range of values.
572 Unspecified, block fill glyphs U+2581-2588 will be used.
576 Total statistics after all data.
578 =item -u, --unmodified
580 Do not reformat values, keeping leading whitespace.
581 Keep original value alignment, which may be significant in some programs.
583 =item --value-length=<size>
585 Reserved space for numbers.
587 =item -w, --width=<columns>
589 Override the maximum number of columns to use.
590 Appended graphics will extend to fill up the entire screen,
591 otherwise determined by the environment variable I<COLUMNS>
592 or by running the C<tput> command.
596 Overview of available options.
600 Full pod documentation
601 as rendered by perldoc.
613 seq 30 | awk '{print sin($1/10)}' | barcat
615 Compare file sizes (with human-readable numbers):
617 du -d0 -b * | barcat -H
619 Same from formatted results, selecting the first numeric value:
621 tree -s --noreport | barcat -H -f+
623 Compare media metadata, like image size or play time:
625 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
627 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
629 find -type f -print0 | xargs -0 -L1 \
630 ffprobe -show_format -of json -v error |
631 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
633 Memory usage of user processes with long names truncated:
635 ps xo rss,pid,cmd | barcat -l40
637 Monitor network latency from prefixed results:
639 ping google.com | barcat -f'time=\K' -t
641 Commonly used after counting, for example users on the current server:
643 users | tr ' ' '\n' | sort | uniq -c | barcat
645 Letter frequencies in text files:
647 cat /usr/share/games/fortunes/*.u8 |
648 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
649 sort | uniq -c | barcat
651 Number of HTTP requests per day:
653 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
655 Any kind of database query results, preserving returned alignment:
657 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
660 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
662 > SELECT schemaname, relname, pg_total_relation_size(relid)
663 FROM pg_statio_user_tables ORDER BY idx_blks_hit
666 Same thing in SQLite (requires the sqlite3 client):
669 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
671 Earthquakes worldwide magnitude 1+ in the last 24 hours:
673 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
674 column -ts, -n | barcat -f4 -u -l80%
676 External datasets, like movies per year:
678 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
679 jq .[].year | uniq -c | barcat
681 Pokémon height comparison:
683 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
684 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
686 USD/EUR exchange rate from CSV provided by the ECB:
688 curl https://sdw.ecb.europa.eu/export.do \
689 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
690 barcat -f',\K' --value-length=7
692 Total population history in XML from the World Bank:
694 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
695 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
698 Population and other information for all countries:
700 curl http://download.geonames.org/export/dump/countryInfo.txt |
701 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -u -l150 -s
703 And of course various Git statistics, such commit count by year:
705 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
707 Or the top 3 most frequent authors with statistics over all:
709 git shortlog -sn | barcat -L3 -s
711 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
713 ( git log --pretty=%ci --since=30day | cut -b-10
714 seq 0 30 | xargs -i date +%F -d-{}day ) |
715 sort | uniq -c | awk '$1--' | barcat --spark
717 Sparkline graphics of simple input given as inline parameters:
719 barcat -_ 3 1 4 1 5 0 9 2 4
721 Misusing the spark functionality to draw a lolcat line:
723 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
727 Mischa POSLAWSKY <perl@shiar.org>