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/) {
249 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
250 my $index = $#order * $1 / 100;
251 return ($order[$index] + $order[$index + .5]) / 2;
253 elsif ($func =~ /\A-?[0-9.]+\z/) {
257 die "Unknown marker $char: $func\n";
266 color(36) for $barmark[$pos * $size] = $char;
269 state $lastmax = $maxval;
270 if ($maxval > $lastmax) {
271 print ' ' x ($lenval + $len);
274 ($lastmax - $minval) * $size + .5,
275 '-' x (($values[$nr - 1] - $minval) * $size);
277 say '+' x (($range - $lastmax) * $size + .5);
284 color(31), sprintf('%*s', $lenval, $minval),
285 color(90), '-', color(36), '+',
286 color(32), sprintf('%*s', $size * $range - 3, $maxval),
287 color(90), '-', color(36), '+',
291 while ($nr <= $limit) {
292 my $val = $values[$nr];
293 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
294 my $color = !length $val || !$opt{palette} ? undef :
295 $val == $order[0] ? $opt{palette}->[-1] : # max
296 $val == $order[-1] ? $opt{palette}->[0] : # min
297 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
298 my $indicator = $opt{indicators} && $opt{indicators}->[
299 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
300 $#{$opt{indicators}} < 2 ? 1 :
301 $val >= $order[0] ? -1 :
302 $rel * ($#{$opt{indicators}} - 1e-14) + 1
306 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
307 print color($color), $_ for $indicator;
310 print $indicator if defined $indicator;
313 $val = sprintf("%*s", $lenval,
314 $opt{reformat} ? $opt{'value-format'}->($val) : $val
316 color($color) for $val;
318 my $line = $lines[$nr] =~ s/\n/$val/r;
319 if (not length $val) {
323 printf '%-*s', $len + length($val), $line;
324 print $barmark[$_] // $opt{'graph-format'}
325 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
331 say $opt{palette} ? color(0) : '' if $opt{spark};
337 if ($opt{hidemin} or $opt{hidemax}) {
338 my $linemin = $opt{hidemin};
339 my $linemax = ($opt{hidemax} || @lines) - 1;
342 $linemax = @lines - $linemax;
344 printf '%.8g of ', $opt{'value-format'}->(
345 sum(grep {length} @values[$linemin .. $linemax]) // 0
349 my $total = sum @order;
350 printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
351 printf ' in %d values', scalar @order;
352 printf ' over %d lines', scalar @lines if @order != @lines;
353 printf(' (%s min, %s avg, %s max)',
354 color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
355 color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
356 color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
365 show_stat() if $opt{stat};
366 exit 130 if @_; # 0x80+signo
374 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
377 -a, --[no-]ascii Restrict user interface to ASCII characters
378 -C, --[no-]color Force colored output of values and bar markers
379 -f, --field=([+]N|REGEXP)
380 Compare values after a given number of whitespace
382 --header Prepend a chart axis with minimum and maximum
384 -H, --human-readable Format values using SI unit prefixes
385 --sexagesimal Convert seconds to HH:MM:SS time format
386 -t, --interval[=(N|-LINES)]
387 Output partial progress every given number of
388 seconds or input lines
389 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
390 -L, --limit[=(N|-LAST|START-[END])]
391 Stop output after a number of lines
392 --graph-format=CHAR Glyph to repeat for the graph line
393 -m, --markers=FORMAT Statistical positions to indicate on bars
394 --min=N, --max=N Bars extend from 0 or the minimum value if lower
395 --palette=(PRESET|COLORS)
396 Override colors of parsed numbers
397 -_, --spark Replace lines by sparklines
398 --indicators[=CHARS] Prefix a unicode character corresponding to each
400 -s, --stat Total statistics after all data
401 -u, --unmodified Do not reformat values, keeping leading whitespace
402 --value-length=SIZE Reserved space for numbers
403 -w, --width=COLUMNS Override the maximum number of columns to use
404 -h, --usage Overview of available options
405 --help Full pod documentation
406 -V, --version Version information
412 barcat - concatenate texts with graph to visualize values
416 B<barcat> [<options>] [<file>... | <numbers>]
420 Visualizes relative sizes of values read from input
421 (parameters, file(s) or STDIN).
422 Contents are concatenated similar to I<cat>,
423 but numbers are reformatted and a bar graph is appended to each line.
425 Don't worry, barcat does not drink and divide.
426 It can has various options for input and output (re)formatting,
427 but remains limited to one-dimensional charts.
428 For more complex graphing needs
429 you'll need a larger animal like I<gnuplot>.
435 =item -a, --[no-]ascii
437 Restrict user interface to ASCII characters,
438 replacing default UTF-8 by their closest approximation.
439 Input is always interpreted as UTF-8 and shown as is.
441 =item -C, --[no-]color
443 Force colored output of values and bar markers.
444 Defaults on if output is a tty,
445 disabled otherwise such as when piped or redirected.
446 Can also be disabled by setting I<-M>
447 or the I<NO_COLOR> environment variable.
449 =item -f, --field=([+]<number> | <regexp>)
451 Compare values after a given number of whitespace separators,
452 or matching a regular expression.
454 Unspecified or I<-f0> means values are at the start of each line.
455 With I<-f1> the second word is taken instead.
456 A string can indicate the starting position of a value
457 (such as I<-f:> if preceded by colons),
458 or capture the numbers itself,
459 for example I<-f'(\d+)'> for the first digits anywhere.
460 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
464 Prepend a chart axis with minimum and maximum values labeled.
466 =item -H, --human-readable
468 Format values using SI unit prefixes,
469 turning long numbers like I<12356789> into I<12.4M>.
470 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
471 Short integers are aligned but kept without decimal point.
475 Convert seconds to HH:MM:SS time format.
477 =item -t, --interval[=(<seconds> | -<lines>)]
479 Output partial progress every given number of seconds or input lines.
480 An update can also be forced by sending a I<SIGALRM> alarm signal.
482 =item -l, --length=[-]<size>[%]
484 Trim line contents (between number and bars)
485 to a maximum number of characters.
486 The exceeding part is replaced by an abbreviation sign,
487 unless C<--length=0>.
489 Prepend a dash (i.e. make negative) to enforce padding
490 regardless of encountered contents.
492 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
494 Stop output after a number of lines.
495 A single value indicates the last line number (like C<head>),
496 or first line counting from the bottom if negative (like C<tail>).
497 A specific range can be given by two values.
499 All input is still counted and analyzed for statistics,
500 but disregarded for padding and bar size.
502 =item --graph-format=<character>
504 Glyph to repeat for the graph line.
505 Defaults to a dash C<->.
507 =item -m, --markers=<format>
509 Statistical positions to indicate on bars.
510 A single indicator glyph precedes each position:
516 Exact value to match on the axis.
517 A vertical bar at the zero crossing is displayed by I<|0>
519 For example I<:3.14> would show a colon at pi.
521 =item <percentage>I<v>
523 Ranked value at the given percentile.
524 The default shows I<+> at I<50v> for the mean or median;
525 the middle value or average between middle values.
526 One standard deviation right of the mean is at about I<68.3v>.
527 The default includes I<< >31.73v <68.27v >>
528 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
533 the sum of all values divided by the number of counted lines.
534 Indicated by default as I<=>.
538 =item --min=<number>, --max=<number>
540 Bars extend from 0 or the minimum value if lower,
541 to the largest value encountered.
542 These options can be set to customize this range.
544 =item --palette=(<preset> | <color>...)
546 Override colors of parsed numbers.
547 Can be any CSI escape, such as I<90> for default dark grey,
548 or alternatively I<1;30> for bright black.
550 In case of additional colors,
551 the last is used for values equal to the maximum, the first for minima.
552 If unspecified, these are green and red respectively (I<31 90 32>).
553 Multiple intermediate colors will be distributed
554 relative to the size of values.
556 Predefined color schemes are named I<whites> and I<fire>,
557 or I<greys> and I<fire256> for 256-color variants.
561 Replace lines by I<sparklines>,
562 single characters (configured by C<--indicators>)
563 corresponding to input values.
565 =item --indicators[=<characters>]
567 Prefix a unicode character corresponding to each value.
568 The first specified character will be used for non-values,
569 the remaining sequence will be distributed over the range of values.
570 Unspecified, block fill glyphs U+2581-2588 will be used.
574 Total statistics after all data.
576 =item -u, --unmodified
578 Do not reformat values, keeping leading whitespace.
579 Keep original value alignment, which may be significant in some programs.
581 =item --value-length=<size>
583 Reserved space for numbers.
585 =item -w, --width=<columns>
587 Override the maximum number of columns to use.
588 Appended graphics will extend to fill up the entire screen,
589 otherwise determined by the environment variable I<COLUMNS>
590 or by running the C<tput> command.
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 -ts, -n | 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 -ts$'\t' -n | 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>