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 $maxval 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}) / $range; # 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 * $size] = $char;
274 state $lastmax = $maxval;
275 if ($maxval > $lastmax) {
276 print ' ' x ($lenval + $len);
279 ($lastmax - $minval) * $size + .5,
280 '-' x (($values[$nr - 1] - $minval) * $size);
282 say '+' x (($range - $lastmax) * $size + .5);
289 color(31), sprintf('%*s', $lenval, $minval),
290 color(90), '-', color(36), '+',
291 color(32), sprintf('%*s', $size * $range - 3, $maxval),
292 color(90), '-', color(36), '+',
296 while ($nr <= $limit) {
297 my $val = $values[$nr];
298 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
299 my $color = !length $val || !$opt{palette} ? undef :
300 $val == $order[0] ? $opt{palette}->[-1] : # max
301 $val == $order[-1] ? $opt{palette}->[0] : # min
302 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
303 my $indicator = $opt{indicators} && $opt{indicators}->[
304 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
305 $#{$opt{indicators}} < 2 ? 1 :
306 $val >= $order[0] ? -1 :
307 $rel * ($#{$opt{indicators}} - 1e-14) + 1
311 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
312 print color($color), $_ for $indicator;
315 print $indicator if defined $indicator;
318 $val = sprintf("%*s", $lenval,
319 $opt{reformat} ? $opt{'value-format'}->($val) : $val
321 color($color) for $val;
323 my $line = $lines[$nr] =~ s/\n/$val/r;
324 if (not length $val) {
328 printf '%-*s', $len + length($val), $line;
329 my $barlen = $values[$nr] || 0;
330 $barlen &&= log $barlen if $opt{log};
331 print $barmark[$_] // $opt{'graph-format'}
332 for 1 .. $size && ($barlen - $minval) * $size + .5;
338 say $opt{palette} ? color(0) : '' if $opt{spark};
344 if ($opt{hidemin} or $opt{hidemax}) {
345 my $linemin = $opt{hidemin};
346 my $linemax = ($opt{hidemax} || @lines) - 1;
349 $linemax = @lines - $linemax;
351 printf '%.8g of ', $opt{'value-format'}->(
352 sum(grep {length} @values[$linemin .. $linemax]) // 0
356 my $total = sum @order;
357 printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
358 printf ' in %d values', scalar @order;
359 printf ' over %d lines', scalar @lines if @order != @lines;
360 printf(' (%s min, %s avg, %s max)',
361 color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
362 color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
363 color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
372 show_stat() if $opt{stat};
373 exit 130 if @_; # 0x80+signo
381 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
384 -a, --[no-]ascii Restrict user interface to ASCII characters
385 -C, --[no-]color Force colored output of values and bar markers
386 -f, --field=([+]N|REGEXP)
387 Compare values after a given number of whitespace
389 --header Prepend a chart axis with minimum and maximum
391 -H, --human-readable Format values using SI unit prefixes
392 --sexagesimal Convert seconds to HH:MM:SS time format
393 -t, --interval[=(N|-LINES)]
394 Output partial progress every given number of
395 seconds or input lines
396 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
397 -L, --limit[=(N|-LAST|START-[END])]
398 Stop output after a number of lines
399 -e, --log Logarithmic (exponential) scale instead of linear
400 --graph-format=CHAR Glyph to repeat for the graph line
401 -m, --markers=FORMAT Statistical positions to indicate on bars
402 --min=N, --max=N Bars extend from 0 or the minimum value if lower
403 --palette=(PRESET|COLORS)
404 Override colors of parsed numbers
405 -_, --spark Replace lines by sparklines
406 --indicators[=CHARS] Prefix a unicode character corresponding to each
408 -s, --stat Total statistics after all data
409 -u, --unmodified Do not reformat values, keeping leading whitespace
410 --value-length=SIZE Reserved space for numbers
411 -w, --width=COLUMNS Override the maximum number of columns to use
412 -h, --usage Overview of available options
413 --help Full pod documentation
414 -V, --version Version information
420 barcat - concatenate texts with graph to visualize values
424 B<barcat> [<options>] [<file>... | <numbers>]
428 Visualizes relative sizes of values read from input
429 (parameters, file(s) or STDIN).
430 Contents are concatenated similar to I<cat>,
431 but numbers are reformatted and a bar graph is appended to each line.
433 Don't worry, barcat does not drink and divide.
434 It can has various options for input and output (re)formatting,
435 but remains limited to one-dimensional charts.
436 For more complex graphing needs
437 you'll need a larger animal like I<gnuplot>.
443 =item -a, --[no-]ascii
445 Restrict user interface to ASCII characters,
446 replacing default UTF-8 by their closest approximation.
447 Input is always interpreted as UTF-8 and shown as is.
449 =item -C, --[no-]color
451 Force colored output of values and bar markers.
452 Defaults on if output is a tty,
453 disabled otherwise such as when piped or redirected.
454 Can also be disabled by setting I<-M>
455 or the I<NO_COLOR> environment variable.
457 =item -f, --field=([+]<number> | <regexp>)
459 Compare values after a given number of whitespace separators,
460 or matching a regular expression.
462 Unspecified or I<-f0> means values are at the start of each line.
463 With I<-f1> the second word is taken instead.
464 A string can indicate the starting position of a value
465 (such as I<-f:> if preceded by colons),
466 or capture the numbers itself,
467 for example I<-f'(\d+)'> for the first digits anywhere.
468 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
472 Prepend a chart axis with minimum and maximum values labeled.
474 =item -H, --human-readable
476 Format values using SI unit prefixes,
477 turning long numbers like I<12356789> into I<12.4M>.
478 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
479 Short integers are aligned but kept without decimal point.
483 Convert seconds to HH:MM:SS time format.
485 =item -t, --interval[=(<seconds> | -<lines>)]
487 Output partial progress every given number of seconds or input lines.
488 An update can also be forced by sending a I<SIGALRM> alarm signal.
490 =item -l, --length=[-]<size>[%]
492 Trim line contents (between number and bars)
493 to a maximum number of characters.
494 The exceeding part is replaced by an abbreviation sign,
495 unless C<--length=0>.
497 Prepend a dash (i.e. make negative) to enforce padding
498 regardless of encountered contents.
500 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
502 Stop output after a number of lines.
503 A single value indicates the last line number (like C<head>),
504 or first line counting from the bottom if negative (like C<tail>).
505 A specific range can be given by two values.
507 All input is still counted and analyzed for statistics,
508 but disregarded for padding and bar size.
512 Logarithmic (I<e>xponential) scale instead of linear
513 to compare orders of magnitude.
515 =item --graph-format=<character>
517 Glyph to repeat for the graph line.
518 Defaults to a dash C<->.
520 =item -m, --markers=<format>
522 Statistical positions to indicate on bars.
523 A single indicator glyph precedes each position:
529 Exact value to match on the axis.
530 A vertical bar at the zero crossing is displayed by I<|0>
532 For example I<:3.14> would show a colon at pi.
534 =item <percentage>I<v>
536 Ranked value at the given percentile.
537 The default shows I<+> at I<50v> for the mean or median;
538 the middle value or average between middle values.
539 One standard deviation right of the mean is at about I<68.3v>.
540 The default includes I<< >31.73v <68.27v >>
541 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
546 the sum of all values divided by the number of counted lines.
547 Indicated by default as I<=>.
551 =item --min=<number>, --max=<number>
553 Bars extend from 0 or the minimum value if lower,
554 to the largest value encountered.
555 These options can be set to customize this range.
557 =item --palette=(<preset> | <color>...)
559 Override colors of parsed numbers.
560 Can be any CSI escape, such as I<90> for default dark grey,
561 or alternatively I<1;30> for bright black.
563 In case of additional colors,
564 the last is used for values equal to the maximum, the first for minima.
565 If unspecified, these are green and red respectively (I<31 90 32>).
566 Multiple intermediate colors will be distributed
567 relative to the size of values.
569 Predefined color schemes are named I<whites> and I<fire>,
570 or I<greys> and I<fire256> for 256-color variants.
574 Replace lines by I<sparklines>,
575 single characters (configured by C<--indicators>)
576 corresponding to input values.
578 =item --indicators[=<characters>]
580 Prefix a unicode character corresponding to each value.
581 The first specified character will be used for non-values,
582 the remaining sequence will be distributed over the range of values.
583 Unspecified, block fill glyphs U+2581-2588 will be used.
587 Total statistics after all data.
589 =item -u, --unmodified
591 Do not reformat values, keeping leading whitespace.
592 Keep original value alignment, which may be significant in some programs.
594 =item --value-length=<size>
596 Reserved space for numbers.
598 =item -w, --width=<columns>
600 Override the maximum number of columns to use.
601 Appended graphics will extend to fill up the entire screen,
602 otherwise determined by the environment variable I<COLUMNS>
603 or by running the C<tput> command.
607 Overview of available options.
611 Full pod documentation
612 as rendered by perldoc.
624 seq 30 | awk '{print sin($1/10)}' | barcat
626 Compare file sizes (with human-readable numbers):
628 du -d0 -b * | barcat -H
630 Same from formatted results, selecting the first numeric value:
632 tree -s --noreport | barcat -H -f+
634 Compare media metadata, like image size or play time:
636 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
638 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
640 find -type f -print0 | xargs -0 -L1 \
641 ffprobe -show_format -of json -v error |
642 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
644 Memory usage of user processes with long names truncated:
646 ps xo rss,pid,cmd | barcat -l40
648 Monitor network latency from prefixed results:
650 ping google.com | barcat -f'time=\K' -t
652 Commonly used after counting, for example users on the current server:
654 users | tr ' ' '\n' | sort | uniq -c | barcat
656 Letter frequencies in text files:
658 cat /usr/share/games/fortunes/*.u8 |
659 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
660 sort | uniq -c | barcat
662 Number of HTTP requests per day:
664 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
666 Any kind of database query results, preserving returned alignment:
668 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
671 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
673 > SELECT schemaname, relname, pg_total_relation_size(relid)
674 FROM pg_statio_user_tables ORDER BY idx_blks_hit
677 Same thing in SQLite (requires the sqlite3 client):
680 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
682 Earthquakes worldwide magnitude 1+ in the last 24 hours:
684 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
685 column -ts, -n | barcat -f4 -u -l80%
687 External datasets, like movies per year:
689 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
690 jq .[].year | uniq -c | barcat
692 Pokémon height comparison:
694 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
695 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
697 USD/EUR exchange rate from CSV provided by the ECB:
699 curl https://sdw.ecb.europa.eu/export.do \
700 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
701 barcat -f',\K' --value-length=7
703 Total population history in XML from the World Bank:
705 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
706 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
709 Population and other information for all countries:
711 curl http://download.geonames.org/export/dump/countryInfo.txt |
712 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
714 And of course various Git statistics, such commit count by year:
716 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
718 Or the top 3 most frequent authors with statistics over all:
720 git shortlog -sn | barcat -L3 -s
722 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
724 ( git log --pretty=%ci --since=30day | cut -b-10
725 seq 0 30 | xargs -i date +%F -d-{}day ) |
726 sort | uniq -c | awk '$1--' | barcat --spark
728 Sparkline graphics of simple input given as inline parameters:
730 barcat -_ 3 1 4 1 5 0 9 2 4
732 Misusing the spark functionality to draw a lolcat line:
734 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
738 Mischa POSLAWSKY <perl@shiar.org>