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 {
115 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
116 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
118 $float && ($unit % 3) == ($unit < 0), # tenths
119 $_[0] / 1000 ** int($unit/3), # number
120 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
125 my (@lines, @values, @order);
127 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
130 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
132 $SIG{INT} = \&show_exit;
134 if (defined $opt{interval}) {
135 $opt{interval} ||= 1;
136 alarm $opt{interval} if $opt{interval} > 0;
139 require Tie::Array::Sorted;
140 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
141 } or warn $@, "Expect slowdown with large datasets!\n";
145 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
147 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
149 s/\A\h*// unless $opt{unmodified};
150 my $valnum = s/$valmatch/\n/ && $1;
151 push @values, $valnum;
152 push @order, $valnum if length $valnum;
153 if (defined $opt{trim} and defined $valnum) {
154 my $trimpos = abs $opt{trim};
155 $trimpos -= length $valnum if $opt{unmodified};
157 $_ = substr $_, 0, 2;
159 elsif (length > $trimpos) {
160 # cut and replace (intentional lvalue for speed, contrary to PBP)
161 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
165 show_lines() if defined $opt{interval} and $opt{interval} < 0
166 and $. % $opt{interval} == 0;
169 if ($opt{'zero-missing'}) {
170 push @values, (0) x 10;
173 $SIG{INT} = 'DEFAULT';
176 $opt{color} and defined $_[0] or return '';
177 return "\e[$_[0]m" if defined wantarray;
178 $_ = color(@_) . $_ . color(0) if defined;
184 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
186 @lines > $nr or return;
189 if (defined $opt{hidemax}) {
190 if ($opt{hidemin} and $opt{hidemin} < 0) {
191 $limit -= $opt{hidemax} - 1;
194 $limit = $opt{hidemax} - 1;
198 @order = sort { $b <=> $a } @order unless tied @order;
199 my $maxval = $opt{maxval} // (
200 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
203 my $minval = $opt{minval} // min $order[-1] // (), 0;
204 my $range = $maxval - $minval;
205 my $lenval = $opt{'value-length'} // max map { length } @order;
206 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
207 max map { length $values[$_] && length $lines[$_] }
208 0 .. min $#lines, $opt{hidemax} || (); # left padding
209 my $size = defined $opt{width} && $range &&
210 ($opt{width} - $lenval - $len) / $range; # bar multiplication
213 if ($opt{markers} and $size > 0) {
214 for my $markspec (split /\h/, $opt{markers}) {
215 my ($char, $func) = split //, $markspec, 2;
217 if ($func eq 'avg') {
218 return sum(@order) / @order;
220 elsif ($func =~ /\A([0-9.]+)v\z/) {
221 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
222 my $index = $#order * $1 / 100;
223 return ($order[$index] + $order[$index + .5]) / 2;
225 elsif ($func =~ /\A-?[0-9.]+\z/) {
229 die "Unknown marker $char: $func\n";
238 color(36) for $barmark[$pos * $size] = $char;
241 state $lastmax = $maxval;
242 if ($maxval > $lastmax) {
243 print ' ' x ($lenval + $len);
246 ($lastmax - $minval) * $size + .5,
247 '-' x (($values[$nr - 1] - $minval) * $size);
249 say '+' x (($range - $lastmax) * $size + .5);
256 color(31), sprintf('%*s', $lenval, $minval),
257 color(90), '-', color(36), '+',
258 color(32), sprintf('%*s', $size * $range - 3, $maxval),
259 color(90), '-', color(36), '+',
263 while ($nr <= $limit) {
264 my $val = $values[$nr];
265 my $rel = length $val && $range && ($val - $minval) / $range;
266 my $color = !length $val || !$opt{palette} ? undef :
267 $val == $order[0] ? $opt{palette}->[-1] : # max
268 $val == $order[-1] ? $opt{palette}->[0] : # min
269 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
272 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
273 print color($color), $opt{spark}->[
274 !$val || !$#{$opt{spark}} ? 0 : # blank
275 $val == $order[0] ? -1 : # max
276 $val == $order[-1] ? 1 : # min
277 $#{$opt{spark}} < 3 ? 1 :
278 $rel * ($#{$opt{spark}} - 3) + 2.5
284 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
285 sprintf "%*s", $lenval, $val;
286 color($color) for $val;
288 my $line = $lines[$nr] =~ s/\n/$val/r;
289 printf '%-*s', $len + length($val), $line;
290 print $barmark[$_] // $opt{'graph-format'}
291 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
297 say $opt{palette} ? color(0) : '' if $opt{spark};
303 if ($opt{hidemin} or $opt{hidemax}) {
304 my $linemin = $opt{hidemin};
305 my $linemax = ($opt{hidemax} || @lines) - 1;
308 $linemax = @lines - $linemax;
310 printf '%.8g of ', $opt{'sum-format'}->(
311 sum(grep {length} @values[$linemin .. $linemax]) // 0
315 my $total = sum @order;
316 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
317 printf ' in %d values', scalar @order;
318 printf ' over %d lines', scalar @lines if @order != @lines;
319 printf(' (%s min, %s avg, %s max)',
320 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
321 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
322 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
331 show_stat() if $opt{stat};
332 exit 130 if @_; # 0x80+signo
340 barcat [OPTIONS] [FILES|NUMBERS]
343 -a, --[no-]ascii Restrict user interface to ASCII characters
344 -c, --[no-]color Force colored output of values and bar markers
345 -f, --field=(N|REGEXP) Compare values after a given number of whitespace
347 --header Prepend a chart axis with minimum and maximum
349 -H, --human-readable Format values using SI unit prefixes
350 -t, --interval[=(N|-LINES)]
351 Output partial progress every given number of
352 seconds or input lines
353 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
354 -L, --limit[=(N|-LAST|START-[END])]
355 Stop output after a number of lines
356 --graph-format=CHAR Glyph to repeat for the graph line
357 -m, --markers=FORMAT Statistical positions to indicate on bars
358 --min=N, --max=N Bars extend from 0 or the minimum value if lower
359 --palette=(PRESET|COLORS)
360 Override colors of parsed numbers
361 --spark[=CHARS] Replace lines by sparklines
362 -s, --stat Total statistics after all data
363 -u, --unmodified Do not reformat values, keeping leading whitespace
364 --value-length=SIZE Reserved space for numbers
365 -w, --width=COLUMNS Override the maximum number of columns to use
366 -h, --usage Overview of available options
367 --help Full documentation
368 --version Version information
374 barcat - graph to visualize input values
378 B<barcat> [<options>] [<file>... | <numbers>]
382 Visualizes relative sizes of values read from input
383 (parameters, file(s) or STDIN).
384 Contents are concatenated similar to I<cat>,
385 but numbers are reformatted and a bar graph is appended to each line.
387 Don't worry, barcat does not drink and divide.
388 It can has various options for input and output (re)formatting,
389 but remains limited to one-dimensional charts.
390 For more complex graphing needs
391 you'll need a larger animal like I<gnuplot>.
397 =item -a, --[no-]ascii
399 Restrict user interface to ASCII characters,
400 replacing default UTF-8 by their closest approximation.
401 Input is always interpreted as UTF-8 and shown as is.
403 =item -c, --[no-]color
405 Force colored output of values and bar markers.
406 Defaults on if output is a tty,
407 disabled otherwise such as when piped or redirected.
409 =item -f, --field=(<number> | <regexp>)
411 Compare values after a given number of whitespace separators,
412 or matching a regular expression.
414 Unspecified or I<-f0> means values are at the start of each line.
415 With I<-f1> the second word is taken instead.
416 A string can indicate the starting position of a value
417 (such as I<-f:> if preceded by colons),
418 or capture the numbers itself,
419 for example I<-f'(\d+)'> for the first digits anywhere.
423 Prepend a chart axis with minimum and maximum values labeled.
425 =item -H, --human-readable
427 Format values using SI unit prefixes,
428 turning long numbers like I<12356789> into I<12.4M>.
429 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
430 Short integers are aligned but kept without decimal point.
432 =item -t, --interval[=(<seconds> | -<lines>)]
434 Output partial progress every given number of seconds or input lines.
435 An update can also be forced by sending a I<SIGALRM> alarm signal.
437 =item -l, --length=[-]<size>[%]
439 Trim line contents (between number and bars)
440 to a maximum number of characters.
441 The exceeding part is replaced by an abbreviation sign,
442 unless C<--length=0>.
444 Prepend a dash (i.e. make negative) to enforce padding
445 regardless of encountered contents.
447 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
449 Stop output after a number of lines.
450 A single value indicates the last line number (like C<head>),
451 or first line counting from the bottom if negative (like C<tail>).
452 A specific range can be given by two values.
454 All input is still counted and analyzed for statistics,
455 but disregarded for padding and bar size.
457 =item --graph-format=<character>
459 Glyph to repeat for the graph line.
460 Defaults to a dash C<->.
462 =item -m, --markers=<format>
464 Statistical positions to indicate on bars.
465 A single indicator glyph precedes each position:
471 Exact value to match on the axis.
472 A vertical bar at the zero crossing is displayed by I<|0>
474 For example I<:3.14> would show a colon at pi.
476 =item <percentage>I<v>
478 Ranked value at the given percentile.
479 The default shows I<+> at I<50v> for the mean or median;
480 the middle value or average between middle values.
481 One standard deviation right of the mean is at about I<68.3v>.
482 The default includes I<< >31.73v <68.27v >>
483 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
488 the sum of all values divided by the number of counted lines.
489 Indicated by default as I<=>.
493 =item --min=<number>, --max=<number>
495 Bars extend from 0 or the minimum value if lower,
496 to the largest value encountered.
497 These options can be set to customize this range.
499 =item --palette=(<preset> | <color>...)
501 Override colors of parsed numbers.
502 Can be any CSI escape, such as I<90> for default dark grey,
503 or alternatively I<1;30> for bright black.
505 In case of additional colors,
506 the last is used for values equal to the maximum, the first for minima.
507 If unspecified, these are green and red respectively (I<31 90 32>).
508 Multiple intermediate colors will be distributed
509 relative to the size of values.
511 Predefined color schemes are named I<whites> and I<fire>,
512 or I<greys> and I<fire256> for 256-color variants.
514 =item --spark[=<characters>]
516 Replace lines by I<sparklines>,
517 single characters corresponding to input values.
518 A specified sequence of unicode characters will be used for
519 Of a specified sequence of unicode characters,
520 the first one will be used for non-values,
521 the last one for the maximum,
522 the second (if any) for the minimum,
523 and any remaining will be distributed over the range of values.
524 Unspecified, block fill glyphs U+2581-2588 will be used.
528 Total statistics after all data.
530 =item -u, --unmodified
532 Do not reformat values, keeping leading whitespace.
533 Keep original value alignment, which may be significant in some programs.
535 =item --value-length=<size>
537 Reserved space for numbers.
539 =item -w, --width=<columns>
541 Override the maximum number of columns to use.
542 Appended graphics will extend to fill up the entire screen.
546 Overview of available options.
563 seq 30 | awk '{print sin($1/10)}' | barcat
565 Compare file sizes (with human-readable numbers):
567 du -d0 -b * | barcat -H
569 Memory usage of user processes with long names truncated:
571 ps xo %mem,pid,cmd | barcat -l40
573 Monitor network latency from prefixed results:
575 ping google.com | barcat -f'time=\K' -t
577 Commonly used after counting, for example users on the current server:
579 users | tr ' ' '\n' | sort | uniq -c | barcat
581 Letter frequencies in text files:
583 cat /usr/share/games/fortunes/*.u8 |
584 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
585 sort | uniq -c | barcat
587 Number of HTTP requests per day:
589 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
591 Any kind of database query with counts, preserving returned alignment:
593 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
596 In PostgreSQL from within the client:
598 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
600 Earthquakes worldwide magnitude 1+ in the last 24 hours:
602 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
603 column -tns, | barcat -f4 -u -l80%
605 External datasets, like movies per year:
607 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
608 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
610 But please get I<jq> to process JSON
611 and replace the manual selection by C<< jq '.[].year' >>.
613 Pokémon height comparison:
615 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
616 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
618 USD/EUR exchange rate from CSV provided by the ECB:
620 curl https://sdw.ecb.europa.eu/export.do \
621 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
622 grep '^[12]' | barcat -f',\K' --value-length=7
624 Total population history in XML from the World Bank:
626 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
627 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
628 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
630 And of course various Git statistics, such commit count by year:
632 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
634 Or the top 3 most frequent authors with statistics over all:
636 git shortlog -sn | barcat -L3 -s
638 Sparkline graphics of simple input given as inline parameters:
640 barcat --spark= 3 1 4 1 5 0 9 2 4
642 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
644 ( git log --pretty=%ci --since=30day | cut -b-10
645 seq 0 30 | xargs -i date +%F -d-{}day ) |
646 sort | uniq -c | awk '$1--' | barcat --spark
650 Mischa POSLAWSKY <perl@shiar.org>