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'} = 6 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 {
135 sprintf('%d:%02d:%02d', $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 = $opt{'value-format'} ? $opt{'value-format'}->($val) :
317 sprintf "%*s", $lenval, $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{'sum-format'}->(
347 sum(grep {length} @values[$linemin .. $linemax]) // 0
351 my $total = sum @order;
352 printf '%s total', color(1) . $opt{'sum-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{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
357 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
358 color(32) . ($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.
594 Overview of available options.
598 Full pod documentation
599 as rendered by perldoc.
611 seq 30 | awk '{print sin($1/10)}' | barcat
613 Compare file sizes (with human-readable numbers):
615 du -d0 -b * | barcat -H
617 Same from formatted results, selecting the first numeric value:
619 tree -s --noreport | barcat -H -f+
621 Compare media metadata, like image size or play time:
623 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
625 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
627 find -type f -print0 | xargs -0 -L1 \
628 ffprobe -show_format -of json -v error |
629 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
631 Memory usage of user processes with long names truncated:
633 ps xo rss,pid,cmd | barcat -l40
635 Monitor network latency from prefixed results:
637 ping google.com | barcat -f'time=\K' -t
639 Commonly used after counting, for example users on the current server:
641 users | tr ' ' '\n' | sort | uniq -c | barcat
643 Letter frequencies in text files:
645 cat /usr/share/games/fortunes/*.u8 |
646 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
647 sort | uniq -c | barcat
649 Number of HTTP requests per day:
651 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
653 Any kind of database query with counts, preserving returned alignment:
655 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
658 In PostgreSQL from within the client:
660 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
662 Earthquakes worldwide magnitude 1+ in the last 24 hours:
664 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
665 column -tns, | barcat -f4 -u -l80%
667 External datasets, like movies per year:
669 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
670 jq .[].year | uniq -c | barcat
672 Pokémon height comparison:
674 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
675 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
677 USD/EUR exchange rate from CSV provided by the ECB:
679 curl https://sdw.ecb.europa.eu/export.do \
680 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
681 barcat -f',\K' --value-length=7
683 Total population history in XML from the World Bank:
685 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
686 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
689 Population and other information for all countries:
691 curl http://download.geonames.org/export/dump/countryInfo.txt |
692 grep -v '^#\s' | column -tns$'\t' | barcat -f+2 -u -l150 -s
694 And of course various Git statistics, such commit count by year:
696 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
698 Or the top 3 most frequent authors with statistics over all:
700 git shortlog -sn | barcat -L3 -s
702 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
704 ( git log --pretty=%ci --since=30day | cut -b-10
705 seq 0 30 | xargs -i date +%F -d-{}day ) |
706 sort | uniq -c | awk '$1--' | barcat --spark
708 Sparkline graphics of simple input given as inline parameters:
710 barcat -_ 3 1 4 1 5 0 9 2 4
712 Misusing the spark functionality to draw a lolcat line:
714 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
718 Mischa POSLAWSKY <perl@shiar.org>