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;
57 $opt{spark} = [split //,
58 $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
63 fire => [qw( 90 31 91 33 93 97 96 )],
64 fire88 => [map {"38;5;$_"} qw(
65 80 32 48 64 68 72 76 77 78 79 47
67 fire256=> [map {"38;5;$_"} qw(
69 202 208 214 220 226 227 228 229 230 231 159
71 ramp88 => [map {"38;5;$_"} qw(
72 64 65 66 67 51 35 39 23 22 26 25 28
74 whites => [qw( 1;30 0;37 1;37 )],
75 greys => [map {"38;5;$_"} 52, 235..255, 47],
76 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
83 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
84 say "barcat $mascot version $VERSION";
88 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
93 Pod::Usage::pod2usage(
94 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
97 ) or exit 64; # EX_USAGE
100 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
101 $opt{color} //= -t *STDOUT; # enable on tty
102 $opt{'graph-format'} //= '-';
103 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
104 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
105 if $opt{'human-readable'};
106 $opt{anchor} //= qr/\A/;
107 $opt{'value-length'} = 6 if $opt{units};
108 $opt{'value-length'} = 1 if $opt{unmodified};
109 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
110 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
111 $opt{palette} //= $opt{color} && [31, 90, 32];
112 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
113 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
114 and undef $opt{interval};
116 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
117 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
118 $opt{'value-format'} = $opt{units} && sub {
120 log(abs $_[0] || 1) / log(10)
121 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
122 + 1e-15 # float imprecision
124 my $decimal = ($unit % 3) == ($unit < 0);
125 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
126 $decimal = ($unit % 3) == ($unit < 0);
127 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
129 3 + ($_[0] < 0), # digits plus optional negative sign
131 $_[0] / 1000 ** int($unit/3), # number
132 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
133 $opt{units}->[$unit/3] # suffix
138 my (@lines, @values, @order);
140 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
143 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
145 $SIG{INT} = \&show_exit;
147 if (defined $opt{interval}) {
148 $opt{interval} ||= 1;
149 alarm $opt{interval} if $opt{interval} > 0;
152 require Tie::Array::Sorted;
153 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
154 } or warn $@, "Expect slowdown with large datasets!\n";
158 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
160 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
162 s/\A\h*// unless $opt{unmodified};
163 my $valnum = s/$valmatch/\n/ && $1;
164 push @values, $valnum;
165 push @order, $valnum if length $valnum;
166 if (defined $opt{trim} and defined $valnum) {
167 my $trimpos = abs $opt{trim};
168 $trimpos -= length $valnum if $opt{unmodified};
170 $_ = substr $_, 0, 2;
172 elsif (length > $trimpos) {
173 # cut and replace (intentional lvalue for speed, contrary to PBP)
174 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
178 show_lines() if defined $opt{interval} and $opt{interval} < 0
179 and $. % $opt{interval} == 0;
182 if ($opt{'zero-missing'}) {
183 push @values, (0) x 10;
186 $SIG{INT} = 'DEFAULT';
189 $opt{color} and defined $_[0] or return '';
190 return "\e[$_[0]m" if defined wantarray;
191 $_ = color(@_) . $_ . color(0) if defined;
197 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
199 @lines > $nr or return;
202 if (defined $opt{hidemax}) {
203 if ($opt{hidemin} and $opt{hidemin} < 0) {
204 $limit -= $opt{hidemax} - 1;
207 $limit = $opt{hidemax} - 1;
211 @order = sort { $b <=> $a } @order unless tied @order;
212 my $maxval = $opt{maxval} // (
213 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
216 my $minval = $opt{minval} // min $order[-1] // (), 0;
217 my $range = $maxval - $minval;
218 my $lenval = $opt{'value-length'} // max map { length } @order;
219 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
220 max map { length $values[$_] && length $lines[$_] }
221 0 .. min $#lines, $opt{hidemax} || (); # left padding
222 my $size = defined $opt{width} && $range &&
223 ($opt{width} - $lenval - $len) / $range; # bar multiplication
226 if ($opt{markers} and $size > 0) {
227 for my $markspec (split /\h/, $opt{markers}) {
228 my ($char, $func) = split //, $markspec, 2;
230 if ($func eq 'avg') {
231 return sum(@order) / @order;
233 elsif ($func =~ /\A([0-9.]+)v\z/) {
234 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
235 my $index = $#order * $1 / 100;
236 return ($order[$index] + $order[$index + .5]) / 2;
238 elsif ($func =~ /\A-?[0-9.]+\z/) {
242 die "Unknown marker $char: $func\n";
251 color(36) for $barmark[$pos * $size] = $char;
254 state $lastmax = $maxval;
255 if ($maxval > $lastmax) {
256 print ' ' x ($lenval + $len);
259 ($lastmax - $minval) * $size + .5,
260 '-' x (($values[$nr - 1] - $minval) * $size);
262 say '+' x (($range - $lastmax) * $size + .5);
269 color(31), sprintf('%*s', $lenval, $minval),
270 color(90), '-', color(36), '+',
271 color(32), sprintf('%*s', $size * $range - 3, $maxval),
272 color(90), '-', color(36), '+',
276 while ($nr <= $limit) {
277 my $val = $values[$nr];
278 my $rel = length $val && $range && ($val - $minval) / $range;
279 my $color = !length $val || !$opt{palette} ? undef :
280 $val == $order[0] ? $opt{palette}->[-1] : # max
281 $val == $order[-1] ? $opt{palette}->[0] : # min
282 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
285 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
286 print color($color), $opt{spark}->[
287 !$val || !$#{$opt{spark}} ? 0 : # blank
288 $val == $order[0] ? -1 : # max
289 $val == $order[-1] ? 1 : # min
290 $#{$opt{spark}} < 3 ? 1 :
291 $rel * ($#{$opt{spark}} - 3) + 2.5
297 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
298 sprintf "%*s", $lenval, $val;
299 color($color) for $val;
301 my $line = $lines[$nr] =~ s/\n/$val/r;
302 if (not length $val) {
306 printf '%-*s', $len + length($val), $line;
307 print $barmark[$_] // $opt{'graph-format'}
308 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
314 say $opt{palette} ? color(0) : '' if $opt{spark};
320 if ($opt{hidemin} or $opt{hidemax}) {
321 my $linemin = $opt{hidemin};
322 my $linemax = ($opt{hidemax} || @lines) - 1;
325 $linemax = @lines - $linemax;
327 printf '%.8g of ', $opt{'sum-format'}->(
328 sum(grep {length} @values[$linemin .. $linemax]) // 0
332 my $total = sum @order;
333 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
334 printf ' in %d values', scalar @order;
335 printf ' over %d lines', scalar @lines if @order != @lines;
336 printf(' (%s min, %s avg, %s max)',
337 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
338 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
339 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
348 show_stat() if $opt{stat};
349 exit 130 if @_; # 0x80+signo
357 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
360 -a, --[no-]ascii Restrict user interface to ASCII characters
361 -c, --[no-]color Force colored output of values and bar markers
362 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
364 --header Prepend a chart axis with minimum and maximum
366 -H, --human-readable Format values using SI unit prefixes
367 -t, --interval[=(N|-LINES)]
368 Output partial progress every given number of
369 seconds or input lines
370 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
371 -L, --limit[=(N|-LAST|START-[END])]
372 Stop output after a number of lines
373 --graph-format=CHAR Glyph to repeat for the graph line
374 -m, --markers=FORMAT Statistical positions to indicate on bars
375 --min=N, --max=N Bars extend from 0 or the minimum value if lower
376 --palette=(PRESET|COLORS)
377 Override colors of parsed numbers
378 --spark[=CHARS] Replace lines by sparklines
379 -s, --stat Total statistics after all data
380 -u, --unmodified Do not reformat values, keeping leading whitespace
381 --value-length=SIZE Reserved space for numbers
382 -w, --width=COLUMNS Override the maximum number of columns to use
383 -h, --usage Overview of available options
384 --help Full pod documentation
385 -V, --version Version information
391 barcat - concatenate texts with graph to visualize values
395 B<barcat> [<options>] [<file>... | <numbers>]
399 Visualizes relative sizes of values read from input
400 (parameters, file(s) or STDIN).
401 Contents are concatenated similar to I<cat>,
402 but numbers are reformatted and a bar graph is appended to each line.
404 Don't worry, barcat does not drink and divide.
405 It can has various options for input and output (re)formatting,
406 but remains limited to one-dimensional charts.
407 For more complex graphing needs
408 you'll need a larger animal like I<gnuplot>.
414 =item -a, --[no-]ascii
416 Restrict user interface to ASCII characters,
417 replacing default UTF-8 by their closest approximation.
418 Input is always interpreted as UTF-8 and shown as is.
420 =item -c, --[no-]color
422 Force colored output of values and bar markers.
423 Defaults on if output is a tty,
424 disabled otherwise such as when piped or redirected.
426 =item -f, --field=(<number> | <regexp>)
428 Compare values after a given number of whitespace separators,
429 or matching a regular expression.
431 Unspecified or I<-f0> means values are at the start of each line.
432 With I<-f1> the second word is taken instead.
433 A string can indicate the starting position of a value
434 (such as I<-f:> if preceded by colons),
435 or capture the numbers itself,
436 for example I<-f'(\d+)'> for the first digits anywhere.
440 Prepend a chart axis with minimum and maximum values labeled.
442 =item -H, --human-readable
444 Format values using SI unit prefixes,
445 turning long numbers like I<12356789> into I<12.4M>.
446 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
447 Short integers are aligned but kept without decimal point.
449 =item -t, --interval[=(<seconds> | -<lines>)]
451 Output partial progress every given number of seconds or input lines.
452 An update can also be forced by sending a I<SIGALRM> alarm signal.
454 =item -l, --length=[-]<size>[%]
456 Trim line contents (between number and bars)
457 to a maximum number of characters.
458 The exceeding part is replaced by an abbreviation sign,
459 unless C<--length=0>.
461 Prepend a dash (i.e. make negative) to enforce padding
462 regardless of encountered contents.
464 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
466 Stop output after a number of lines.
467 A single value indicates the last line number (like C<head>),
468 or first line counting from the bottom if negative (like C<tail>).
469 A specific range can be given by two values.
471 All input is still counted and analyzed for statistics,
472 but disregarded for padding and bar size.
474 =item --graph-format=<character>
476 Glyph to repeat for the graph line.
477 Defaults to a dash C<->.
479 =item -m, --markers=<format>
481 Statistical positions to indicate on bars.
482 A single indicator glyph precedes each position:
488 Exact value to match on the axis.
489 A vertical bar at the zero crossing is displayed by I<|0>
491 For example I<:3.14> would show a colon at pi.
493 =item <percentage>I<v>
495 Ranked value at the given percentile.
496 The default shows I<+> at I<50v> for the mean or median;
497 the middle value or average between middle values.
498 One standard deviation right of the mean is at about I<68.3v>.
499 The default includes I<< >31.73v <68.27v >>
500 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
505 the sum of all values divided by the number of counted lines.
506 Indicated by default as I<=>.
510 =item --min=<number>, --max=<number>
512 Bars extend from 0 or the minimum value if lower,
513 to the largest value encountered.
514 These options can be set to customize this range.
516 =item --palette=(<preset> | <color>...)
518 Override colors of parsed numbers.
519 Can be any CSI escape, such as I<90> for default dark grey,
520 or alternatively I<1;30> for bright black.
522 In case of additional colors,
523 the last is used for values equal to the maximum, the first for minima.
524 If unspecified, these are green and red respectively (I<31 90 32>).
525 Multiple intermediate colors will be distributed
526 relative to the size of values.
528 Predefined color schemes are named I<whites> and I<fire>,
529 or I<greys> and I<fire256> for 256-color variants.
531 =item --spark[=<characters>]
533 Replace lines by I<sparklines>,
534 single characters corresponding to input values.
535 A specified sequence of unicode characters will be used for
536 Of a specified sequence of unicode characters,
537 the first one will be used for non-values,
538 the last one for the maximum,
539 the second (if any) for the minimum,
540 and any remaining will be distributed over the range of values.
541 Unspecified, block fill glyphs U+2581-2588 will be used.
545 Total statistics after all data.
547 =item -u, --unmodified
549 Do not reformat values, keeping leading whitespace.
550 Keep original value alignment, which may be significant in some programs.
552 =item --value-length=<size>
554 Reserved space for numbers.
556 =item -w, --width=<columns>
558 Override the maximum number of columns to use.
559 Appended graphics will extend to fill up the entire screen.
563 Overview of available options.
567 Full pod documentation
568 as rendered by perldoc.
580 seq 30 | awk '{print sin($1/10)}' | barcat
582 Compare file sizes (with human-readable numbers):
584 du -d0 -b * | barcat -H
586 Memory usage of user processes with long names truncated:
588 ps xo %mem,pid,cmd | barcat -l40
590 Monitor network latency from prefixed results:
592 ping google.com | barcat -f'time=\K' -t
594 Commonly used after counting, for example users on the current server:
596 users | tr ' ' '\n' | sort | uniq -c | barcat
598 Letter frequencies in text files:
600 cat /usr/share/games/fortunes/*.u8 |
601 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
602 sort | uniq -c | barcat
604 Number of HTTP requests per day:
606 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
608 Any kind of database query with counts, preserving returned alignment:
610 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
613 In PostgreSQL from within the client:
615 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
617 Earthquakes worldwide magnitude 1+ in the last 24 hours:
619 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
620 column -tns, | barcat -f4 -u -l80%
622 External datasets, like movies per year:
624 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
625 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
627 But please get I<jq> to process JSON
628 and replace the manual selection by C<< jq '.[].year' >>.
630 Pokémon height comparison:
632 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
633 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
635 USD/EUR exchange rate from CSV provided by the ECB:
637 curl https://sdw.ecb.europa.eu/export.do \
638 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
639 grep '^[12]' | barcat -f',\K' --value-length=7
641 Total population history in XML from the World Bank:
643 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
644 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
645 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
647 And of course various Git statistics, such commit count by year:
649 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
651 Or the top 3 most frequent authors with statistics over all:
653 git shortlog -sn | barcat -L3 -s
655 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
657 ( git log --pretty=%ci --since=30day | cut -b-10
658 seq 0 30 | xargs -i date +%F -d-{}day ) |
659 sort | uniq -c | awk '$1--' | barcat --spark
661 Sparkline graphics of simple input given as inline parameters:
663 barcat --spark= 3 1 4 1 5 0 9 2 4
667 Mischa POSLAWSKY <perl@shiar.org>