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 if (not length $val) {
298 printf '%-*s', $len + length($val), $line;
299 print $barmark[$_] // $opt{'graph-format'}
300 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
306 say $opt{palette} ? color(0) : '' if $opt{spark};
312 if ($opt{hidemin} or $opt{hidemax}) {
313 my $linemin = $opt{hidemin};
314 my $linemax = ($opt{hidemax} || @lines) - 1;
317 $linemax = @lines - $linemax;
319 printf '%.8g of ', $opt{'sum-format'}->(
320 sum(grep {length} @values[$linemin .. $linemax]) // 0
324 my $total = sum @order;
325 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
326 printf ' in %d values', scalar @order;
327 printf ' over %d lines', scalar @lines if @order != @lines;
328 printf(' (%s min, %s avg, %s max)',
329 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
330 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
331 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
340 show_stat() if $opt{stat};
341 exit 130 if @_; # 0x80+signo
349 barcat [OPTIONS] [FILES|NUMBERS]
352 -a, --[no-]ascii Restrict user interface to ASCII characters
353 -c, --[no-]color Force colored output of values and bar markers
354 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
356 --header Prepend a chart axis with minimum and maximum
358 -H, --human-readable Format values using SI unit prefixes
359 -t, --interval[=(N|-LINES)]
360 Output partial progress every given number of
361 seconds or input lines
362 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
363 -L, --limit[=(N|-LAST|START-[END])]
364 Stop output after a number of lines
365 --graph-format=CHAR Glyph to repeat for the graph line
366 -m, --markers=FORMAT Statistical positions to indicate on bars
367 --min=N, --max=N Bars extend from 0 or the minimum value if lower
368 --palette=(PRESET|COLORS)
369 Override colors of parsed numbers
370 --spark[=CHARS] Replace lines by sparklines
371 -s, --stat Total statistics after all data
372 -u, --unmodified Do not reformat values, keeping leading whitespace
373 --value-length=SIZE Reserved space for numbers
374 -w, --width=COLUMNS Override the maximum number of columns to use
375 -h, --usage Overview of available options
376 --help Full documentation
377 --version Version information
383 barcat - graph to visualize input values
387 B<barcat> [<options>] [<file>... | <numbers>]
391 Visualizes relative sizes of values read from input
392 (parameters, file(s) or STDIN).
393 Contents are concatenated similar to I<cat>,
394 but numbers are reformatted and a bar graph is appended to each line.
396 Don't worry, barcat does not drink and divide.
397 It can has various options for input and output (re)formatting,
398 but remains limited to one-dimensional charts.
399 For more complex graphing needs
400 you'll need a larger animal like I<gnuplot>.
406 =item -a, --[no-]ascii
408 Restrict user interface to ASCII characters,
409 replacing default UTF-8 by their closest approximation.
410 Input is always interpreted as UTF-8 and shown as is.
412 =item -c, --[no-]color
414 Force colored output of values and bar markers.
415 Defaults on if output is a tty,
416 disabled otherwise such as when piped or redirected.
418 =item -f, --field=(<number> | <regexp>)
420 Compare values after a given number of whitespace separators,
421 or matching a regular expression.
423 Unspecified or I<-f0> means values are at the start of each line.
424 With I<-f1> the second word is taken instead.
425 A string can indicate the starting position of a value
426 (such as I<-f:> if preceded by colons),
427 or capture the numbers itself,
428 for example I<-f'(\d+)'> for the first digits anywhere.
432 Prepend a chart axis with minimum and maximum values labeled.
434 =item -H, --human-readable
436 Format values using SI unit prefixes,
437 turning long numbers like I<12356789> into I<12.4M>.
438 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
439 Short integers are aligned but kept without decimal point.
441 =item -t, --interval[=(<seconds> | -<lines>)]
443 Output partial progress every given number of seconds or input lines.
444 An update can also be forced by sending a I<SIGALRM> alarm signal.
446 =item -l, --length=[-]<size>[%]
448 Trim line contents (between number and bars)
449 to a maximum number of characters.
450 The exceeding part is replaced by an abbreviation sign,
451 unless C<--length=0>.
453 Prepend a dash (i.e. make negative) to enforce padding
454 regardless of encountered contents.
456 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
458 Stop output after a number of lines.
459 A single value indicates the last line number (like C<head>),
460 or first line counting from the bottom if negative (like C<tail>).
461 A specific range can be given by two values.
463 All input is still counted and analyzed for statistics,
464 but disregarded for padding and bar size.
466 =item --graph-format=<character>
468 Glyph to repeat for the graph line.
469 Defaults to a dash C<->.
471 =item -m, --markers=<format>
473 Statistical positions to indicate on bars.
474 A single indicator glyph precedes each position:
480 Exact value to match on the axis.
481 A vertical bar at the zero crossing is displayed by I<|0>
483 For example I<:3.14> would show a colon at pi.
485 =item <percentage>I<v>
487 Ranked value at the given percentile.
488 The default shows I<+> at I<50v> for the mean or median;
489 the middle value or average between middle values.
490 One standard deviation right of the mean is at about I<68.3v>.
491 The default includes I<< >31.73v <68.27v >>
492 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
497 the sum of all values divided by the number of counted lines.
498 Indicated by default as I<=>.
502 =item --min=<number>, --max=<number>
504 Bars extend from 0 or the minimum value if lower,
505 to the largest value encountered.
506 These options can be set to customize this range.
508 =item --palette=(<preset> | <color>...)
510 Override colors of parsed numbers.
511 Can be any CSI escape, such as I<90> for default dark grey,
512 or alternatively I<1;30> for bright black.
514 In case of additional colors,
515 the last is used for values equal to the maximum, the first for minima.
516 If unspecified, these are green and red respectively (I<31 90 32>).
517 Multiple intermediate colors will be distributed
518 relative to the size of values.
520 Predefined color schemes are named I<whites> and I<fire>,
521 or I<greys> and I<fire256> for 256-color variants.
523 =item --spark[=<characters>]
525 Replace lines by I<sparklines>,
526 single characters corresponding to input values.
527 A specified sequence of unicode characters will be used for
528 Of a specified sequence of unicode characters,
529 the first one will be used for non-values,
530 the last one for the maximum,
531 the second (if any) for the minimum,
532 and any remaining will be distributed over the range of values.
533 Unspecified, block fill glyphs U+2581-2588 will be used.
537 Total statistics after all data.
539 =item -u, --unmodified
541 Do not reformat values, keeping leading whitespace.
542 Keep original value alignment, which may be significant in some programs.
544 =item --value-length=<size>
546 Reserved space for numbers.
548 =item -w, --width=<columns>
550 Override the maximum number of columns to use.
551 Appended graphics will extend to fill up the entire screen.
555 Overview of available options.
572 seq 30 | awk '{print sin($1/10)}' | barcat
574 Compare file sizes (with human-readable numbers):
576 du -d0 -b * | barcat -H
578 Memory usage of user processes with long names truncated:
580 ps xo %mem,pid,cmd | barcat -l40
582 Monitor network latency from prefixed results:
584 ping google.com | barcat -f'time=\K' -t
586 Commonly used after counting, for example users on the current server:
588 users | tr ' ' '\n' | sort | uniq -c | barcat
590 Letter frequencies in text files:
592 cat /usr/share/games/fortunes/*.u8 |
593 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
594 sort | uniq -c | barcat
596 Number of HTTP requests per day:
598 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
600 Any kind of database query with counts, preserving returned alignment:
602 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
605 In PostgreSQL from within the client:
607 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
609 Earthquakes worldwide magnitude 1+ in the last 24 hours:
611 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
612 column -tns, | barcat -f4 -u -l80%
614 External datasets, like movies per year:
616 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
617 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
619 But please get I<jq> to process JSON
620 and replace the manual selection by C<< jq '.[].year' >>.
622 Pokémon height comparison:
624 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
625 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
627 USD/EUR exchange rate from CSV provided by the ECB:
629 curl https://sdw.ecb.europa.eu/export.do \
630 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
631 grep '^[12]' | barcat -f',\K' --value-length=7
633 Total population history in XML from the World Bank:
635 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
636 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
637 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
639 And of course various Git statistics, such commit count by year:
641 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
643 Or the top 3 most frequent authors with statistics over all:
645 git shortlog -sn | barcat -L3 -s
647 Sparkline graphics of simple input given as inline parameters:
649 barcat --spark= 3 1 4 1 5 0 9 2 4
651 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
653 ( git log --pretty=%ci --since=30day | cut -b-10
654 seq 0 30 | xargs -i date +%F -d-{}day ) |
655 sort | uniq -c | awk '$1--' | barcat --spark
659 Mischa POSLAWSKY <perl@shiar.org>