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 * (abs($_[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 3 + ($_[0] < 0), # digits plus optional negative sign
124 $float && ($unit % 3) == ($unit < 0), # tenths
125 $_[0] / 1000 ** int($unit/3), # number
126 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
131 my (@lines, @values, @order);
133 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
136 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
138 $SIG{INT} = \&show_exit;
140 if (defined $opt{interval}) {
141 $opt{interval} ||= 1;
142 alarm $opt{interval} if $opt{interval} > 0;
145 require Tie::Array::Sorted;
146 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
147 } or warn $@, "Expect slowdown with large datasets!\n";
151 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
153 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
155 s/\A\h*// unless $opt{unmodified};
156 my $valnum = s/$valmatch/\n/ && $1;
157 push @values, $valnum;
158 push @order, $valnum if length $valnum;
159 if (defined $opt{trim} and defined $valnum) {
160 my $trimpos = abs $opt{trim};
161 $trimpos -= length $valnum if $opt{unmodified};
163 $_ = substr $_, 0, 2;
165 elsif (length > $trimpos) {
166 # cut and replace (intentional lvalue for speed, contrary to PBP)
167 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
171 show_lines() if defined $opt{interval} and $opt{interval} < 0
172 and $. % $opt{interval} == 0;
175 if ($opt{'zero-missing'}) {
176 push @values, (0) x 10;
179 $SIG{INT} = 'DEFAULT';
182 $opt{color} and defined $_[0] or return '';
183 return "\e[$_[0]m" if defined wantarray;
184 $_ = color(@_) . $_ . color(0) if defined;
190 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
192 @lines > $nr or return;
195 if (defined $opt{hidemax}) {
196 if ($opt{hidemin} and $opt{hidemin} < 0) {
197 $limit -= $opt{hidemax} - 1;
200 $limit = $opt{hidemax} - 1;
204 @order = sort { $b <=> $a } @order unless tied @order;
205 my $maxval = $opt{maxval} // (
206 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
209 my $minval = $opt{minval} // min $order[-1] // (), 0;
210 my $range = $maxval - $minval;
211 my $lenval = $opt{'value-length'} // max map { length } @order;
212 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
213 max map { length $values[$_] && length $lines[$_] }
214 0 .. min $#lines, $opt{hidemax} || (); # left padding
215 my $size = defined $opt{width} && $range &&
216 ($opt{width} - $lenval - $len) / $range; # bar multiplication
219 if ($opt{markers} and $size > 0) {
220 for my $markspec (split /\h/, $opt{markers}) {
221 my ($char, $func) = split //, $markspec, 2;
223 if ($func eq 'avg') {
224 return sum(@order) / @order;
226 elsif ($func =~ /\A([0-9.]+)v\z/) {
227 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
228 my $index = $#order * $1 / 100;
229 return ($order[$index] + $order[$index + .5]) / 2;
231 elsif ($func =~ /\A-?[0-9.]+\z/) {
235 die "Unknown marker $char: $func\n";
244 color(36) for $barmark[$pos * $size] = $char;
247 state $lastmax = $maxval;
248 if ($maxval > $lastmax) {
249 print ' ' x ($lenval + $len);
252 ($lastmax - $minval) * $size + .5,
253 '-' x (($values[$nr - 1] - $minval) * $size);
255 say '+' x (($range - $lastmax) * $size + .5);
262 color(31), sprintf('%*s', $lenval, $minval),
263 color(90), '-', color(36), '+',
264 color(32), sprintf('%*s', $size * $range - 3, $maxval),
265 color(90), '-', color(36), '+',
269 while ($nr <= $limit) {
270 my $val = $values[$nr];
271 my $rel = length $val && $range && ($val - $minval) / $range;
272 my $color = !length $val || !$opt{palette} ? undef :
273 $val == $order[0] ? $opt{palette}->[-1] : # max
274 $val == $order[-1] ? $opt{palette}->[0] : # min
275 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
278 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
279 print color($color), $opt{spark}->[
280 !$val || !$#{$opt{spark}} ? 0 : # blank
281 $val == $order[0] ? -1 : # max
282 $val == $order[-1] ? 1 : # min
283 $#{$opt{spark}} < 3 ? 1 :
284 $rel * ($#{$opt{spark}} - 3) + 2.5
290 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
291 sprintf "%*s", $lenval, $val;
292 color($color) for $val;
294 my $line = $lines[$nr] =~ s/\n/$val/r;
295 if (not length $val) {
299 printf '%-*s', $len + length($val), $line;
300 print $barmark[$_] // $opt{'graph-format'}
301 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
307 say $opt{palette} ? color(0) : '' if $opt{spark};
313 if ($opt{hidemin} or $opt{hidemax}) {
314 my $linemin = $opt{hidemin};
315 my $linemax = ($opt{hidemax} || @lines) - 1;
318 $linemax = @lines - $linemax;
320 printf '%.8g of ', $opt{'sum-format'}->(
321 sum(grep {length} @values[$linemin .. $linemax]) // 0
325 my $total = sum @order;
326 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
327 printf ' in %d values', scalar @order;
328 printf ' over %d lines', scalar @lines if @order != @lines;
329 printf(' (%s min, %s avg, %s max)',
330 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
331 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
332 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
341 show_stat() if $opt{stat};
342 exit 130 if @_; # 0x80+signo
350 barcat [OPTIONS] [FILES|NUMBERS]
353 -a, --[no-]ascii Restrict user interface to ASCII characters
354 -c, --[no-]color Force colored output of values and bar markers
355 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
357 --header Prepend a chart axis with minimum and maximum
359 -H, --human-readable Format values using SI unit prefixes
360 -t, --interval[=(N|-LINES)]
361 Output partial progress every given number of
362 seconds or input lines
363 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
364 -L, --limit[=(N|-LAST|START-[END])]
365 Stop output after a number of lines
366 --graph-format=CHAR Glyph to repeat for the graph line
367 -m, --markers=FORMAT Statistical positions to indicate on bars
368 --min=N, --max=N Bars extend from 0 or the minimum value if lower
369 --palette=(PRESET|COLORS)
370 Override colors of parsed numbers
371 --spark[=CHARS] Replace lines by sparklines
372 -s, --stat Total statistics after all data
373 -u, --unmodified Do not reformat values, keeping leading whitespace
374 --value-length=SIZE Reserved space for numbers
375 -w, --width=COLUMNS Override the maximum number of columns to use
376 -h, --usage Overview of available options
377 --help Full documentation
378 --version Version information
384 barcat - graph to visualize input values
388 B<barcat> [<options>] [<file>... | <numbers>]
392 Visualizes relative sizes of values read from input
393 (parameters, file(s) or STDIN).
394 Contents are concatenated similar to I<cat>,
395 but numbers are reformatted and a bar graph is appended to each line.
397 Don't worry, barcat does not drink and divide.
398 It can has various options for input and output (re)formatting,
399 but remains limited to one-dimensional charts.
400 For more complex graphing needs
401 you'll need a larger animal like I<gnuplot>.
407 =item -a, --[no-]ascii
409 Restrict user interface to ASCII characters,
410 replacing default UTF-8 by their closest approximation.
411 Input is always interpreted as UTF-8 and shown as is.
413 =item -c, --[no-]color
415 Force colored output of values and bar markers.
416 Defaults on if output is a tty,
417 disabled otherwise such as when piped or redirected.
419 =item -f, --field=(<number> | <regexp>)
421 Compare values after a given number of whitespace separators,
422 or matching a regular expression.
424 Unspecified or I<-f0> means values are at the start of each line.
425 With I<-f1> the second word is taken instead.
426 A string can indicate the starting position of a value
427 (such as I<-f:> if preceded by colons),
428 or capture the numbers itself,
429 for example I<-f'(\d+)'> for the first digits anywhere.
433 Prepend a chart axis with minimum and maximum values labeled.
435 =item -H, --human-readable
437 Format values using SI unit prefixes,
438 turning long numbers like I<12356789> into I<12.4M>.
439 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
440 Short integers are aligned but kept without decimal point.
442 =item -t, --interval[=(<seconds> | -<lines>)]
444 Output partial progress every given number of seconds or input lines.
445 An update can also be forced by sending a I<SIGALRM> alarm signal.
447 =item -l, --length=[-]<size>[%]
449 Trim line contents (between number and bars)
450 to a maximum number of characters.
451 The exceeding part is replaced by an abbreviation sign,
452 unless C<--length=0>.
454 Prepend a dash (i.e. make negative) to enforce padding
455 regardless of encountered contents.
457 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
459 Stop output after a number of lines.
460 A single value indicates the last line number (like C<head>),
461 or first line counting from the bottom if negative (like C<tail>).
462 A specific range can be given by two values.
464 All input is still counted and analyzed for statistics,
465 but disregarded for padding and bar size.
467 =item --graph-format=<character>
469 Glyph to repeat for the graph line.
470 Defaults to a dash C<->.
472 =item -m, --markers=<format>
474 Statistical positions to indicate on bars.
475 A single indicator glyph precedes each position:
481 Exact value to match on the axis.
482 A vertical bar at the zero crossing is displayed by I<|0>
484 For example I<:3.14> would show a colon at pi.
486 =item <percentage>I<v>
488 Ranked value at the given percentile.
489 The default shows I<+> at I<50v> for the mean or median;
490 the middle value or average between middle values.
491 One standard deviation right of the mean is at about I<68.3v>.
492 The default includes I<< >31.73v <68.27v >>
493 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
498 the sum of all values divided by the number of counted lines.
499 Indicated by default as I<=>.
503 =item --min=<number>, --max=<number>
505 Bars extend from 0 or the minimum value if lower,
506 to the largest value encountered.
507 These options can be set to customize this range.
509 =item --palette=(<preset> | <color>...)
511 Override colors of parsed numbers.
512 Can be any CSI escape, such as I<90> for default dark grey,
513 or alternatively I<1;30> for bright black.
515 In case of additional colors,
516 the last is used for values equal to the maximum, the first for minima.
517 If unspecified, these are green and red respectively (I<31 90 32>).
518 Multiple intermediate colors will be distributed
519 relative to the size of values.
521 Predefined color schemes are named I<whites> and I<fire>,
522 or I<greys> and I<fire256> for 256-color variants.
524 =item --spark[=<characters>]
526 Replace lines by I<sparklines>,
527 single characters corresponding to input values.
528 A specified sequence of unicode characters will be used for
529 Of a specified sequence of unicode characters,
530 the first one will be used for non-values,
531 the last one for the maximum,
532 the second (if any) for the minimum,
533 and any remaining will be distributed over the range of values.
534 Unspecified, block fill glyphs U+2581-2588 will be used.
538 Total statistics after all data.
540 =item -u, --unmodified
542 Do not reformat values, keeping leading whitespace.
543 Keep original value alignment, which may be significant in some programs.
545 =item --value-length=<size>
547 Reserved space for numbers.
549 =item -w, --width=<columns>
551 Override the maximum number of columns to use.
552 Appended graphics will extend to fill up the entire screen.
556 Overview of available options.
573 seq 30 | awk '{print sin($1/10)}' | barcat
575 Compare file sizes (with human-readable numbers):
577 du -d0 -b * | barcat -H
579 Memory usage of user processes with long names truncated:
581 ps xo %mem,pid,cmd | barcat -l40
583 Monitor network latency from prefixed results:
585 ping google.com | barcat -f'time=\K' -t
587 Commonly used after counting, for example users on the current server:
589 users | tr ' ' '\n' | sort | uniq -c | barcat
591 Letter frequencies in text files:
593 cat /usr/share/games/fortunes/*.u8 |
594 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
595 sort | uniq -c | barcat
597 Number of HTTP requests per day:
599 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
601 Any kind of database query with counts, preserving returned alignment:
603 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
606 In PostgreSQL from within the client:
608 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
610 Earthquakes worldwide magnitude 1+ in the last 24 hours:
612 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
613 column -tns, | barcat -f4 -u -l80%
615 External datasets, like movies per year:
617 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
618 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
620 But please get I<jq> to process JSON
621 and replace the manual selection by C<< jq '.[].year' >>.
623 Pokémon height comparison:
625 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
626 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
628 USD/EUR exchange rate from CSV provided by the ECB:
630 curl https://sdw.ecb.europa.eu/export.do \
631 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
632 grep '^[12]' | barcat -f',\K' --value-length=7
634 Total population history in XML from the World Bank:
636 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
637 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
638 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
640 And of course various Git statistics, such commit count by year:
642 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
644 Or the top 3 most frequent authors with statistics over all:
646 git shortlog -sn | barcat -L3 -s
648 Sparkline graphics of simple input given as inline parameters:
650 barcat --spark= 3 1 4 1 5 0 9 2 4
652 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
654 ( git log --pretty=%ci --since=30day | cut -b-10
655 seq 0 30 | xargs -i date +%F -d-{}day ) |
656 sort | uniq -c | awk '$1--' | barcat --spark
660 Mischa POSLAWSKY <perl@shiar.org>