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;
31 'trim|length|l=s' => sub {
32 my ($optname, $optval) = @_;
33 $optval =~ s/%$// and $opt{trimpct}++;
34 $optval =~ m/\A-?[0-9]+\z/ or die(
35 "Value \"$optval\" invalid for option $optname",
36 " (number or percentage expected)\n"
46 my ($optname, $optval) = @_;
48 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
49 ($opt{hidemin}, $opt{hidemax}) =
50 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
51 "Value \"$optval\" invalid for option limit",
57 'graph-format=s' => sub {
58 $opt{'graph-format'} = substr $_[1], 0, 1;
65 fire => [qw( 90 31 91 33 93 97 96 )],
66 fire256=> [map {"38;5;$_"} qw(
68 202 208 214 220 226 227 228 229 230 231 159
70 whites => [qw( 1;30 0;37 1;37 )],
71 greys => [map {"38;5;$_"} 0, 232..255, 15],
72 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
73 rainbow=> [map {"38;5;$_"}
75 (map { 196 + $_*6 } 0..4), # +g
76 (map { 226 - $_*6*6 } 0..4), # -r
77 (map { 46 + $_ } 0..4), # +b
78 (map { 51 - $_*6 } 0..4), # -g
79 (map { 21 + $_*6*6 } 0..4), # +r
80 (map { 201 - $_ } 0..4), # -b
84 my @vals = split /[^0-9;]/, $_[1]
85 or die "Empty palette resulting from \"$_[1]\"\n";
94 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
95 say "barcat $mascot version $VERSION";
99 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
104 Pod::Usage::pod2usage(
105 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
108 ) or exit 64; # EX_USAGE
111 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
112 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
113 $opt{'graph-format'} //= '-';
114 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
115 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
116 if $opt{'human-readable'};
117 $opt{anchor} //= qr/\A/;
118 $opt{'value-length'} = 6 if $opt{units};
119 $opt{'value-length'} = 1 if $opt{unmodified};
120 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
121 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
122 $opt{palette} //= $opt{color} && [31, 90, 32];
123 $opt{indicators} = [split //, $opt{indicators} ||
124 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
125 ] if defined $opt{indicators} or $opt{spark};
126 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
127 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
128 and undef $opt{interval};
130 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
131 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
132 $opt{'value-format'} = $opt{units} && sub {
134 log(abs $_[0] || 1) / log(10)
135 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
136 + 1e-15 # float imprecision
138 my $decimal = ($unit % 3) == ($unit < 0);
139 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
140 $decimal = ($unit % 3) == ($unit < 0);
141 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
143 3 + ($_[0] < 0), # digits plus optional negative sign
145 $_[0] / 1000 ** int($unit/3), # number
146 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
147 $opt{units}->[$unit/3] # suffix
152 my (@lines, @values, @order);
154 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
157 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
159 $SIG{INT} = \&show_exit;
161 if (defined $opt{interval}) {
162 $opt{interval} ||= 1;
163 alarm $opt{interval} if $opt{interval} > 0;
166 require Tie::Array::Sorted;
167 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
168 } or warn $@, "Expect slowdown with large datasets!\n";
172 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
174 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
176 s/\A\h*// unless $opt{unmodified};
177 my $valnum = s/$valmatch/\n/ && $1;
178 push @values, $valnum;
179 push @order, $valnum if length $valnum;
180 if (defined $opt{trim} and defined $valnum) {
181 my $trimpos = abs $opt{trim};
182 $trimpos -= length $valnum if $opt{unmodified};
184 $_ = substr $_, 0, 2;
186 elsif (length > $trimpos) {
187 # cut and replace (intentional lvalue for speed, contrary to PBP)
188 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
192 show_lines() if defined $opt{interval} and $opt{interval} < 0
193 and $. % $opt{interval} == 0;
196 if ($opt{'zero-missing'}) {
197 push @values, (0) x 10;
200 $SIG{INT} = 'DEFAULT';
203 $opt{color} and defined $_[0] or return '';
204 return "\e[$_[0]m" if defined wantarray;
205 $_ = color(@_) . $_ . color(0) if defined;
211 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
213 @lines > $nr or return;
216 if (defined $opt{hidemax}) {
217 if ($opt{hidemin} and $opt{hidemin} < 0) {
218 $limit -= $opt{hidemax} - 1;
220 elsif ($opt{hidemax} <= $limit) {
221 $limit = $opt{hidemax} - 1;
225 @order = sort { $b <=> $a } @order unless tied @order;
226 my $maxval = $opt{maxval} // (
227 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
230 my $minval = $opt{minval} // min $order[-1] // (), 0;
231 my $range = $maxval - $minval;
232 my $lenval = $opt{'value-length'} // max map { length } @order;
233 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
234 max map { length $values[$_] && length $lines[$_] }
235 0 .. min $#lines, $opt{hidemax} || (); # left padding
236 my $size = defined $opt{width} && $range &&
237 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
240 if ($opt{markers} and $size > 0) {
241 for my $markspec (split /\h/, $opt{markers}) {
242 my ($char, $func) = split //, $markspec, 2;
244 if ($func eq 'avg') {
245 return sum(@order) / @order;
247 elsif ($func =~ /\A([0-9.]+)v\z/) {
248 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
249 my $index = $#order * $1 / 100;
250 return ($order[$index] + $order[$index + .5]) / 2;
252 elsif ($func =~ /\A-?[0-9.]+\z/) {
256 die "Unknown marker $char: $func\n";
265 color(36) for $barmark[$pos * $size] = $char;
268 state $lastmax = $maxval;
269 if ($maxval > $lastmax) {
270 print ' ' x ($lenval + $len);
273 ($lastmax - $minval) * $size + .5,
274 '-' x (($values[$nr - 1] - $minval) * $size);
276 say '+' x (($range - $lastmax) * $size + .5);
283 color(31), sprintf('%*s', $lenval, $minval),
284 color(90), '-', color(36), '+',
285 color(32), sprintf('%*s', $size * $range - 3, $maxval),
286 color(90), '-', color(36), '+',
290 while ($nr <= $limit) {
291 my $val = $values[$nr];
292 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
293 my $color = !length $val || !$opt{palette} ? undef :
294 $val == $order[0] ? $opt{palette}->[-1] : # max
295 $val == $order[-1] ? $opt{palette}->[0] : # min
296 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
297 my $indicator = $opt{indicators} && $opt{indicators}->[
298 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
299 $#{$opt{indicators}} < 2 ? 1 :
300 $val >= $order[0] ? -1 :
301 $rel * ($#{$opt{indicators}} - 1e-14) + 1
305 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
306 print color($color), $_ for $indicator;
309 print $indicator if defined $indicator;
312 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
313 sprintf "%*s", $lenval, $val;
314 color($color) for $val;
316 my $line = $lines[$nr] =~ s/\n/$val/r;
317 if (not length $val) {
321 printf '%-*s', $len + length($val), $line;
322 print $barmark[$_] // $opt{'graph-format'}
323 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
329 say $opt{palette} ? color(0) : '' if $opt{spark};
335 if ($opt{hidemin} or $opt{hidemax}) {
336 my $linemin = $opt{hidemin};
337 my $linemax = ($opt{hidemax} || @lines) - 1;
340 $linemax = @lines - $linemax;
342 printf '%.8g of ', $opt{'sum-format'}->(
343 sum(grep {length} @values[$linemin .. $linemax]) // 0
347 my $total = sum @order;
348 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
349 printf ' in %d values', scalar @order;
350 printf ' over %d lines', scalar @lines if @order != @lines;
351 printf(' (%s min, %s avg, %s max)',
352 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
353 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
354 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
363 show_stat() if $opt{stat};
364 exit 130 if @_; # 0x80+signo
372 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
375 -a, --[no-]ascii Restrict user interface to ASCII characters
376 -C, --[no-]color Force colored output of values and bar markers
377 -f, --field=([+]N|REGEXP)
378 Compare values after a given number of whitespace
380 --header Prepend a chart axis with minimum and maximum
382 -H, --human-readable Format values using SI unit prefixes
383 -t, --interval[=(N|-LINES)]
384 Output partial progress every given number of
385 seconds or input lines
386 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
387 -L, --limit[=(N|-LAST|START-[END])]
388 Stop output after a number of lines
389 --graph-format=CHAR Glyph to repeat for the graph line
390 -m, --markers=FORMAT Statistical positions to indicate on bars
391 --min=N, --max=N Bars extend from 0 or the minimum value if lower
392 --palette=(PRESET|COLORS)
393 Override colors of parsed numbers
394 -_, --spark Replace lines by sparklines
395 --indicators[=CHARS] Prefix a unicode character corresponding to each
397 -s, --stat Total statistics after all data
398 -u, --unmodified Do not reformat values, keeping leading whitespace
399 --value-length=SIZE Reserved space for numbers
400 -w, --width=COLUMNS Override the maximum number of columns to use
401 -h, --usage Overview of available options
402 --help Full pod documentation
403 -V, --version Version information
409 barcat - concatenate texts with graph to visualize values
413 B<barcat> [<options>] [<file>... | <numbers>]
417 Visualizes relative sizes of values read from input
418 (parameters, file(s) or STDIN).
419 Contents are concatenated similar to I<cat>,
420 but numbers are reformatted and a bar graph is appended to each line.
422 Don't worry, barcat does not drink and divide.
423 It can has various options for input and output (re)formatting,
424 but remains limited to one-dimensional charts.
425 For more complex graphing needs
426 you'll need a larger animal like I<gnuplot>.
432 =item -a, --[no-]ascii
434 Restrict user interface to ASCII characters,
435 replacing default UTF-8 by their closest approximation.
436 Input is always interpreted as UTF-8 and shown as is.
438 =item -C, --[no-]color
440 Force colored output of values and bar markers.
441 Defaults on if output is a tty,
442 disabled otherwise such as when piped or redirected.
443 Can also be disabled by setting I<-M>
444 or the I<NO_COLOR> environment variable.
446 =item -f, --field=([+]<number> | <regexp>)
448 Compare values after a given number of whitespace separators,
449 or matching a regular expression.
451 Unspecified or I<-f0> means values are at the start of each line.
452 With I<-f1> the second word is taken instead.
453 A string can indicate the starting position of a value
454 (such as I<-f:> if preceded by colons),
455 or capture the numbers itself,
456 for example I<-f'(\d+)'> for the first digits anywhere.
457 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
461 Prepend a chart axis with minimum and maximum values labeled.
463 =item -H, --human-readable
465 Format values using SI unit prefixes,
466 turning long numbers like I<12356789> into I<12.4M>.
467 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
468 Short integers are aligned but kept without decimal point.
470 =item -t, --interval[=(<seconds> | -<lines>)]
472 Output partial progress every given number of seconds or input lines.
473 An update can also be forced by sending a I<SIGALRM> alarm signal.
475 =item -l, --length=[-]<size>[%]
477 Trim line contents (between number and bars)
478 to a maximum number of characters.
479 The exceeding part is replaced by an abbreviation sign,
480 unless C<--length=0>.
482 Prepend a dash (i.e. make negative) to enforce padding
483 regardless of encountered contents.
485 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
487 Stop output after a number of lines.
488 A single value indicates the last line number (like C<head>),
489 or first line counting from the bottom if negative (like C<tail>).
490 A specific range can be given by two values.
492 All input is still counted and analyzed for statistics,
493 but disregarded for padding and bar size.
495 =item --graph-format=<character>
497 Glyph to repeat for the graph line.
498 Defaults to a dash C<->.
500 =item -m, --markers=<format>
502 Statistical positions to indicate on bars.
503 A single indicator glyph precedes each position:
509 Exact value to match on the axis.
510 A vertical bar at the zero crossing is displayed by I<|0>
512 For example I<:3.14> would show a colon at pi.
514 =item <percentage>I<v>
516 Ranked value at the given percentile.
517 The default shows I<+> at I<50v> for the mean or median;
518 the middle value or average between middle values.
519 One standard deviation right of the mean is at about I<68.3v>.
520 The default includes I<< >31.73v <68.27v >>
521 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
526 the sum of all values divided by the number of counted lines.
527 Indicated by default as I<=>.
531 =item --min=<number>, --max=<number>
533 Bars extend from 0 or the minimum value if lower,
534 to the largest value encountered.
535 These options can be set to customize this range.
537 =item --palette=(<preset> | <color>...)
539 Override colors of parsed numbers.
540 Can be any CSI escape, such as I<90> for default dark grey,
541 or alternatively I<1;30> for bright black.
543 In case of additional colors,
544 the last is used for values equal to the maximum, the first for minima.
545 If unspecified, these are green and red respectively (I<31 90 32>).
546 Multiple intermediate colors will be distributed
547 relative to the size of values.
549 Predefined color schemes are named I<whites> and I<fire>,
550 or I<greys> and I<fire256> for 256-color variants.
554 Replace lines by I<sparklines>,
555 single characters (configured by C<--indicators>)
556 corresponding to input values.
558 =item --indicators[=<characters>]
560 Prefix a unicode character corresponding to each value.
561 The first specified character will be used for non-values,
562 the remaining sequence will be distributed over the range of values.
563 Unspecified, block fill glyphs U+2581-2588 will be used.
567 Total statistics after all data.
569 =item -u, --unmodified
571 Do not reformat values, keeping leading whitespace.
572 Keep original value alignment, which may be significant in some programs.
574 =item --value-length=<size>
576 Reserved space for numbers.
578 =item -w, --width=<columns>
580 Override the maximum number of columns to use.
581 Appended graphics will extend to fill up the entire screen.
585 Overview of available options.
589 Full pod documentation
590 as rendered by perldoc.
602 seq 30 | awk '{print sin($1/10)}' | barcat
604 Compare file sizes (with human-readable numbers):
606 du -d0 -b * | barcat -H
608 Same from formatted results, selecting the first numeric value:
610 tree -s --noreport | barcat -H -f+
612 Compare media metadata, like image size or play time:
614 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
616 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat -H
618 find -type f -print0 | xargs -0 -L1 \
619 ffprobe -show_format -of json -v error |
620 jq -r '[.format|.duration,.bit_rate,.filename]|join(" ")' | barcat -H
622 Memory usage of user processes with long names truncated:
624 ps xo rss,pid,cmd | barcat -l40
626 Monitor network latency from prefixed results:
628 ping google.com | barcat -f'time=\K' -t
630 Commonly used after counting, for example users on the current server:
632 users | tr ' ' '\n' | sort | uniq -c | barcat
634 Letter frequencies in text files:
636 cat /usr/share/games/fortunes/*.u8 |
637 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
638 sort | uniq -c | barcat
640 Number of HTTP requests per day:
642 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
644 Any kind of database query with counts, preserving returned alignment:
646 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
649 In PostgreSQL from within the client:
651 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
653 Earthquakes worldwide magnitude 1+ in the last 24 hours:
655 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
656 column -tns, | barcat -f4 -u -l80%
658 External datasets, like movies per year:
660 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
661 jq .[].year | uniq -c | barcat
663 Pokémon height comparison:
665 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
666 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
668 USD/EUR exchange rate from CSV provided by the ECB:
670 curl https://sdw.ecb.europa.eu/export.do \
671 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
672 barcat -f',\K' --value-length=7
674 Total population history in XML from the World Bank:
676 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
677 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
680 Population and other information for all countries:
682 curl http://download.geonames.org/export/dump/countryInfo.txt |
683 grep -v '^#\s' | column -tns$'\t' | barcat -f+2 -u -l150 -s
685 And of course various Git statistics, such commit count by year:
687 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
689 Or the top 3 most frequent authors with statistics over all:
691 git shortlog -sn | barcat -L3 -s
693 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
695 ( git log --pretty=%ci --since=30day | cut -b-10
696 seq 0 30 | xargs -i date +%F -d-{}day ) |
697 sort | uniq -c | awk '$1--' | barcat --spark
699 Sparkline graphics of simple input given as inline parameters:
701 barcat -_ 3 1 4 1 5 0 9 2 4
703 Misusing the spark functionality to draw a lolcat line:
705 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
709 Mischa POSLAWSKY <perl@shiar.org>