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 local $/ = undef; # slurp
86 my $pod = readline *DATA;
87 $pod =~ s/^=over\K/ 25/; # indent options list
89 ^=item \h \N*\n\n \N*\n \K # first line
90 (?: (?: ^=over .*? ^=back\n )? (?!=) \N*\n )*
91 }{\n}g; # abbreviate options
92 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
93 $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
94 # abbreviate <variable> indicators
95 $pod =~ s/\Q>.../s>/g;
96 $pod =~ s/<(?:number|count|seconds)>/N/g;
97 $pod =~ s/<character(s?)>/\Uchar$1/g;
99 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
102 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
103 -indent => 2, -width => 78,
105 $parser->select('SYNOPSIS', 'OPTIONS');
106 $parser->output_string(\my $contents);
107 $parser->parse_string_document($pod);
109 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
110 $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
116 Pod::Usage::pod2usage(
117 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
120 ) or exit 64; # EX_USAGE
122 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
123 $opt{color} //= -t *STDOUT; # enable on tty
124 $opt{'graph-format'} //= '-';
125 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
126 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
127 if $opt{'human-readable'};
128 $opt{anchor} //= qr/\A/;
129 $opt{'value-length'} = 6 if $opt{units};
130 $opt{'value-length'} = 1 if $opt{unmodified};
131 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
132 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
133 $opt{palette} //= $opt{color} && [31, 90, 32];
134 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
135 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
136 and undef $opt{interval};
138 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
139 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
140 $opt{'value-format'} = $opt{units} && sub {
141 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
142 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
144 $float && ($unit % 3) == ($unit < 0), # tenths
145 $_[0] / 1000 ** int($unit/3), # number
146 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
151 my (@lines, @values, @order);
153 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
156 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
158 $SIG{INT} = \&show_exit;
160 if (defined $opt{interval}) {
161 $opt{interval} ||= 1;
162 alarm $opt{interval} if $opt{interval} > 0;
165 require Tie::Array::Sorted;
166 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
167 } or warn $@, "Expect slowdown with large datasets!\n";
171 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
173 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
175 s/\A\h*// unless $opt{unmodified};
176 my $valnum = s/$valmatch/\n/ && $1;
177 push @values, $valnum;
178 push @order, $valnum if length $valnum;
179 if (defined $opt{trim} and defined $valnum) {
180 my $trimpos = abs $opt{trim};
181 $trimpos -= length $valnum if $opt{unmodified};
183 $_ = substr $_, 0, 2;
185 elsif (length > $trimpos) {
186 # cut and replace (intentional lvalue for speed, contrary to PBP)
187 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
191 show_lines() if defined $opt{interval} and $opt{interval} < 0
192 and $. % $opt{interval} == 0;
195 if ($opt{'zero-missing'}) {
196 push @values, (0) x 10;
199 $SIG{INT} = 'DEFAULT';
202 $opt{color} and defined $_[0] or return '';
203 return "\e[$_[0]m" if defined wantarray;
204 $_ = color(@_) . $_ . color(0) if defined;
210 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
212 @lines > $nr or return;
215 if (defined $opt{hidemax}) {
216 if ($opt{hidemin} and $opt{hidemin} < 0) {
217 $limit -= $opt{hidemax} - 1;
220 $limit = $opt{hidemax} - 1;
224 @order = sort { $b <=> $a } @order unless tied @order;
225 my $maxval = $opt{maxval} // (
226 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
229 my $minval = $opt{minval} // min $order[-1] // (), 0;
230 my $range = $maxval - $minval;
231 my $lenval = $opt{'value-length'} // max map { length } @order;
232 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
233 max map { length $values[$_] && length $lines[$_] }
234 0 .. min $#lines, $opt{hidemax} || (); # left padding
235 my $size = defined $opt{width} && $range &&
236 ($opt{width} - $lenval - $len) / $range; # bar multiplication
239 if ($opt{markers} and $size > 0) {
240 for my $markspec (split /\h/, $opt{markers}) {
241 my ($char, $func) = split //, $markspec, 2;
243 if ($func eq 'avg') {
244 return sum(@order) / @order;
246 elsif ($func =~ /\A([0-9.]+)v\z/) {
247 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
248 my $index = $#order * $1 / 100;
249 return ($order[$index] + $order[$index + .5]) / 2;
251 elsif ($func =~ /\A-?[0-9.]+\z/) {
255 die "Unknown marker $char: $func\n";
264 color(36) for $barmark[$pos * $size] = $char;
267 state $lastmax = $maxval;
268 if ($maxval > $lastmax) {
269 print ' ' x ($lenval + $len);
272 ($lastmax - $minval) * $size + .5,
273 '-' x (($values[$nr - 1] - $minval) * $size);
275 say '+' x (($range - $lastmax) * $size + .5);
282 color(31), sprintf('%*s', $lenval, $minval),
283 color(90), '-', color(36), '+',
284 color(32), sprintf('%*s', $size * $range - 3, $maxval),
285 color(90), '-', color(36), '+',
289 while ($nr <= $limit) {
290 my $val = $values[$nr];
291 my $rel = length $val && $range && ($val - $minval) / $range;
292 my $color = !length $val || !$opt{palette} ? undef :
293 $val == $order[0] ? $opt{palette}->[-1] : # max
294 $val == $order[-1] ? $opt{palette}->[0] : # min
295 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
298 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
299 print color($color), $opt{spark}->[
300 !$val || !$#{$opt{spark}} ? 0 : # blank
301 $val == $order[0] ? -1 : # max
302 $val == $order[-1] ? 1 : # min
303 $#{$opt{spark}} < 3 ? 1 :
304 $rel * ($#{$opt{spark}} - 3) + 2.5
310 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
311 sprintf "%*s", $lenval, $val;
312 color($color) for $val;
314 my $line = $lines[$nr] =~ s/\n/$val/r;
315 printf '%-*s', $len + length($val), $line;
316 print $barmark[$_] // $opt{'graph-format'}
317 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
323 say $opt{palette} ? color(0) : '' if $opt{spark};
329 if ($opt{hidemin} or $opt{hidemax}) {
330 my $linemin = $opt{hidemin};
331 my $linemax = ($opt{hidemax} || @lines) - 1;
334 $linemax = @lines - $linemax;
336 printf '%.8g of ', $opt{'sum-format'}->(
337 sum(grep {length} @values[$linemin .. $linemax]) // 0
341 my $total = sum @order;
342 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
343 printf ' in %d values', scalar @order;
344 printf ' over %d lines', scalar @lines if @order != @lines;
345 printf(' (%s min, %s avg, %s max)',
346 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
347 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
348 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
357 show_stat() if $opt{stat};
358 exit 130 if @_; # 0x80+signo
369 barcat - graph to visualize input values
373 B<barcat> [<options>] [<file>... | <numbers>]
377 Visualizes relative sizes of values read from input
378 (parameters, file(s) or STDIN).
379 Contents are concatenated similar to I<cat>,
380 but numbers are reformatted and a bar graph is appended to each line.
382 Don't worry, barcat does not drink and divide.
383 It can has various options for input and output (re)formatting,
384 but remains limited to one-dimensional charts.
385 For more complex graphing needs
386 you'll need a larger animal like I<gnuplot>.
392 =item -a, --[no-]ascii
394 Restrict user interface to ASCII characters,
395 replacing default UTF-8 by their closest approximation.
396 Input is always interpreted as UTF-8 and shown as is.
398 =item -c, --[no-]color
400 Force colored output of values and bar markers.
401 Defaults on if output is a tty,
402 disabled otherwise such as when piped or redirected.
404 =item -f, --field=(<number> | <regexp>)
406 Compare values after a given number of whitespace separators,
407 or matching a regular expression.
409 Unspecified or I<-f0> means values are at the start of each line.
410 With I<-f1> the second word is taken instead.
411 A string can indicate the starting position of a value
412 (such as I<-f:> if preceded by colons),
413 or capture the numbers itself,
414 for example I<-f'(\d+)'> for the first digits anywhere.
418 Prepend a chart axis with minimum and maximum values labeled.
420 =item -H, --human-readable
422 Format values using SI unit prefixes,
423 turning long numbers like I<12356789> into I<12.4M>.
424 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
425 Short integers are aligned but kept without decimal point.
427 =item -t, --interval[=(<seconds> | -<lines>)]
429 Output partial progress every given number of seconds or input lines.
430 An update can also be forced by sending a I<SIGALRM> alarm signal.
432 =item -l, --length=[-]<size>[%]
434 Trim line contents (between number and bars)
435 to a maximum number of characters.
436 The exceeding part is replaced by an abbreviation sign,
437 unless C<--length=0>.
439 Prepend a dash (i.e. make negative) to enforce padding
440 regardless of encountered contents.
442 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
444 Stop output after a number of lines.
445 A single value indicates the last line number (like C<head>),
446 or first line counting from the bottom if negative (like C<tail>).
447 A specific range can be given by two values.
449 All input is still counted and analyzed for statistics,
450 but disregarded for padding and bar size.
452 =item --graph-format=<character>
454 Glyph to repeat for the graph line.
455 Defaults to a dash C<->.
457 =item -m, --markers=<format>
459 Statistical positions to indicate on bars.
460 A single indicator glyph precedes each position:
466 Exact value to match on the axis.
467 A vertical bar at the zero crossing is displayed by I<|0>
469 For example I<:3.14> would show a colon at pi.
471 =item <percentage>I<v>
473 Ranked value at the given percentile.
474 The default shows I<+> at I<50v> for the mean or median;
475 the middle value or average between middle values.
476 One standard deviation right of the mean is at about I<68.3v>.
477 The default includes I<< >31.73v <68.27v >>
478 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
483 the sum of all values divided by the number of counted lines.
484 Indicated by default as I<=>.
488 =item --min=<number>, --max=<number>
490 Bars extend from 0 or the minimum value if lower,
491 to the largest value encountered.
492 These options can be set to customize this range.
494 =item --palette=(<preset> | <color>...)
496 Override colors of parsed numbers.
497 Can be any CSI escape, such as I<90> for default dark grey,
498 or alternatively I<1;30> for bright black.
500 In case of additional colors,
501 the last is used for values equal to the maximum, the first for minima.
502 If unspecified, these are green and red respectively (I<31 90 32>).
503 Multiple intermediate colors will be distributed
504 relative to the size of values.
506 Predefined color schemes are named I<whites> and I<fire>,
507 or I<greys> and I<fire256> for 256-color variants.
509 =item --spark[=<characters>]
511 Replace lines by I<sparklines>,
512 single characters corresponding to input values.
513 A specified sequence of unicode characters will be used for
514 Of a specified sequence of unicode characters,
515 the first one will be used for non-values,
516 the last one for the maximum,
517 the second (if any) for the minimum,
518 and any remaining will be distributed over the range of values.
519 Unspecified, block fill glyphs U+2581-2588 will be used.
523 Total statistics after all data.
525 =item -u, --unmodified
527 Do not reformat values, keeping leading whitespace.
528 Keep original value alignment, which may be significant in some programs.
530 =item --value-length=<size>
532 Reserved space for numbers.
534 =item -w, --width=<columns>
536 Override the maximum number of columns to use.
537 Appended graphics will extend to fill up the entire screen.
541 Overview of available options.
558 seq 30 | awk '{print sin($1/10)}' | barcat
560 Compare file sizes (with human-readable numbers):
562 du -d0 -b * | barcat -H
564 Memory usage of user processes with long names truncated:
566 ps xo %mem,pid,cmd | barcat -l40
568 Monitor network latency from prefixed results:
570 ping google.com | barcat -f'time=\K' -t
572 Commonly used after counting, for example users on the current server:
574 users | tr ' ' '\n' | sort | uniq -c | barcat
576 Letter frequencies in text files:
578 cat /usr/share/games/fortunes/*.u8 |
579 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
580 sort | uniq -c | barcat
582 Number of HTTP requests per day:
584 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
586 Any kind of database query with counts, preserving returned alignment:
588 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
591 In PostgreSQL from within the client:
593 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
595 Earthquakes worldwide magnitude 1+ in the last 24 hours:
597 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
598 column -tns, | barcat -f4 -u -l80%
600 External datasets, like movies per year:
602 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
603 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
605 But please get I<jq> to process JSON
606 and replace the manual selection by C<< jq '.[].year' >>.
608 Pokémon height comparison:
610 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
611 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
613 USD/EUR exchange rate from CSV provided by the ECB:
615 curl https://sdw.ecb.europa.eu/export.do \
616 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
617 grep '^[12]' | barcat -f',\K' --value-length=7
619 Total population history in XML from the World Bank:
621 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
622 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
623 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
625 And of course various Git statistics, such commit count by year:
627 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
629 Or the top 3 most frequent authors with statistics over all:
631 git shortlog -sn | barcat -L3 -s
633 Sparkline graphics of simple input given as inline parameters:
635 barcat --spark= 3 1 4 1 5 0 9 2 4
637 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
639 ( git log --pretty=%ci --since=30day | cut -b-10
640 seq 0 30 | xargs -i date +%F -d-{}day ) |
641 sort | uniq -c | awk '$1--' | barcat --spark
645 Mischa POSLAWSKY <perl@shiar.org>