5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
11 use Getopt::Long '2.33', qw( :config gnu_getopt );
16 'C' => sub { $opt{color} = 0 },
20 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
21 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
25 'trim|length|l=s' => sub {
26 my ($optname, $optval) = @_;
27 $optval =~ s/%$// and $opt{trimpct}++;
28 $optval =~ m/\A-?[0-9]+\z/ or die(
29 "Value \"$optval\" invalid for option $optname",
30 " (number or percentage expected)\n"
40 my ($optname, $optval) = @_;
42 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
43 ($opt{hidemin}, $opt{hidemax}) =
44 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
45 "Value \"$optval\" invalid for option limit",
51 'graph-format=s' => sub {
52 $opt{'graph-format'} = substr $_[1], 0, 1;
55 $opt{spark} = [split //,
56 $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
61 fire => [qw( 90 31 91 33 93 97 96 )],
62 fire88 => [map {"38;5;$_"} qw(
63 80 32 48 64 68 72 76 77 78 79 47
65 fire256=> [map {"38;5;$_"} qw(
67 202 208 214 220 226 227 228 229 230 231 159
69 ramp88 => [map {"38;5;$_"} qw(
70 64 65 66 67 51 35 39 23 22 26 25 28
72 whites => [qw( 1;30 0;37 1;37 )],
73 greys => [map {"38;5;$_"} 52, 235..255, 47],
74 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
81 say "barcat version $VERSION";
85 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
90 Pod::Usage::pod2usage(
91 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
94 ) or exit 64; # EX_USAGE
96 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
97 $opt{color} //= -t *STDOUT; # enable on tty
98 $opt{'graph-format'} //= '-';
99 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
100 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
101 if $opt{'human-readable'};
102 $opt{anchor} //= qr/\A/;
103 $opt{'value-length'} = 6 if $opt{units};
104 $opt{'value-length'} = 1 if $opt{unmodified};
105 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
106 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
107 $opt{palette} //= $opt{color} && [31, 90, 32];
108 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
109 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
110 and undef $opt{interval};
112 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
113 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
114 $opt{'value-format'} = $opt{units} && sub {
116 log(abs $_[0] || 1) / log(10)
117 - 3 * ($_[0] < .9995) # shift to smaller unit if below 1
118 - log(.9995) / log(10) # 3 digits rounding up
119 + 1e-15 # float imprecision
121 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
123 $float && ($unit % 3) == ($unit < 0), # tenths
124 $_[0] / 1000 ** int($unit/3), # number
125 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
130 my (@lines, @values, @order);
132 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
135 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
137 $SIG{INT} = \&show_exit;
139 if (defined $opt{interval}) {
140 $opt{interval} ||= 1;
141 alarm $opt{interval} if $opt{interval} > 0;
144 require Tie::Array::Sorted;
145 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
146 } or warn $@, "Expect slowdown with large datasets!\n";
150 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
152 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
154 s/\A\h*// unless $opt{unmodified};
155 my $valnum = s/$valmatch/\n/ && $1;
156 push @values, $valnum;
157 push @order, $valnum if length $valnum;
158 if (defined $opt{trim} and defined $valnum) {
159 my $trimpos = abs $opt{trim};
160 $trimpos -= length $valnum if $opt{unmodified};
162 $_ = substr $_, 0, 2;
164 elsif (length > $trimpos) {
165 # cut and replace (intentional lvalue for speed, contrary to PBP)
166 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
170 show_lines() if defined $opt{interval} and $opt{interval} < 0
171 and $. % $opt{interval} == 0;
174 if ($opt{'zero-missing'}) {
175 push @values, (0) x 10;
178 $SIG{INT} = 'DEFAULT';
181 $opt{color} and defined $_[0] or return '';
182 return "\e[$_[0]m" if defined wantarray;
183 $_ = color(@_) . $_ . color(0) if defined;
189 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
191 @lines > $nr or return;
194 if (defined $opt{hidemax}) {
195 if ($opt{hidemin} and $opt{hidemin} < 0) {
196 $limit -= $opt{hidemax} - 1;
199 $limit = $opt{hidemax} - 1;
203 @order = sort { $b <=> $a } @order unless tied @order;
204 my $maxval = $opt{maxval} // (
205 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
208 my $minval = $opt{minval} // min $order[-1] // (), 0;
209 my $range = $maxval - $minval;
210 my $lenval = $opt{'value-length'} // max map { length } @order;
211 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
212 max map { length $values[$_] && length $lines[$_] }
213 0 .. min $#lines, $opt{hidemax} || (); # left padding
214 my $size = defined $opt{width} && $range &&
215 ($opt{width} - $lenval - $len) / $range; # bar multiplication
218 if ($opt{markers} and $size > 0) {
219 for my $markspec (split /\h/, $opt{markers}) {
220 my ($char, $func) = split //, $markspec, 2;
222 if ($func eq 'avg') {
223 return sum(@order) / @order;
225 elsif ($func =~ /\A([0-9.]+)v\z/) {
226 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
227 my $index = $#order * $1 / 100;
228 return ($order[$index] + $order[$index + .5]) / 2;
230 elsif ($func =~ /\A-?[0-9.]+\z/) {
234 die "Unknown marker $char: $func\n";
243 color(36) for $barmark[$pos * $size] = $char;
246 state $lastmax = $maxval;
247 if ($maxval > $lastmax) {
248 print ' ' x ($lenval + $len);
251 ($lastmax - $minval) * $size + .5,
252 '-' x (($values[$nr - 1] - $minval) * $size);
254 say '+' x (($range - $lastmax) * $size + .5);
261 color(31), sprintf('%*s', $lenval, $minval),
262 color(90), '-', color(36), '+',
263 color(32), sprintf('%*s', $size * $range - 3, $maxval),
264 color(90), '-', color(36), '+',
268 while ($nr <= $limit) {
269 my $val = $values[$nr];
270 my $rel = length $val && $range && ($val - $minval) / $range;
271 my $color = !length $val || !$opt{palette} ? undef :
272 $val == $order[0] ? $opt{palette}->[-1] : # max
273 $val == $order[-1] ? $opt{palette}->[0] : # min
274 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
277 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
278 print color($color), $opt{spark}->[
279 !$val || !$#{$opt{spark}} ? 0 : # blank
280 $val == $order[0] ? -1 : # max
281 $val == $order[-1] ? 1 : # min
282 $#{$opt{spark}} < 3 ? 1 :
283 $rel * ($#{$opt{spark}} - 3) + 2.5
289 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
290 sprintf "%*s", $lenval, $val;
291 color($color) for $val;
293 my $line = $lines[$nr] =~ s/\n/$val/r;
294 printf '%-*s', $len + length($val), $line;
295 print $barmark[$_] // $opt{'graph-format'}
296 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
302 say $opt{palette} ? color(0) : '' if $opt{spark};
308 if ($opt{hidemin} or $opt{hidemax}) {
309 my $linemin = $opt{hidemin};
310 my $linemax = ($opt{hidemax} || @lines) - 1;
313 $linemax = @lines - $linemax;
315 printf '%.8g of ', $opt{'sum-format'}->(
316 sum(grep {length} @values[$linemin .. $linemax]) // 0
320 my $total = sum @order;
321 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
322 printf ' in %d values', scalar @order;
323 printf ' over %d lines', scalar @lines if @order != @lines;
324 printf(' (%s min, %s avg, %s max)',
325 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
326 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
327 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
336 show_stat() if $opt{stat};
337 exit 130 if @_; # 0x80+signo
345 barcat [OPTIONS] [FILES|NUMBERS]
348 -a, --[no-]ascii Restrict user interface to ASCII characters
349 -c, --[no-]color Force colored output of values and bar markers
350 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
352 --header Prepend a chart axis with minimum and maximum
354 -H, --human-readable Format values using SI unit prefixes
355 -t, --interval[=(N|-LINES)]
356 Output partial progress every given number of
357 seconds or input lines
358 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
359 -L, --limit[=(N|-LAST|START-[END])]
360 Stop output after a number of lines
361 --graph-format=CHAR Glyph to repeat for the graph line
362 -m, --markers=FORMAT Statistical positions to indicate on bars
363 --min=N, --max=N Bars extend from 0 or the minimum value if lower
364 --palette=(PRESET|COLORS)
365 Override colors of parsed numbers
366 --spark[=CHARS] Replace lines by sparklines
367 -s, --stat Total statistics after all data
368 -u, --unmodified Do not reformat values, keeping leading whitespace
369 --value-length=SIZE Reserved space for numbers
370 -w, --width=COLUMNS Override the maximum number of columns to use
371 -h, --usage Overview of available options
372 --help Full documentation
373 --version Version information
379 barcat - graph to visualize input values
383 B<barcat> [<options>] [<file>... | <numbers>]
387 Visualizes relative sizes of values read from input
388 (parameters, file(s) or STDIN).
389 Contents are concatenated similar to I<cat>,
390 but numbers are reformatted and a bar graph is appended to each line.
392 Don't worry, barcat does not drink and divide.
393 It can has various options for input and output (re)formatting,
394 but remains limited to one-dimensional charts.
395 For more complex graphing needs
396 you'll need a larger animal like I<gnuplot>.
402 =item -a, --[no-]ascii
404 Restrict user interface to ASCII characters,
405 replacing default UTF-8 by their closest approximation.
406 Input is always interpreted as UTF-8 and shown as is.
408 =item -c, --[no-]color
410 Force colored output of values and bar markers.
411 Defaults on if output is a tty,
412 disabled otherwise such as when piped or redirected.
414 =item -f, --field=(<number> | <regexp>)
416 Compare values after a given number of whitespace separators,
417 or matching a regular expression.
419 Unspecified or I<-f0> means values are at the start of each line.
420 With I<-f1> the second word is taken instead.
421 A string can indicate the starting position of a value
422 (such as I<-f:> if preceded by colons),
423 or capture the numbers itself,
424 for example I<-f'(\d+)'> for the first digits anywhere.
428 Prepend a chart axis with minimum and maximum values labeled.
430 =item -H, --human-readable
432 Format values using SI unit prefixes,
433 turning long numbers like I<12356789> into I<12.4M>.
434 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
435 Short integers are aligned but kept without decimal point.
437 =item -t, --interval[=(<seconds> | -<lines>)]
439 Output partial progress every given number of seconds or input lines.
440 An update can also be forced by sending a I<SIGALRM> alarm signal.
442 =item -l, --length=[-]<size>[%]
444 Trim line contents (between number and bars)
445 to a maximum number of characters.
446 The exceeding part is replaced by an abbreviation sign,
447 unless C<--length=0>.
449 Prepend a dash (i.e. make negative) to enforce padding
450 regardless of encountered contents.
452 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
454 Stop output after a number of lines.
455 A single value indicates the last line number (like C<head>),
456 or first line counting from the bottom if negative (like C<tail>).
457 A specific range can be given by two values.
459 All input is still counted and analyzed for statistics,
460 but disregarded for padding and bar size.
462 =item --graph-format=<character>
464 Glyph to repeat for the graph line.
465 Defaults to a dash C<->.
467 =item -m, --markers=<format>
469 Statistical positions to indicate on bars.
470 A single indicator glyph precedes each position:
476 Exact value to match on the axis.
477 A vertical bar at the zero crossing is displayed by I<|0>
479 For example I<:3.14> would show a colon at pi.
481 =item <percentage>I<v>
483 Ranked value at the given percentile.
484 The default shows I<+> at I<50v> for the mean or median;
485 the middle value or average between middle values.
486 One standard deviation right of the mean is at about I<68.3v>.
487 The default includes I<< >31.73v <68.27v >>
488 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
493 the sum of all values divided by the number of counted lines.
494 Indicated by default as I<=>.
498 =item --min=<number>, --max=<number>
500 Bars extend from 0 or the minimum value if lower,
501 to the largest value encountered.
502 These options can be set to customize this range.
504 =item --palette=(<preset> | <color>...)
506 Override colors of parsed numbers.
507 Can be any CSI escape, such as I<90> for default dark grey,
508 or alternatively I<1;30> for bright black.
510 In case of additional colors,
511 the last is used for values equal to the maximum, the first for minima.
512 If unspecified, these are green and red respectively (I<31 90 32>).
513 Multiple intermediate colors will be distributed
514 relative to the size of values.
516 Predefined color schemes are named I<whites> and I<fire>,
517 or I<greys> and I<fire256> for 256-color variants.
519 =item --spark[=<characters>]
521 Replace lines by I<sparklines>,
522 single characters corresponding to input values.
523 A specified sequence of unicode characters will be used for
524 Of a specified sequence of unicode characters,
525 the first one will be used for non-values,
526 the last one for the maximum,
527 the second (if any) for the minimum,
528 and any remaining will be distributed over the range of values.
529 Unspecified, block fill glyphs U+2581-2588 will be used.
533 Total statistics after all data.
535 =item -u, --unmodified
537 Do not reformat values, keeping leading whitespace.
538 Keep original value alignment, which may be significant in some programs.
540 =item --value-length=<size>
542 Reserved space for numbers.
544 =item -w, --width=<columns>
546 Override the maximum number of columns to use.
547 Appended graphics will extend to fill up the entire screen.
551 Overview of available options.
568 seq 30 | awk '{print sin($1/10)}' | barcat
570 Compare file sizes (with human-readable numbers):
572 du -d0 -b * | barcat -H
574 Memory usage of user processes with long names truncated:
576 ps xo %mem,pid,cmd | barcat -l40
578 Monitor network latency from prefixed results:
580 ping google.com | barcat -f'time=\K' -t
582 Commonly used after counting, for example users on the current server:
584 users | tr ' ' '\n' | sort | uniq -c | barcat
586 Letter frequencies in text files:
588 cat /usr/share/games/fortunes/*.u8 |
589 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
590 sort | uniq -c | barcat
592 Number of HTTP requests per day:
594 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
596 Any kind of database query with counts, preserving returned alignment:
598 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
601 In PostgreSQL from within the client:
603 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
605 Earthquakes worldwide magnitude 1+ in the last 24 hours:
607 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
608 column -tns, | barcat -f4 -u -l80%
610 External datasets, like movies per year:
612 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
613 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
615 But please get I<jq> to process JSON
616 and replace the manual selection by C<< jq '.[].year' >>.
618 Pokémon height comparison:
620 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
621 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
623 USD/EUR exchange rate from CSV provided by the ECB:
625 curl https://sdw.ecb.europa.eu/export.do \
626 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
627 grep '^[12]' | barcat -f',\K' --value-length=7
629 Total population history in XML from the World Bank:
631 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
632 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
633 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
635 And of course various Git statistics, such commit count by year:
637 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
639 Or the top 3 most frequent authors with statistics over all:
641 git shortlog -sn | barcat -L3 -s
643 Sparkline graphics of simple input given as inline parameters:
645 barcat --spark= 3 1 4 1 5 0 9 2 4
647 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
649 ( git log --pretty=%ci --since=30day | cut -b-10
650 seq 0 30 | xargs -i date +%F -d-{}day ) |
651 sort | uniq -c | awk '$1--' | barcat --spark
655 Mischa POSLAWSKY <perl@shiar.org>