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 + 1e-15 # float imprecision
120 my $decimal = ($unit % 3) == ($unit < 0);
121 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
122 $decimal = ($unit % 3) == ($unit < 0);
123 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
125 3 + ($_[0] < 0), # digits plus optional negative sign
127 $_[0] / 1000 ** int($unit/3), # number
128 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
129 $opt{units}->[$unit/3] # suffix
134 my (@lines, @values, @order);
136 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
139 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
141 $SIG{INT} = \&show_exit;
143 if (defined $opt{interval}) {
144 $opt{interval} ||= 1;
145 alarm $opt{interval} if $opt{interval} > 0;
148 require Tie::Array::Sorted;
149 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
150 } or warn $@, "Expect slowdown with large datasets!\n";
154 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
156 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
158 s/\A\h*// unless $opt{unmodified};
159 my $valnum = s/$valmatch/\n/ && $1;
160 push @values, $valnum;
161 push @order, $valnum if length $valnum;
162 if (defined $opt{trim} and defined $valnum) {
163 my $trimpos = abs $opt{trim};
164 $trimpos -= length $valnum if $opt{unmodified};
166 $_ = substr $_, 0, 2;
168 elsif (length > $trimpos) {
169 # cut and replace (intentional lvalue for speed, contrary to PBP)
170 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
174 show_lines() if defined $opt{interval} and $opt{interval} < 0
175 and $. % $opt{interval} == 0;
178 if ($opt{'zero-missing'}) {
179 push @values, (0) x 10;
182 $SIG{INT} = 'DEFAULT';
185 $opt{color} and defined $_[0] or return '';
186 return "\e[$_[0]m" if defined wantarray;
187 $_ = color(@_) . $_ . color(0) if defined;
193 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
195 @lines > $nr or return;
198 if (defined $opt{hidemax}) {
199 if ($opt{hidemin} and $opt{hidemin} < 0) {
200 $limit -= $opt{hidemax} - 1;
203 $limit = $opt{hidemax} - 1;
207 @order = sort { $b <=> $a } @order unless tied @order;
208 my $maxval = $opt{maxval} // (
209 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
212 my $minval = $opt{minval} // min $order[-1] // (), 0;
213 my $range = $maxval - $minval;
214 my $lenval = $opt{'value-length'} // max map { length } @order;
215 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
216 max map { length $values[$_] && length $lines[$_] }
217 0 .. min $#lines, $opt{hidemax} || (); # left padding
218 my $size = defined $opt{width} && $range &&
219 ($opt{width} - $lenval - $len) / $range; # bar multiplication
222 if ($opt{markers} and $size > 0) {
223 for my $markspec (split /\h/, $opt{markers}) {
224 my ($char, $func) = split //, $markspec, 2;
226 if ($func eq 'avg') {
227 return sum(@order) / @order;
229 elsif ($func =~ /\A([0-9.]+)v\z/) {
230 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
231 my $index = $#order * $1 / 100;
232 return ($order[$index] + $order[$index + .5]) / 2;
234 elsif ($func =~ /\A-?[0-9.]+\z/) {
238 die "Unknown marker $char: $func\n";
247 color(36) for $barmark[$pos * $size] = $char;
250 state $lastmax = $maxval;
251 if ($maxval > $lastmax) {
252 print ' ' x ($lenval + $len);
255 ($lastmax - $minval) * $size + .5,
256 '-' x (($values[$nr - 1] - $minval) * $size);
258 say '+' x (($range - $lastmax) * $size + .5);
265 color(31), sprintf('%*s', $lenval, $minval),
266 color(90), '-', color(36), '+',
267 color(32), sprintf('%*s', $size * $range - 3, $maxval),
268 color(90), '-', color(36), '+',
272 while ($nr <= $limit) {
273 my $val = $values[$nr];
274 my $rel = length $val && $range && ($val - $minval) / $range;
275 my $color = !length $val || !$opt{palette} ? undef :
276 $val == $order[0] ? $opt{palette}->[-1] : # max
277 $val == $order[-1] ? $opt{palette}->[0] : # min
278 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
281 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
282 print color($color), $opt{spark}->[
283 !$val || !$#{$opt{spark}} ? 0 : # blank
284 $val == $order[0] ? -1 : # max
285 $val == $order[-1] ? 1 : # min
286 $#{$opt{spark}} < 3 ? 1 :
287 $rel * ($#{$opt{spark}} - 3) + 2.5
293 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
294 sprintf "%*s", $lenval, $val;
295 color($color) for $val;
297 my $line = $lines[$nr] =~ s/\n/$val/r;
298 if (not length $val) {
302 printf '%-*s', $len + length($val), $line;
303 print $barmark[$_] // $opt{'graph-format'}
304 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
310 say $opt{palette} ? color(0) : '' if $opt{spark};
316 if ($opt{hidemin} or $opt{hidemax}) {
317 my $linemin = $opt{hidemin};
318 my $linemax = ($opt{hidemax} || @lines) - 1;
321 $linemax = @lines - $linemax;
323 printf '%.8g of ', $opt{'sum-format'}->(
324 sum(grep {length} @values[$linemin .. $linemax]) // 0
328 my $total = sum @order;
329 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
330 printf ' in %d values', scalar @order;
331 printf ' over %d lines', scalar @lines if @order != @lines;
332 printf(' (%s min, %s avg, %s max)',
333 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
334 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
335 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
344 show_stat() if $opt{stat};
345 exit 130 if @_; # 0x80+signo
353 barcat [OPTIONS] [FILES|NUMBERS]
356 -a, --[no-]ascii Restrict user interface to ASCII characters
357 -c, --[no-]color Force colored output of values and bar markers
358 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
360 --header Prepend a chart axis with minimum and maximum
362 -H, --human-readable Format values using SI unit prefixes
363 -t, --interval[=(N|-LINES)]
364 Output partial progress every given number of
365 seconds or input lines
366 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
367 -L, --limit[=(N|-LAST|START-[END])]
368 Stop output after a number of lines
369 --graph-format=CHAR Glyph to repeat for the graph line
370 -m, --markers=FORMAT Statistical positions to indicate on bars
371 --min=N, --max=N Bars extend from 0 or the minimum value if lower
372 --palette=(PRESET|COLORS)
373 Override colors of parsed numbers
374 --spark[=CHARS] Replace lines by sparklines
375 -s, --stat Total statistics after all data
376 -u, --unmodified Do not reformat values, keeping leading whitespace
377 --value-length=SIZE Reserved space for numbers
378 -w, --width=COLUMNS Override the maximum number of columns to use
379 -h, --usage Overview of available options
380 --help Full documentation
381 --version Version information
387 barcat - graph to visualize input values
391 B<barcat> [<options>] [<file>... | <numbers>]
395 Visualizes relative sizes of values read from input
396 (parameters, file(s) or STDIN).
397 Contents are concatenated similar to I<cat>,
398 but numbers are reformatted and a bar graph is appended to each line.
400 Don't worry, barcat does not drink and divide.
401 It can has various options for input and output (re)formatting,
402 but remains limited to one-dimensional charts.
403 For more complex graphing needs
404 you'll need a larger animal like I<gnuplot>.
410 =item -a, --[no-]ascii
412 Restrict user interface to ASCII characters,
413 replacing default UTF-8 by their closest approximation.
414 Input is always interpreted as UTF-8 and shown as is.
416 =item -c, --[no-]color
418 Force colored output of values and bar markers.
419 Defaults on if output is a tty,
420 disabled otherwise such as when piped or redirected.
422 =item -f, --field=(<number> | <regexp>)
424 Compare values after a given number of whitespace separators,
425 or matching a regular expression.
427 Unspecified or I<-f0> means values are at the start of each line.
428 With I<-f1> the second word is taken instead.
429 A string can indicate the starting position of a value
430 (such as I<-f:> if preceded by colons),
431 or capture the numbers itself,
432 for example I<-f'(\d+)'> for the first digits anywhere.
436 Prepend a chart axis with minimum and maximum values labeled.
438 =item -H, --human-readable
440 Format values using SI unit prefixes,
441 turning long numbers like I<12356789> into I<12.4M>.
442 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
443 Short integers are aligned but kept without decimal point.
445 =item -t, --interval[=(<seconds> | -<lines>)]
447 Output partial progress every given number of seconds or input lines.
448 An update can also be forced by sending a I<SIGALRM> alarm signal.
450 =item -l, --length=[-]<size>[%]
452 Trim line contents (between number and bars)
453 to a maximum number of characters.
454 The exceeding part is replaced by an abbreviation sign,
455 unless C<--length=0>.
457 Prepend a dash (i.e. make negative) to enforce padding
458 regardless of encountered contents.
460 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
462 Stop output after a number of lines.
463 A single value indicates the last line number (like C<head>),
464 or first line counting from the bottom if negative (like C<tail>).
465 A specific range can be given by two values.
467 All input is still counted and analyzed for statistics,
468 but disregarded for padding and bar size.
470 =item --graph-format=<character>
472 Glyph to repeat for the graph line.
473 Defaults to a dash C<->.
475 =item -m, --markers=<format>
477 Statistical positions to indicate on bars.
478 A single indicator glyph precedes each position:
484 Exact value to match on the axis.
485 A vertical bar at the zero crossing is displayed by I<|0>
487 For example I<:3.14> would show a colon at pi.
489 =item <percentage>I<v>
491 Ranked value at the given percentile.
492 The default shows I<+> at I<50v> for the mean or median;
493 the middle value or average between middle values.
494 One standard deviation right of the mean is at about I<68.3v>.
495 The default includes I<< >31.73v <68.27v >>
496 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
501 the sum of all values divided by the number of counted lines.
502 Indicated by default as I<=>.
506 =item --min=<number>, --max=<number>
508 Bars extend from 0 or the minimum value if lower,
509 to the largest value encountered.
510 These options can be set to customize this range.
512 =item --palette=(<preset> | <color>...)
514 Override colors of parsed numbers.
515 Can be any CSI escape, such as I<90> for default dark grey,
516 or alternatively I<1;30> for bright black.
518 In case of additional colors,
519 the last is used for values equal to the maximum, the first for minima.
520 If unspecified, these are green and red respectively (I<31 90 32>).
521 Multiple intermediate colors will be distributed
522 relative to the size of values.
524 Predefined color schemes are named I<whites> and I<fire>,
525 or I<greys> and I<fire256> for 256-color variants.
527 =item --spark[=<characters>]
529 Replace lines by I<sparklines>,
530 single characters corresponding to input values.
531 A specified sequence of unicode characters will be used for
532 Of a specified sequence of unicode characters,
533 the first one will be used for non-values,
534 the last one for the maximum,
535 the second (if any) for the minimum,
536 and any remaining will be distributed over the range of values.
537 Unspecified, block fill glyphs U+2581-2588 will be used.
541 Total statistics after all data.
543 =item -u, --unmodified
545 Do not reformat values, keeping leading whitespace.
546 Keep original value alignment, which may be significant in some programs.
548 =item --value-length=<size>
550 Reserved space for numbers.
552 =item -w, --width=<columns>
554 Override the maximum number of columns to use.
555 Appended graphics will extend to fill up the entire screen.
559 Overview of available options.
576 seq 30 | awk '{print sin($1/10)}' | barcat
578 Compare file sizes (with human-readable numbers):
580 du -d0 -b * | barcat -H
582 Memory usage of user processes with long names truncated:
584 ps xo %mem,pid,cmd | barcat -l40
586 Monitor network latency from prefixed results:
588 ping google.com | barcat -f'time=\K' -t
590 Commonly used after counting, for example users on the current server:
592 users | tr ' ' '\n' | sort | uniq -c | barcat
594 Letter frequencies in text files:
596 cat /usr/share/games/fortunes/*.u8 |
597 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
598 sort | uniq -c | barcat
600 Number of HTTP requests per day:
602 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
604 Any kind of database query with counts, preserving returned alignment:
606 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
609 In PostgreSQL from within the client:
611 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
613 Earthquakes worldwide magnitude 1+ in the last 24 hours:
615 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
616 column -tns, | barcat -f4 -u -l80%
618 External datasets, like movies per year:
620 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
621 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
623 But please get I<jq> to process JSON
624 and replace the manual selection by C<< jq '.[].year' >>.
626 Pokémon height comparison:
628 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
629 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
631 USD/EUR exchange rate from CSV provided by the ECB:
633 curl https://sdw.ecb.europa.eu/export.do \
634 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
635 grep '^[12]' | barcat -f',\K' --value-length=7
637 Total population history in XML from the World Bank:
639 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
640 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
641 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
643 And of course various Git statistics, such commit count by year:
645 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
647 Or the top 3 most frequent authors with statistics over all:
649 git shortlog -sn | barcat -L3 -s
651 Sparkline graphics of simple input given as inline parameters:
653 barcat --spark= 3 1 4 1 5 0 9 2 4
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
663 Mischa POSLAWSKY <perl@shiar.org>