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;
32 'trim|length|l=s' => sub {
33 my ($optname, $optval) = @_;
34 $optval =~ s/%$// and $opt{trimpct}++;
35 $optval =~ m/\A-?[0-9]+\z/ or die(
36 "Value \"$optval\" invalid for option $optname",
37 " (number or percentage expected)\n"
47 my ($optname, $optval) = @_;
49 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
50 ($opt{hidemin}, $opt{hidemax}) =
51 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
52 "Value \"$optval\" invalid for option limit",
58 'graph-format=s' => sub {
59 $opt{'graph-format'} = substr $_[1], 0, 1;
66 fire => [qw( 90 31 91 33 93 97 96 )],
67 fire256=> [map {"38;5;$_"} qw(
69 202 208 214 220 226 227 228 229 230 231 159
71 whites => [qw( 1;30 0;37 1;37 )],
72 greys => [map {"38;5;$_"} 0, 232..255, 15],
73 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
74 rainbow=> [map {"38;5;$_"}
76 (map { 196 + $_*6 } 0..4), # +g
77 (map { 226 - $_*6*6 } 0..4), # -r
78 (map { 46 + $_ } 0..4), # +b
79 (map { 51 - $_*6 } 0..4), # -g
80 (map { 21 + $_*6*6 } 0..4), # +r
81 (map { 201 - $_ } 0..4), # -b
85 my @vals = split /[^0-9;]/, $_[1]
86 or die "Empty palette resulting from \"$_[1]\"\n";
95 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
96 say "barcat $mascot version $VERSION";
100 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
105 Pod::Usage::pod2usage(
106 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
109 ) or exit 64; # EX_USAGE
112 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
113 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
114 $opt{'graph-format'} //= '-';
115 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
116 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
117 if $opt{'human-readable'};
118 $opt{anchor} //= qr/\A/;
119 $opt{'value-length'} = 4 if $opt{units};
120 $opt{'value-length'} = 1 if $opt{unmodified};
121 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
122 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
123 $opt{palette} //= $opt{color} && [31, 90, 32];
124 $opt{indicators} = [split //, $opt{indicators} ||
125 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
126 ] if defined $opt{indicators} or $opt{spark};
127 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
128 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
129 and undef $opt{interval};
131 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
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
156 my (@lines, @values, @order);
158 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
161 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
163 $SIG{INT} = \&show_exit;
165 if (defined $opt{interval}) {
166 $opt{interval} ||= 1;
167 alarm $opt{interval} if $opt{interval} > 0;
170 require Tie::Array::Sorted;
171 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
172 } or warn $@, "Expect slowdown with large datasets!\n";
176 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
178 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
180 s/\A\h*// unless $opt{unmodified};
181 my $valnum = s/$valmatch/\n/ && $1;
182 push @values, $valnum;
183 push @order, $valnum if length $valnum;
184 if (defined $opt{trim} and defined $valnum) {
185 my $trimpos = abs $opt{trim};
186 $trimpos -= length $valnum if $opt{unmodified};
188 $_ = substr $_, 0, 2;
190 elsif (length > $trimpos) {
191 # cut and replace (intentional lvalue for speed, contrary to PBP)
192 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
196 show_lines() if defined $opt{interval} and $opt{interval} < 0
197 and $. % $opt{interval} == 0;
200 if ($opt{'zero-missing'}) {
201 push @values, (0) x 10;
204 $SIG{INT} = 'DEFAULT';
207 $opt{color} and defined $_[0] or return '';
208 return "\e[$_[0]m" if defined wantarray;
209 $_ = color(@_) . $_ . color(0) if defined;
215 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
217 @lines > $nr or return;
220 if (defined $opt{hidemax}) {
221 if ($opt{hidemin} and $opt{hidemin} < 0) {
222 $limit -= $opt{hidemax} - 1;
224 elsif ($opt{hidemax} <= $limit) {
225 $limit = $opt{hidemax} - 1;
229 @order = sort { $b <=> $a } @order unless tied @order;
230 my $maxval = $opt{maxval} // (
231 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
234 my $minval = $opt{minval} // min $order[-1] // (), 0;
235 my $range = $maxval - $minval;
236 my $lenval = $opt{'value-length'} // max map { length } @order;
237 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
238 max map { length $values[$_] && length $lines[$_] }
239 0 .. min $#lines, $opt{hidemax} || (); # left padding
240 my $size = defined $opt{width} && $range &&
241 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
244 if ($opt{markers} and $size > 0) {
245 for my $markspec (split /\h/, $opt{markers}) {
246 my ($char, $func) = split //, $markspec, 2;
248 if ($func eq 'avg') {
249 return sum(@order) / @order;
251 elsif ($func =~ /\A([0-9.]+)v\z/) {
252 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
253 my $index = $#order * $1 / 100;
254 return ($order[$index] + $order[$index + .5]) / 2;
256 elsif ($func =~ /\A-?[0-9.]+\z/) {
260 die "Unknown marker $char: $func\n";
269 color(36) for $barmark[$pos * $size] = $char;
272 state $lastmax = $maxval;
273 if ($maxval > $lastmax) {
274 print ' ' x ($lenval + $len);
277 ($lastmax - $minval) * $size + .5,
278 '-' x (($values[$nr - 1] - $minval) * $size);
280 say '+' x (($range - $lastmax) * $size + .5);
287 color(31), sprintf('%*s', $lenval, $minval),
288 color(90), '-', color(36), '+',
289 color(32), sprintf('%*s', $size * $range - 3, $maxval),
290 color(90), '-', color(36), '+',
294 while ($nr <= $limit) {
295 my $val = $values[$nr];
296 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
297 my $color = !length $val || !$opt{palette} ? undef :
298 $val == $order[0] ? $opt{palette}->[-1] : # max
299 $val == $order[-1] ? $opt{palette}->[0] : # min
300 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
301 my $indicator = $opt{indicators} && $opt{indicators}->[
302 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
303 $#{$opt{indicators}} < 2 ? 1 :
304 $val >= $order[0] ? -1 :
305 $rel * ($#{$opt{indicators}} - 1e-14) + 1
309 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
310 print color($color), $_ for $indicator;
313 print $indicator if defined $indicator;
316 $val = sprintf("%*s", $lenval,
317 $opt{'value-format'} ? $opt{'value-format'}->($val) : $val
319 color($color) for $val;
321 my $line = $lines[$nr] =~ s/\n/$val/r;
322 if (not length $val) {
326 printf '%-*s', $len + length($val), $line;
327 print $barmark[$_] // $opt{'graph-format'}
328 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
334 say $opt{palette} ? color(0) : '' if $opt{spark};
340 if ($opt{hidemin} or $opt{hidemax}) {
341 my $linemin = $opt{hidemin};
342 my $linemax = ($opt{hidemax} || @lines) - 1;
345 $linemax = @lines - $linemax;
347 printf '%.8g of ', $opt{'sum-format'}->(
348 sum(grep {length} @values[$linemin .. $linemax]) // 0
352 my $total = sum @order;
353 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
354 printf ' in %d values', scalar @order;
355 printf ' over %d lines', scalar @lines if @order != @lines;
356 printf(' (%s min, %s avg, %s max)',
357 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
358 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
359 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
368 show_stat() if $opt{stat};
369 exit 130 if @_; # 0x80+signo
377 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
380 -a, --[no-]ascii Restrict user interface to ASCII characters
381 -C, --[no-]color Force colored output of values and bar markers
382 -f, --field=([+]N|REGEXP)
383 Compare values after a given number of whitespace
385 --header Prepend a chart axis with minimum and maximum
387 -H, --human-readable Format values using SI unit prefixes
388 --sexagesimal Convert seconds to HH:MM:SS time format
389 -t, --interval[=(N|-LINES)]
390 Output partial progress every given number of
391 seconds or input lines
392 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
393 -L, --limit[=(N|-LAST|START-[END])]
394 Stop output after a number of lines
395 --graph-format=CHAR Glyph to repeat for the graph line
396 -m, --markers=FORMAT Statistical positions to indicate on bars
397 --min=N, --max=N Bars extend from 0 or the minimum value if lower
398 --palette=(PRESET|COLORS)
399 Override colors of parsed numbers
400 -_, --spark Replace lines by sparklines
401 --indicators[=CHARS] Prefix a unicode character corresponding to each
403 -s, --stat Total statistics after all data
404 -u, --unmodified Do not reformat values, keeping leading whitespace
405 --value-length=SIZE Reserved space for numbers
406 -w, --width=COLUMNS Override the maximum number of columns to use
407 -h, --usage Overview of available options
408 --help Full pod documentation
409 -V, --version Version information
415 barcat - concatenate texts with graph to visualize values
419 B<barcat> [<options>] [<file>... | <numbers>]
423 Visualizes relative sizes of values read from input
424 (parameters, file(s) or STDIN).
425 Contents are concatenated similar to I<cat>,
426 but numbers are reformatted and a bar graph is appended to each line.
428 Don't worry, barcat does not drink and divide.
429 It can has various options for input and output (re)formatting,
430 but remains limited to one-dimensional charts.
431 For more complex graphing needs
432 you'll need a larger animal like I<gnuplot>.
438 =item -a, --[no-]ascii
440 Restrict user interface to ASCII characters,
441 replacing default UTF-8 by their closest approximation.
442 Input is always interpreted as UTF-8 and shown as is.
444 =item -C, --[no-]color
446 Force colored output of values and bar markers.
447 Defaults on if output is a tty,
448 disabled otherwise such as when piped or redirected.
449 Can also be disabled by setting I<-M>
450 or the I<NO_COLOR> environment variable.
452 =item -f, --field=([+]<number> | <regexp>)
454 Compare values after a given number of whitespace separators,
455 or matching a regular expression.
457 Unspecified or I<-f0> means values are at the start of each line.
458 With I<-f1> the second word is taken instead.
459 A string can indicate the starting position of a value
460 (such as I<-f:> if preceded by colons),
461 or capture the numbers itself,
462 for example I<-f'(\d+)'> for the first digits anywhere.
463 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
467 Prepend a chart axis with minimum and maximum values labeled.
469 =item -H, --human-readable
471 Format values using SI unit prefixes,
472 turning long numbers like I<12356789> into I<12.4M>.
473 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
474 Short integers are aligned but kept without decimal point.
478 Convert seconds to HH:MM:SS time format.
480 =item -t, --interval[=(<seconds> | -<lines>)]
482 Output partial progress every given number of seconds or input lines.
483 An update can also be forced by sending a I<SIGALRM> alarm signal.
485 =item -l, --length=[-]<size>[%]
487 Trim line contents (between number and bars)
488 to a maximum number of characters.
489 The exceeding part is replaced by an abbreviation sign,
490 unless C<--length=0>.
492 Prepend a dash (i.e. make negative) to enforce padding
493 regardless of encountered contents.
495 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
497 Stop output after a number of lines.
498 A single value indicates the last line number (like C<head>),
499 or first line counting from the bottom if negative (like C<tail>).
500 A specific range can be given by two values.
502 All input is still counted and analyzed for statistics,
503 but disregarded for padding and bar size.
505 =item --graph-format=<character>
507 Glyph to repeat for the graph line.
508 Defaults to a dash C<->.
510 =item -m, --markers=<format>
512 Statistical positions to indicate on bars.
513 A single indicator glyph precedes each position:
519 Exact value to match on the axis.
520 A vertical bar at the zero crossing is displayed by I<|0>
522 For example I<:3.14> would show a colon at pi.
524 =item <percentage>I<v>
526 Ranked value at the given percentile.
527 The default shows I<+> at I<50v> for the mean or median;
528 the middle value or average between middle values.
529 One standard deviation right of the mean is at about I<68.3v>.
530 The default includes I<< >31.73v <68.27v >>
531 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
536 the sum of all values divided by the number of counted lines.
537 Indicated by default as I<=>.
541 =item --min=<number>, --max=<number>
543 Bars extend from 0 or the minimum value if lower,
544 to the largest value encountered.
545 These options can be set to customize this range.
547 =item --palette=(<preset> | <color>...)
549 Override colors of parsed numbers.
550 Can be any CSI escape, such as I<90> for default dark grey,
551 or alternatively I<1;30> for bright black.
553 In case of additional colors,
554 the last is used for values equal to the maximum, the first for minima.
555 If unspecified, these are green and red respectively (I<31 90 32>).
556 Multiple intermediate colors will be distributed
557 relative to the size of values.
559 Predefined color schemes are named I<whites> and I<fire>,
560 or I<greys> and I<fire256> for 256-color variants.
564 Replace lines by I<sparklines>,
565 single characters (configured by C<--indicators>)
566 corresponding to input values.
568 =item --indicators[=<characters>]
570 Prefix a unicode character corresponding to each value.
571 The first specified character will be used for non-values,
572 the remaining sequence will be distributed over the range of values.
573 Unspecified, block fill glyphs U+2581-2588 will be used.
577 Total statistics after all data.
579 =item -u, --unmodified
581 Do not reformat values, keeping leading whitespace.
582 Keep original value alignment, which may be significant in some programs.
584 =item --value-length=<size>
586 Reserved space for numbers.
588 =item -w, --width=<columns>
590 Override the maximum number of columns to use.
591 Appended graphics will extend to fill up the entire screen.
595 Overview of available options.
599 Full pod documentation
600 as rendered by perldoc.
612 seq 30 | awk '{print sin($1/10)}' | barcat
614 Compare file sizes (with human-readable numbers):
616 du -d0 -b * | barcat -H
618 Same from formatted results, selecting the first numeric value:
620 tree -s --noreport | barcat -H -f+
622 Compare media metadata, like image size or play time:
624 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
626 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
628 find -type f -print0 | xargs -0 -L1 \
629 ffprobe -show_format -of json -v error |
630 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
632 Memory usage of user processes with long names truncated:
634 ps xo rss,pid,cmd | barcat -l40
636 Monitor network latency from prefixed results:
638 ping google.com | barcat -f'time=\K' -t
640 Commonly used after counting, for example users on the current server:
642 users | tr ' ' '\n' | sort | uniq -c | barcat
644 Letter frequencies in text files:
646 cat /usr/share/games/fortunes/*.u8 |
647 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
648 sort | uniq -c | barcat
650 Number of HTTP requests per day:
652 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
654 Any kind of database query with counts, preserving returned alignment:
656 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
659 In PostgreSQL from within the client:
661 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
663 Earthquakes worldwide magnitude 1+ in the last 24 hours:
665 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
666 column -tns, | barcat -f4 -u -l80%
668 External datasets, like movies per year:
670 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
671 jq .[].year | uniq -c | barcat
673 Pokémon height comparison:
675 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
676 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
678 USD/EUR exchange rate from CSV provided by the ECB:
680 curl https://sdw.ecb.europa.eu/export.do \
681 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
682 barcat -f',\K' --value-length=7
684 Total population history in XML from the World Bank:
686 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
687 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
690 Population and other information for all countries:
692 curl http://download.geonames.org/export/dump/countryInfo.txt |
693 grep -v '^#\s' | column -tns$'\t' | barcat -f+2 -u -l150 -s
695 And of course various Git statistics, such commit count by year:
697 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
699 Or the top 3 most frequent authors with statistics over all:
701 git shortlog -sn | barcat -L3 -s
703 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
705 ( git log --pretty=%ci --since=30day | cut -b-10
706 seq 0 30 | xargs -i date +%F -d-{}day ) |
707 sort | uniq -c | awk '$1--' | barcat --spark
709 Sparkline graphics of simple input given as inline parameters:
711 barcat -_ 3 1 4 1 5 0 9 2 4
713 Misusing the spark functionality to draw a lolcat line:
715 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
719 Mischa POSLAWSKY <perl@shiar.org>