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 ($opt{hidemin}, $opt{hidemax}) =
43 $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/ or die(
44 "Value \"$optval\" invalid for option limit",
50 'graph-format=s' => sub {
51 $opt{'graph-format'} = substr $_[1], 0, 1;
54 $opt{spark} = [split //,
55 $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
60 fire => [qw( 90 31 91 33 93 97 96 )],
61 fire88 => [map {"38;5;$_"} qw(
62 80 32 48 64 68 72 76 77 78 79 47
64 fire256=> [map {"38;5;$_"} qw(
66 202 208 214 220 226 227 228 229 230 231 159
68 ramp88 => [map {"38;5;$_"} qw(
69 64 65 66 67 51 35 39 23 22 26 25 28
71 whites => [qw( 1;30 0;37 1;37 )],
72 greys => [map {"38;5;$_"} 52, 235..255, 47],
73 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
80 say "barcat version $VERSION";
84 local $/ = undef; # slurp
85 my $pod = readline *DATA;
86 $pod =~ s/^=over\K/ 25/; # indent options list
88 ^=item \h \N*\n\n \N*\n \K # first line
89 (?: (?: ^=over .*? ^=back\n )? (?!=) \N*\n )*
90 }{\n}g; # abbreviate options
91 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
92 $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
93 # abbreviate <variable> indicators
94 $pod =~ s/\Q>.../s>/g;
95 $pod =~ s/<(?:number|count|seconds)>/N/g;
96 $pod =~ s/<character(s?)>/\Uchar$1/g;
98 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
101 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
102 -indent => 2, -width => 78,
104 $parser->select('SYNOPSIS', 'OPTIONS');
105 $parser->output_string(\my $contents);
106 $parser->parse_string_document($pod);
108 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
109 $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
115 Pod::Usage::pod2usage(
116 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
119 ) or exit 64; # EX_USAGE
121 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
122 $opt{color} //= -t *STDOUT; # enable on tty
123 $opt{'graph-format'} //= '-';
124 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
125 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
126 if $opt{'human-readable'};
127 $opt{anchor} //= qr/\A/;
128 $opt{'value-length'} = 6 if $opt{units};
129 $opt{'value-length'} = 1 if $opt{unmodified};
130 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
131 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
132 $opt{palette} //= $opt{color} && [31, 90, 32];
133 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
134 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
135 and undef $opt{interval};
137 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
138 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
139 $opt{'value-format'} = $opt{units} && sub {
140 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
141 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
143 $float && ($unit % 3) == ($unit < 0), # tenths
144 $_[0] / 1000 ** int($unit/3), # number
145 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
150 my (@lines, @values, @order);
152 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
155 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
157 $SIG{INT} = \&show_exit;
159 if (defined $opt{interval}) {
160 $opt{interval} ||= 1;
161 alarm $opt{interval} if $opt{interval} > 0;
164 require Tie::Array::Sorted;
165 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
166 } or warn $@, "Expect slowdown with large datasets!\n";
170 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
172 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
174 s/\A\h*// unless $opt{unmodified};
175 my $valnum = s/$valmatch/\n/ && $1;
176 push @values, $valnum;
177 push @order, $valnum if length $valnum;
178 if (defined $opt{trim} and defined $valnum) {
179 my $trimpos = abs $opt{trim};
180 $trimpos -= length $valnum if $opt{unmodified};
182 $_ = substr $_, 0, 2;
184 elsif (length > $trimpos) {
185 # cut and replace (intentional lvalue for speed, contrary to PBP)
186 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
190 show_lines() if defined $opt{interval} and $opt{interval} < 0
191 and $. % $opt{interval} == 0;
194 if ($opt{'zero-missing'}) {
195 push @values, (0) x 10;
198 $SIG{INT} = 'DEFAULT';
201 $opt{color} and defined $_[0] or return '';
202 return "\e[$_[0]m" if defined wantarray;
203 $_ = color(@_) . $_ . color(0) if defined;
208 state $nr = $opt{hidemin};
210 @lines > $nr or return;
212 @order = sort { $b <=> $a } @order unless tied @order;
213 my $maxval = $opt{maxval} // (
214 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
217 my $minval = $opt{minval} // min $order[-1] // (), 0;
218 my $range = $maxval - $minval;
219 my $lenval = $opt{'value-length'} // max map { length } @order;
220 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
221 max map { length $values[$_] && length $lines[$_] }
222 0 .. min $#lines, $opt{hidemax} || (); # left padding
223 my $size = defined $opt{width} && $range &&
224 ($opt{width} - $lenval - $len) / $range; # bar multiplication
227 if ($opt{markers} and $size > 0) {
228 for my $markspec (split /\h/, $opt{markers}) {
229 my ($char, $func) = split //, $markspec, 2;
231 if ($func eq 'avg') {
232 return sum(@order) / @order;
234 elsif ($func =~ /\A([0-9.]+)v\z/) {
235 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
236 my $index = $#order * $1 / 100;
237 return ($order[$index] + $order[$index + .5]) / 2;
239 elsif ($func =~ /\A-?[0-9.]+\z/) {
243 die "Unknown marker $char: $func\n";
252 color(36) for $barmark[$pos * $size] = $char;
255 state $lastmax = $maxval;
256 if ($maxval > $lastmax) {
257 print ' ' x ($lenval + $len);
260 ($lastmax - $minval) * $size + .5,
261 '-' x (($values[$nr - 1] - $minval) * $size);
263 say '+' x (($range - $lastmax) * $size + .5);
270 color(31), sprintf('%*s', $lenval, $minval),
271 color(90), '-', color(36), '+',
272 color(32), sprintf('%*s', $size * $range - 3, $maxval),
273 color(90), '-', color(36), '+',
277 while ($nr <= $#lines) {
278 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
279 my $val = $values[$nr];
280 my $rel = length $val && $range && ($val - $minval) / $range;
281 my $color = !length $val || !$opt{palette} ? undef :
282 $val == $order[0] ? $opt{palette}->[-1] : # max
283 $val == $order[-1] ? $opt{palette}->[0] : # min
284 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
287 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
288 print color($color), $opt{spark}->[
289 !$val || !$#{$opt{spark}} ? 0 : # blank
290 $val == $order[0] ? -1 : # max
291 $val == $order[-1] ? 1 : # min
292 $#{$opt{spark}} < 3 ? 1 :
293 $rel * ($#{$opt{spark}} - 3) + 2.5
299 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
300 sprintf "%*s", $lenval, $val;
301 color($color) for $val;
303 my $line = $lines[$nr] =~ s/\n/$val/r;
304 printf '%-*s', $len + length($val), $line;
305 print $barmark[$_] // $opt{'graph-format'}
306 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
312 say $opt{palette} ? color(0) : '' if $opt{spark};
318 if ($opt{hidemin} or $opt{hidemax}) {
319 printf '%.8g of ', $opt{'sum-format'}->(sum(grep { length }
320 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
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) . $order[-1] . color(0),
330 color(36) . $opt{'calc-format'}->($total / @order) . color(0),
331 color(32) . $order[0] . color(0),
340 show_stat() if $opt{stat};
341 exit 130 if @_; # 0x80+signo
352 barcat - graph to visualize input values
356 B<barcat> [<options>] [<file>... | <numbers>]
360 Visualizes relative sizes of values read from input
361 (parameters, file(s) or STDIN).
362 Contents are concatenated similar to I<cat>,
363 but numbers are reformatted and a bar graph is appended to each line.
365 Don't worry, barcat does not drink and divide.
366 It can has various options for input and output (re)formatting,
367 but remains limited to one-dimensional charts.
368 For more complex graphing needs
369 you'll need a larger animal like I<gnuplot>.
375 =item -a, --[no-]ascii
377 Restrict user interface to ASCII characters,
378 replacing default UTF-8 by their closest approximation.
379 Input is always interpreted as UTF-8 and shown as is.
381 =item -c, --[no-]color
383 Force colored output of values and bar markers.
384 Defaults on if output is a tty,
385 disabled otherwise such as when piped or redirected.
387 =item -f, --field=(<number> | <regexp>)
389 Compare values after a given number of whitespace separators,
390 or matching a regular expression.
392 Unspecified or I<-f0> means values are at the start of each line.
393 With I<-f1> the second word is taken instead.
394 A string can indicate the starting position of a value
395 (such as I<-f:> if preceded by colons),
396 or capture the numbers itself,
397 for example I<-f'(\d+)'> for the first digits anywhere.
401 Prepend a chart axis with minimum and maximum values labeled.
403 =item -H, --human-readable
405 Format values using SI unit prefixes,
406 turning long numbers like I<12356789> into I<12.4M>.
407 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
408 Short integers are aligned but kept without decimal point.
410 =item -t, --interval[=(<seconds> | -<lines>)]
412 Output partial progress every given number of seconds or input lines.
413 An update can also be forced by sending a I<SIGALRM> alarm signal.
415 =item -l, --length=[-]<size>[%]
417 Trim line contents (between number and bars)
418 to a maximum number of characters.
419 The exceeding part is replaced by an abbreviation sign,
420 unless C<--length=0>.
422 Prepend a dash (i.e. make negative) to enforce padding
423 regardless of encountered contents.
425 =item -L, --limit[=(<count> | <start>-[<end>])]
427 Stop output after a number of lines.
428 All input is still counted and analyzed for statistics,
429 but disregarded for padding and bar size.
431 =item --graph-format=<character>
433 Glyph to repeat for the graph line.
434 Defaults to a dash C<->.
436 =item -m, --markers=<format>
438 Statistical positions to indicate on bars.
439 A single indicator glyph precedes each position:
445 Exact value to match on the axis.
446 A vertical bar at the zero crossing is displayed by I<|0>
448 For example I<:3.14> would show a colon at pi.
450 =item <percentage>I<v>
452 Ranked value at the given percentile.
453 The default shows I<+> at I<50v> for the mean or median;
454 the middle value or average between middle values.
455 One standard deviation right of the mean is at about I<68.3v>.
456 The default includes I<< >31.73v <68.27v >>
457 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
462 the sum of all values divided by the number of counted lines.
463 Indicated by default as I<=>.
467 =item --min=<number>, --max=<number>
469 Bars extend from 0 or the minimum value if lower,
470 to the largest value encountered.
471 These options can be set to customize this range.
473 =item --palette=(<preset> | <color>...)
475 Override colors of parsed numbers.
476 Can be any CSI escape, such as I<90> for default dark grey,
477 or alternatively I<1;30> for bright black.
479 In case of additional colors,
480 the last is used for values equal to the maximum, the first for minima.
481 If unspecified, these are green and red respectively (I<31 90 32>).
482 Multiple intermediate colors will be distributed
483 relative to the size of values.
485 Predefined color schemes are named I<whites> and I<fire>,
486 or I<greys> and I<fire256> for 256-color variants.
488 =item --spark[=<characters>]
490 Replace lines by I<sparklines>,
491 single characters corresponding to input values.
492 A specified sequence of unicode characters will be used for
493 Of a specified sequence of unicode characters,
494 the first one will be used for non-values,
495 the last one for the maximum,
496 the second (if any) for the minimum,
497 and any remaining will be distributed over the range of values.
498 Unspecified, block fill glyphs U+2581-2588 will be used.
502 Total statistics after all data.
504 =item -u, --unmodified
506 Do not reformat values, keeping leading whitespace.
507 Keep original value alignment, which may be significant in some programs.
509 =item --value-length=<size>
511 Reserved space for numbers.
513 =item -w, --width=<columns>
515 Override the maximum number of columns to use.
516 Appended graphics will extend to fill up the entire screen.
520 Overview of available options.
537 seq 30 | awk '{print sin($1/10)}' | barcat
539 Compare file sizes (with human-readable numbers):
541 du -d0 -b * | barcat -H
543 Memory usage of user processes with long names truncated:
545 ps xo %mem,pid,cmd | barcat -l40
547 Monitor network latency from prefixed results:
549 ping google.com | barcat -f'time=\K' -t
551 Commonly used after counting, for example users on the current server:
553 users | tr ' ' '\n' | sort | uniq -c | barcat
555 Letter frequencies in text files:
557 cat /usr/share/games/fortunes/*.u8 |
558 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
559 sort | uniq -c | barcat
561 Number of HTTP requests per day:
563 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
565 Any kind of database query with counts, preserving returned alignment:
567 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
570 In PostgreSQL from within the client:
572 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
574 Earthquakes worldwide magnitude 1+ in the last 24 hours:
576 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
577 column -tns, | barcat -f4 -u -l80%
579 External datasets, like movies per year:
581 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
582 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
584 But please get I<jq> to process JSON
585 and replace the manual selection by C<< jq '.[].year' >>.
587 Pokémon height comparison:
589 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
590 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
592 USD/EUR exchange rate from CSV provided by the ECB:
594 curl https://sdw.ecb.europa.eu/export.do \
595 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
596 grep '^[12]' | barcat -f',\K' --value-length=7
598 Total population history in XML from the World Bank:
600 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
601 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
602 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
604 And of course various Git statistics, such commit count by year:
606 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
608 Or the top 3 most frequent authors with statistics over all:
610 git shortlog -sn | barcat -L3 -s
612 Sparkline graphics of simple input given as inline parameters:
614 barcat --spark= 3 1 4 1 5 0 9 2 4
616 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
618 ( git log --pretty=%ci --since=30day | cut -b-10
619 seq 0 30 | xargs -i date +%F -d-{}day ) |
620 sort | uniq -c | awk '$1--' | barcat --spark
624 Mischa POSLAWSKY <perl@shiar.org>