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/;
23 s/\A[+]([0-9]*)\z/(?:\\d+\\D+\\b){$1}\\K \\s* (?=\\d)/;
24 $opt{anchor} = qr/$_/;
25 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
29 'trim|length|l=s' => sub {
30 my ($optname, $optval) = @_;
31 $optval =~ s/%$// and $opt{trimpct}++;
32 $optval =~ m/\A-?[0-9]+\z/ or die(
33 "Value \"$optval\" invalid for option $optname",
34 " (number or percentage expected)\n"
44 my ($optname, $optval) = @_;
46 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
47 ($opt{hidemin}, $opt{hidemax}) =
48 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
49 "Value \"$optval\" invalid for option limit",
55 'graph-format=s' => sub {
56 $opt{'graph-format'} = substr $_[1], 0, 1;
63 fire => [qw( 90 31 91 33 93 97 96 )],
64 fire256=> [map {"38;5;$_"} qw(
66 202 208 214 220 226 227 228 229 230 231 159
68 whites => [qw( 1;30 0;37 1;37 )],
69 greys => [map {"38;5;$_"} 0, 232..255, 15],
70 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
71 rainbow=> [map {"38;5;$_"}
73 (map { 196 + $_*6 } 0..4), # +g
74 (map { 226 - $_*6*6 } 0..4), # -r
75 (map { 46 + $_ } 0..4), # +b
76 (map { 51 - $_*6 } 0..4), # -g
77 (map { 21 + $_*6*6 } 0..4), # +r
78 (map { 201 - $_ } 0..4), # -b
82 my @vals = split /[^0-9;]/, $_[1]
83 or die "Empty palette resulting from \"$_[1]\"\n";
92 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
93 say "barcat $mascot version $VERSION";
97 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
102 Pod::Usage::pod2usage(
103 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
106 ) or exit 64; # EX_USAGE
109 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
110 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
111 $opt{'graph-format'} //= '-';
112 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
113 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
114 if $opt{'human-readable'};
115 $opt{anchor} //= qr/\A/;
116 $opt{'value-length'} = 6 if $opt{units};
117 $opt{'value-length'} = 1 if $opt{unmodified};
118 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
119 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
120 $opt{palette} //= $opt{color} && [31, 90, 32];
121 $opt{indicators} = [split //, $opt{indicators} ||
122 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
123 ] if defined $opt{indicators} or $opt{spark};
124 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
125 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
126 and undef $opt{interval};
128 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
129 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
130 $opt{'value-format'} = $opt{units} && sub {
132 log(abs $_[0] || 1) / log(10)
133 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
134 + 1e-15 # float imprecision
136 my $decimal = ($unit % 3) == ($unit < 0);
137 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
138 $decimal = ($unit % 3) == ($unit < 0);
139 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
141 3 + ($_[0] < 0), # digits plus optional negative sign
143 $_[0] / 1000 ** int($unit/3), # number
144 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
145 $opt{units}->[$unit/3] # suffix
150 my (@lines, @values, @order);
152 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
155 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
157 $SIG{INT} = \&show_exit;
159 if (defined $opt{interval}) {
160 $opt{interval} ||= 1;
161 alarm $opt{interval} if $opt{interval} > 0;
164 require Tie::Array::Sorted;
165 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
166 } or warn $@, "Expect slowdown with large datasets!\n";
170 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
172 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
174 s/\A\h*// unless $opt{unmodified};
175 my $valnum = s/$valmatch/\n/ && $1;
176 push @values, $valnum;
177 push @order, $valnum if length $valnum;
178 if (defined $opt{trim} and defined $valnum) {
179 my $trimpos = abs $opt{trim};
180 $trimpos -= length $valnum if $opt{unmodified};
182 $_ = substr $_, 0, 2;
184 elsif (length > $trimpos) {
185 # cut and replace (intentional lvalue for speed, contrary to PBP)
186 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
190 show_lines() if defined $opt{interval} and $opt{interval} < 0
191 and $. % $opt{interval} == 0;
194 if ($opt{'zero-missing'}) {
195 push @values, (0) x 10;
198 $SIG{INT} = 'DEFAULT';
201 $opt{color} and defined $_[0] or return '';
202 return "\e[$_[0]m" if defined wantarray;
203 $_ = color(@_) . $_ . color(0) if defined;
209 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
211 @lines > $nr or return;
214 if (defined $opt{hidemax}) {
215 if ($opt{hidemin} and $opt{hidemin} < 0) {
216 $limit -= $opt{hidemax} - 1;
218 elsif ($opt{hidemax} <= $limit) {
219 $limit = $opt{hidemax} - 1;
223 @order = sort { $b <=> $a } @order unless tied @order;
224 my $maxval = $opt{maxval} // (
225 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
228 my $minval = $opt{minval} // min $order[-1] // (), 0;
229 my $range = $maxval - $minval;
230 my $lenval = $opt{'value-length'} // max map { length } @order;
231 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
232 max map { length $values[$_] && length $lines[$_] }
233 0 .. min $#lines, $opt{hidemax} || (); # left padding
234 my $size = defined $opt{width} && $range &&
235 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
238 if ($opt{markers} and $size > 0) {
239 for my $markspec (split /\h/, $opt{markers}) {
240 my ($char, $func) = split //, $markspec, 2;
242 if ($func eq 'avg') {
243 return sum(@order) / @order;
245 elsif ($func =~ /\A([0-9.]+)v\z/) {
246 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
247 my $index = $#order * $1 / 100;
248 return ($order[$index] + $order[$index + .5]) / 2;
250 elsif ($func =~ /\A-?[0-9.]+\z/) {
254 die "Unknown marker $char: $func\n";
263 color(36) for $barmark[$pos * $size] = $char;
266 state $lastmax = $maxval;
267 if ($maxval > $lastmax) {
268 print ' ' x ($lenval + $len);
271 ($lastmax - $minval) * $size + .5,
272 '-' x (($values[$nr - 1] - $minval) * $size);
274 say '+' x (($range - $lastmax) * $size + .5);
281 color(31), sprintf('%*s', $lenval, $minval),
282 color(90), '-', color(36), '+',
283 color(32), sprintf('%*s', $size * $range - 3, $maxval),
284 color(90), '-', color(36), '+',
288 while ($nr <= $limit) {
289 my $val = $values[$nr];
290 my $rel = length $val && $range && min(1, ($val - $minval) / $range);
291 my $color = !length $val || !$opt{palette} ? undef :
292 $val == $order[0] ? $opt{palette}->[-1] : # max
293 $val == $order[-1] ? $opt{palette}->[0] : # min
294 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
295 my $indicator = $opt{indicators} && $opt{indicators}->[
296 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
297 $#{$opt{indicators}} < 2 ? 1 :
298 $val >= $order[0] ? -1 :
299 $rel * ($#{$opt{indicators}} - 1e-14) + 1
303 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
304 print color($color), $_ for $indicator;
307 print $indicator if defined $indicator;
310 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
311 sprintf "%*s", $lenval, $val;
312 color($color) for $val;
314 my $line = $lines[$nr] =~ s/\n/$val/r;
315 if (not length $val) {
319 printf '%-*s', $len + length($val), $line;
320 print $barmark[$_] // $opt{'graph-format'}
321 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
327 say $opt{palette} ? color(0) : '' if $opt{spark};
333 if ($opt{hidemin} or $opt{hidemax}) {
334 my $linemin = $opt{hidemin};
335 my $linemax = ($opt{hidemax} || @lines) - 1;
338 $linemax = @lines - $linemax;
340 printf '%.8g of ', $opt{'sum-format'}->(
341 sum(grep {length} @values[$linemin .. $linemax]) // 0
345 my $total = sum @order;
346 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
347 printf ' in %d values', scalar @order;
348 printf ' over %d lines', scalar @lines if @order != @lines;
349 printf(' (%s min, %s avg, %s max)',
350 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
351 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
352 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
361 show_stat() if $opt{stat};
362 exit 130 if @_; # 0x80+signo
370 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
373 -a, --[no-]ascii Restrict user interface to ASCII characters
374 -C, --[no-]color Force colored output of values and bar markers
375 -f, --field=([+]N|REGEXP)
376 Compare values after a given number of whitespace
378 --header Prepend a chart axis with minimum and maximum
380 -H, --human-readable Format values using SI unit prefixes
381 -t, --interval[=(N|-LINES)]
382 Output partial progress every given number of
383 seconds or input lines
384 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
385 -L, --limit[=(N|-LAST|START-[END])]
386 Stop output after a number of lines
387 --graph-format=CHAR Glyph to repeat for the graph line
388 -m, --markers=FORMAT Statistical positions to indicate on bars
389 --min=N, --max=N Bars extend from 0 or the minimum value if lower
390 --palette=(PRESET|COLORS)
391 Override colors of parsed numbers
392 -_, --spark Replace lines by sparklines
393 --indicators[=CHARS] Prefix a unicode character corresponding to each
395 -s, --stat Total statistics after all data
396 -u, --unmodified Do not reformat values, keeping leading whitespace
397 --value-length=SIZE Reserved space for numbers
398 -w, --width=COLUMNS Override the maximum number of columns to use
399 -h, --usage Overview of available options
400 --help Full pod documentation
401 -V, --version Version information
407 barcat - concatenate texts with graph to visualize values
411 B<barcat> [<options>] [<file>... | <numbers>]
415 Visualizes relative sizes of values read from input
416 (parameters, file(s) or STDIN).
417 Contents are concatenated similar to I<cat>,
418 but numbers are reformatted and a bar graph is appended to each line.
420 Don't worry, barcat does not drink and divide.
421 It can has various options for input and output (re)formatting,
422 but remains limited to one-dimensional charts.
423 For more complex graphing needs
424 you'll need a larger animal like I<gnuplot>.
430 =item -a, --[no-]ascii
432 Restrict user interface to ASCII characters,
433 replacing default UTF-8 by their closest approximation.
434 Input is always interpreted as UTF-8 and shown as is.
436 =item -C, --[no-]color
438 Force colored output of values and bar markers.
439 Defaults on if output is a tty,
440 disabled otherwise such as when piped or redirected.
441 Can also be disabled by setting I<-M>
442 or the I<NO_COLOR> environment variable.
444 =item -f, --field=([+]<number> | <regexp>)
446 Compare values after a given number of whitespace separators,
447 or matching a regular expression.
449 Unspecified or I<-f0> means values are at the start of each line.
450 With I<-f1> the second word is taken instead.
451 A string can indicate the starting position of a value
452 (such as I<-f:> if preceded by colons),
453 or capture the numbers itself,
454 for example I<-f'(\d+)'> for the first digits anywhere.
455 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
459 Prepend a chart axis with minimum and maximum values labeled.
461 =item -H, --human-readable
463 Format values using SI unit prefixes,
464 turning long numbers like I<12356789> into I<12.4M>.
465 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
466 Short integers are aligned but kept without decimal point.
468 =item -t, --interval[=(<seconds> | -<lines>)]
470 Output partial progress every given number of seconds or input lines.
471 An update can also be forced by sending a I<SIGALRM> alarm signal.
473 =item -l, --length=[-]<size>[%]
475 Trim line contents (between number and bars)
476 to a maximum number of characters.
477 The exceeding part is replaced by an abbreviation sign,
478 unless C<--length=0>.
480 Prepend a dash (i.e. make negative) to enforce padding
481 regardless of encountered contents.
483 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
485 Stop output after a number of lines.
486 A single value indicates the last line number (like C<head>),
487 or first line counting from the bottom if negative (like C<tail>).
488 A specific range can be given by two values.
490 All input is still counted and analyzed for statistics,
491 but disregarded for padding and bar size.
493 =item --graph-format=<character>
495 Glyph to repeat for the graph line.
496 Defaults to a dash C<->.
498 =item -m, --markers=<format>
500 Statistical positions to indicate on bars.
501 A single indicator glyph precedes each position:
507 Exact value to match on the axis.
508 A vertical bar at the zero crossing is displayed by I<|0>
510 For example I<:3.14> would show a colon at pi.
512 =item <percentage>I<v>
514 Ranked value at the given percentile.
515 The default shows I<+> at I<50v> for the mean or median;
516 the middle value or average between middle values.
517 One standard deviation right of the mean is at about I<68.3v>.
518 The default includes I<< >31.73v <68.27v >>
519 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
524 the sum of all values divided by the number of counted lines.
525 Indicated by default as I<=>.
529 =item --min=<number>, --max=<number>
531 Bars extend from 0 or the minimum value if lower,
532 to the largest value encountered.
533 These options can be set to customize this range.
535 =item --palette=(<preset> | <color>...)
537 Override colors of parsed numbers.
538 Can be any CSI escape, such as I<90> for default dark grey,
539 or alternatively I<1;30> for bright black.
541 In case of additional colors,
542 the last is used for values equal to the maximum, the first for minima.
543 If unspecified, these are green and red respectively (I<31 90 32>).
544 Multiple intermediate colors will be distributed
545 relative to the size of values.
547 Predefined color schemes are named I<whites> and I<fire>,
548 or I<greys> and I<fire256> for 256-color variants.
552 Replace lines by I<sparklines>,
553 single characters (configured by C<--indicators>)
554 corresponding to input values.
556 =item --indicators[=<characters>]
558 Prefix a unicode character corresponding to each value.
559 The first specified character will be used for non-values,
560 the remaining sequence will be distributed over the range of values.
561 Unspecified, block fill glyphs U+2581-2588 will be used.
565 Total statistics after all data.
567 =item -u, --unmodified
569 Do not reformat values, keeping leading whitespace.
570 Keep original value alignment, which may be significant in some programs.
572 =item --value-length=<size>
574 Reserved space for numbers.
576 =item -w, --width=<columns>
578 Override the maximum number of columns to use.
579 Appended graphics will extend to fill up the entire screen.
583 Overview of available options.
587 Full pod documentation
588 as rendered by perldoc.
600 seq 30 | awk '{print sin($1/10)}' | barcat
602 Compare file sizes (with human-readable numbers):
604 du -d0 -b * | barcat -H
606 Same from formatted results, selecting the first numeric value:
608 tree -s --noreport | barcat -H -f+0
610 Memory usage of user processes with long names truncated:
612 ps xo rss,pid,cmd | barcat -l40
614 Monitor network latency from prefixed results:
616 ping google.com | barcat -f'time=\K' -t
618 Commonly used after counting, for example users on the current server:
620 users | tr ' ' '\n' | sort | uniq -c | barcat
622 Letter frequencies in text files:
624 cat /usr/share/games/fortunes/*.u8 |
625 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
626 sort | uniq -c | barcat
628 Number of HTTP requests per day:
630 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
632 Any kind of database query with counts, preserving returned alignment:
634 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
637 In PostgreSQL from within the client:
639 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
641 Earthquakes worldwide magnitude 1+ in the last 24 hours:
643 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
644 column -tns, | barcat -f4 -u -l80%
646 External datasets, like movies per year:
648 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
649 jq .[].year | uniq -c | barcat
651 Pokémon height comparison:
653 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
654 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
656 USD/EUR exchange rate from CSV provided by the ECB:
658 curl https://sdw.ecb.europa.eu/export.do \
659 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
660 barcat -f',\K' --value-length=7
662 Total population history in XML from the World Bank:
664 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
665 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
668 Population and other information for all countries:
670 curl http://download.geonames.org/export/dump/countryInfo.txt |
671 grep -v '^#\s' | column -tns$'\t' | barcat -f+2 -u -l150 -s
673 And of course various Git statistics, such commit count by year:
675 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
677 Or the top 3 most frequent authors with statistics over all:
679 git shortlog -sn | barcat -L3 -s
681 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
683 ( git log --pretty=%ci --since=30day | cut -b-10
684 seq 0 30 | xargs -i date +%F -d-{}day ) |
685 sort | uniq -c | awk '$1--' | barcat --spark
687 Sparkline graphics of simple input given as inline parameters:
689 barcat -_ 3 1 4 1 5 0 9 2 4
691 Misusing the spark functionality to draw a lolcat line:
693 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
697 Mischa POSLAWSKY <perl@shiar.org>