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 'C' => sub { $opt{color} = 0 },
22 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
23 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
27 'trim|length|l=s' => sub {
28 my ($optname, $optval) = @_;
29 $optval =~ s/%$// and $opt{trimpct}++;
30 $optval =~ m/\A-?[0-9]+\z/ or die(
31 "Value \"$optval\" invalid for option $optname",
32 " (number or percentage expected)\n"
42 my ($optname, $optval) = @_;
44 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
45 ($opt{hidemin}, $opt{hidemax}) =
46 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
47 "Value \"$optval\" invalid for option limit",
53 'graph-format=s' => sub {
54 $opt{'graph-format'} = substr $_[1], 0, 1;
60 fire => [qw( 90 31 91 33 93 97 96 )],
61 fire256=> [map {"38;5;$_"} qw(
63 202 208 214 220 226 227 228 229 230 231 159
65 whites => [qw( 1;30 0;37 1;37 )],
66 greys => [map {"38;5;$_"} 0, 232..255, 15],
67 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
74 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
75 say "barcat $mascot version $VERSION";
79 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
84 Pod::Usage::pod2usage(
85 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
88 ) or exit 64; # EX_USAGE
91 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
92 $opt{color} //= -t *STDOUT; # enable on tty
93 $opt{'graph-format'} //= '-';
94 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
95 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
96 if $opt{'human-readable'};
97 $opt{anchor} //= qr/\A/;
98 $opt{'value-length'} = 6 if $opt{units};
99 $opt{'value-length'} = 1 if $opt{unmodified};
100 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
101 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
102 $opt{palette} //= $opt{color} && [31, 90, 32];
103 $opt{indicators} = [split //,
104 $opt{indicators} || ($opt{ascii} ? ' .oO' : ' ▁▂▃▄▅▆▇█')
105 ] if defined $opt{indicators} or $opt{spark};
106 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
107 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
108 and undef $opt{interval};
110 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
111 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
112 $opt{'value-format'} = $opt{units} && sub {
114 log(abs $_[0] || 1) / log(10)
115 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
116 + 1e-15 # float imprecision
118 my $decimal = ($unit % 3) == ($unit < 0);
119 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
120 $decimal = ($unit % 3) == ($unit < 0);
121 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
123 3 + ($_[0] < 0), # digits plus optional negative sign
125 $_[0] / 1000 ** int($unit/3), # number
126 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
127 $opt{units}->[$unit/3] # suffix
132 my (@lines, @values, @order);
134 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
137 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
139 $SIG{INT} = \&show_exit;
141 if (defined $opt{interval}) {
142 $opt{interval} ||= 1;
143 alarm $opt{interval} if $opt{interval} > 0;
146 require Tie::Array::Sorted;
147 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
148 } or warn $@, "Expect slowdown with large datasets!\n";
152 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
154 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
156 s/\A\h*// unless $opt{unmodified};
157 my $valnum = s/$valmatch/\n/ && $1;
158 push @values, $valnum;
159 push @order, $valnum if length $valnum;
160 if (defined $opt{trim} and defined $valnum) {
161 my $trimpos = abs $opt{trim};
162 $trimpos -= length $valnum if $opt{unmodified};
164 $_ = substr $_, 0, 2;
166 elsif (length > $trimpos) {
167 # cut and replace (intentional lvalue for speed, contrary to PBP)
168 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
172 show_lines() if defined $opt{interval} and $opt{interval} < 0
173 and $. % $opt{interval} == 0;
176 if ($opt{'zero-missing'}) {
177 push @values, (0) x 10;
180 $SIG{INT} = 'DEFAULT';
183 $opt{color} and defined $_[0] or return '';
184 return "\e[$_[0]m" if defined wantarray;
185 $_ = color(@_) . $_ . color(0) if defined;
191 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
193 @lines > $nr or return;
196 if (defined $opt{hidemax}) {
197 if ($opt{hidemin} and $opt{hidemin} < 0) {
198 $limit -= $opt{hidemax} - 1;
200 elsif ($opt{hidemax} <= $limit) {
201 $limit = $opt{hidemax} - 1;
205 @order = sort { $b <=> $a } @order unless tied @order;
206 my $maxval = $opt{maxval} // (
207 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
210 my $minval = $opt{minval} // min $order[-1] // (), 0;
211 my $range = $maxval - $minval;
212 my $lenval = $opt{'value-length'} // max map { length } @order;
213 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
214 max map { length $values[$_] && length $lines[$_] }
215 0 .. min $#lines, $opt{hidemax} || (); # left padding
216 my $size = defined $opt{width} && $range &&
217 ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range; # bar multiplication
220 if ($opt{markers} and $size > 0) {
221 for my $markspec (split /\h/, $opt{markers}) {
222 my ($char, $func) = split //, $markspec, 2;
224 if ($func eq 'avg') {
225 return sum(@order) / @order;
227 elsif ($func =~ /\A([0-9.]+)v\z/) {
228 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
229 my $index = $#order * $1 / 100;
230 return ($order[$index] + $order[$index + .5]) / 2;
232 elsif ($func =~ /\A-?[0-9.]+\z/) {
236 die "Unknown marker $char: $func\n";
245 color(36) for $barmark[$pos * $size] = $char;
248 state $lastmax = $maxval;
249 if ($maxval > $lastmax) {
250 print ' ' x ($lenval + $len);
253 ($lastmax - $minval) * $size + .5,
254 '-' x (($values[$nr - 1] - $minval) * $size);
256 say '+' x (($range - $lastmax) * $size + .5);
263 color(31), sprintf('%*s', $lenval, $minval),
264 color(90), '-', color(36), '+',
265 color(32), sprintf('%*s', $size * $range - 3, $maxval),
266 color(90), '-', color(36), '+',
270 while ($nr <= $limit) {
271 my $val = $values[$nr];
272 my $rel = length $val && $range && ($val - $minval) / $range;
273 my $color = !length $val || !$opt{palette} ? undef :
274 $val == $order[0] ? $opt{palette}->[-1] : # max
275 $val == $order[-1] ? $opt{palette}->[0] : # min
276 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
277 my $indicator = $opt{indicators} && $opt{indicators}->[
278 !$val || !$#{$opt{indicators}} ? 0 : # blank
279 $#{$opt{indicators}} < 2 ? 1 :
280 $val >= $order[0] ? -1 :
281 $rel * ($#{$opt{indicators}} - 1e-14) + 1
285 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
286 print color($color), $_ for $indicator;
289 print $indicator if defined $indicator;
292 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
293 sprintf "%*s", $lenval, $val;
294 color($color) for $val;
296 my $line = $lines[$nr] =~ s/\n/$val/r;
297 if (not length $val) {
301 printf '%-*s', $len + length($val), $line;
302 print $barmark[$_] // $opt{'graph-format'}
303 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
309 say $opt{palette} ? color(0) : '' if $opt{spark};
315 if ($opt{hidemin} or $opt{hidemax}) {
316 my $linemin = $opt{hidemin};
317 my $linemax = ($opt{hidemax} || @lines) - 1;
320 $linemax = @lines - $linemax;
322 printf '%.8g of ', $opt{'sum-format'}->(
323 sum(grep {length} @values[$linemin .. $linemax]) // 0
327 my $total = sum @order;
328 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
329 printf ' in %d values', scalar @order;
330 printf ' over %d lines', scalar @lines if @order != @lines;
331 printf(' (%s min, %s avg, %s max)',
332 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
333 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
334 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
343 show_stat() if $opt{stat};
344 exit 130 if @_; # 0x80+signo
352 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
355 -a, --[no-]ascii Restrict user interface to ASCII characters
356 -c, --[no-]color Force colored output of values and bar markers
357 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
359 --header Prepend a chart axis with minimum and maximum
361 -H, --human-readable Format values using SI unit prefixes
362 -t, --interval[=(N|-LINES)]
363 Output partial progress every given number of
364 seconds or input lines
365 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
366 -L, --limit[=(N|-LAST|START-[END])]
367 Stop output after a number of lines
368 --graph-format=CHAR Glyph to repeat for the graph line
369 -m, --markers=FORMAT Statistical positions to indicate on bars
370 --min=N, --max=N Bars extend from 0 or the minimum value if lower
371 --palette=(PRESET|COLORS)
372 Override colors of parsed numbers
373 -_, --spark Replace lines by sparklines
374 --indicators[=CHARS] Prefix a unicode character corresponding to each
376 -s, --stat Total statistics after all data
377 -u, --unmodified Do not reformat values, keeping leading whitespace
378 --value-length=SIZE Reserved space for numbers
379 -w, --width=COLUMNS Override the maximum number of columns to use
380 -h, --usage Overview of available options
381 --help Full pod documentation
382 -V, --version Version information
388 barcat - concatenate texts with graph to visualize values
392 B<barcat> [<options>] [<file>... | <numbers>]
396 Visualizes relative sizes of values read from input
397 (parameters, file(s) or STDIN).
398 Contents are concatenated similar to I<cat>,
399 but numbers are reformatted and a bar graph is appended to each line.
401 Don't worry, barcat does not drink and divide.
402 It can has various options for input and output (re)formatting,
403 but remains limited to one-dimensional charts.
404 For more complex graphing needs
405 you'll need a larger animal like I<gnuplot>.
411 =item -a, --[no-]ascii
413 Restrict user interface to ASCII characters,
414 replacing default UTF-8 by their closest approximation.
415 Input is always interpreted as UTF-8 and shown as is.
417 =item -c, --[no-]color
419 Force colored output of values and bar markers.
420 Defaults on if output is a tty,
421 disabled otherwise such as when piped or redirected.
423 =item -f, --field=(<number> | <regexp>)
425 Compare values after a given number of whitespace separators,
426 or matching a regular expression.
428 Unspecified or I<-f0> means values are at the start of each line.
429 With I<-f1> the second word is taken instead.
430 A string can indicate the starting position of a value
431 (such as I<-f:> if preceded by colons),
432 or capture the numbers itself,
433 for example I<-f'(\d+)'> for the first digits anywhere.
437 Prepend a chart axis with minimum and maximum values labeled.
439 =item -H, --human-readable
441 Format values using SI unit prefixes,
442 turning long numbers like I<12356789> into I<12.4M>.
443 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
444 Short integers are aligned but kept without decimal point.
446 =item -t, --interval[=(<seconds> | -<lines>)]
448 Output partial progress every given number of seconds or input lines.
449 An update can also be forced by sending a I<SIGALRM> alarm signal.
451 =item -l, --length=[-]<size>[%]
453 Trim line contents (between number and bars)
454 to a maximum number of characters.
455 The exceeding part is replaced by an abbreviation sign,
456 unless C<--length=0>.
458 Prepend a dash (i.e. make negative) to enforce padding
459 regardless of encountered contents.
461 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
463 Stop output after a number of lines.
464 A single value indicates the last line number (like C<head>),
465 or first line counting from the bottom if negative (like C<tail>).
466 A specific range can be given by two values.
468 All input is still counted and analyzed for statistics,
469 but disregarded for padding and bar size.
471 =item --graph-format=<character>
473 Glyph to repeat for the graph line.
474 Defaults to a dash C<->.
476 =item -m, --markers=<format>
478 Statistical positions to indicate on bars.
479 A single indicator glyph precedes each position:
485 Exact value to match on the axis.
486 A vertical bar at the zero crossing is displayed by I<|0>
488 For example I<:3.14> would show a colon at pi.
490 =item <percentage>I<v>
492 Ranked value at the given percentile.
493 The default shows I<+> at I<50v> for the mean or median;
494 the middle value or average between middle values.
495 One standard deviation right of the mean is at about I<68.3v>.
496 The default includes I<< >31.73v <68.27v >>
497 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
502 the sum of all values divided by the number of counted lines.
503 Indicated by default as I<=>.
507 =item --min=<number>, --max=<number>
509 Bars extend from 0 or the minimum value if lower,
510 to the largest value encountered.
511 These options can be set to customize this range.
513 =item --palette=(<preset> | <color>...)
515 Override colors of parsed numbers.
516 Can be any CSI escape, such as I<90> for default dark grey,
517 or alternatively I<1;30> for bright black.
519 In case of additional colors,
520 the last is used for values equal to the maximum, the first for minima.
521 If unspecified, these are green and red respectively (I<31 90 32>).
522 Multiple intermediate colors will be distributed
523 relative to the size of values.
525 Predefined color schemes are named I<whites> and I<fire>,
526 or I<greys> and I<fire256> for 256-color variants.
530 Replace lines by I<sparklines>,
531 single characters (configured by C<--indicators>)
532 corresponding to input values.
534 =item --indicators[=<characters>]
536 Prefix a unicode character corresponding to each value.
537 The first specified character will be used for non-values,
538 the remaining sequence will be distributed over the range of values.
539 Unspecified, block fill glyphs U+2581-2588 will be used.
543 Total statistics after all data.
545 =item -u, --unmodified
547 Do not reformat values, keeping leading whitespace.
548 Keep original value alignment, which may be significant in some programs.
550 =item --value-length=<size>
552 Reserved space for numbers.
554 =item -w, --width=<columns>
556 Override the maximum number of columns to use.
557 Appended graphics will extend to fill up the entire screen.
561 Overview of available options.
565 Full pod documentation
566 as rendered by perldoc.
578 seq 30 | awk '{print sin($1/10)}' | barcat
580 Compare file sizes (with human-readable numbers):
582 du -d0 -b * | barcat -H
584 Memory usage of user processes with long names truncated:
586 ps xo rss,pid,cmd | barcat -l40
588 Monitor network latency from prefixed results:
590 ping google.com | barcat -f'time=\K' -t
592 Commonly used after counting, for example users on the current server:
594 users | tr ' ' '\n' | sort | uniq -c | barcat
596 Letter frequencies in text files:
598 cat /usr/share/games/fortunes/*.u8 |
599 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
600 sort | uniq -c | barcat
602 Number of HTTP requests per day:
604 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
606 Any kind of database query with counts, preserving returned alignment:
608 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
611 In PostgreSQL from within the client:
613 > SELECT sin(generate_series(0, 3, .1)) \g |barcat
615 Earthquakes worldwide magnitude 1+ in the last 24 hours:
617 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
618 column -tns, | barcat -f4 -u -l80%
620 External datasets, like movies per year:
622 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
623 jq .[].year | uniq -c | barcat
625 Pokémon height comparison:
627 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
628 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
630 USD/EUR exchange rate from CSV provided by the ECB:
632 curl https://sdw.ecb.europa.eu/export.do \
633 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
634 barcat -f',\K' --value-length=7
636 Total population history in XML from the World Bank:
638 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
639 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
642 And of course various Git statistics, such commit count by year:
644 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
646 Or the top 3 most frequent authors with statistics over all:
648 git shortlog -sn | barcat -L3 -s
650 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
652 ( git log --pretty=%ci --since=30day | cut -b-10
653 seq 0 30 | xargs -i date +%F -d-{}day ) |
654 sort | uniq -c | awk '$1--' | barcat --spark
656 Sparkline graphics of simple input given as inline parameters:
658 barcat -_ 3 1 4 1 5 0 9 2 4
662 Mischa POSLAWSKY <perl@shiar.org>