5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
10 use Getopt::Long '2.33', qw( :config gnu_getopt );
14 'C' => sub { $opt{color} = 0 },
18 $opt{anchor} = /^[0-9]+$/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
19 } or die $@ =~ s/(?: at .+)?$/ for option $_[0]/r;
23 'trim|length|l=s' => sub {
24 my ($optname, $optval) = @_;
25 $optval =~ s/%$// and $opt{trimpct}++;
26 $optval =~ m/^-?[0-9]+$/ or die(
27 "Value \"$optval\" invalid for option $optname",
28 " (number or percentage expected)\n"
38 my ($optname, $optval) = @_;
40 ($opt{hidemin}, $opt{hidemax}) =
41 $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/x or die(
42 "Value \"$optval\" invalid for option limit",
48 'graph-format=s' => sub {
49 $opt{'graph-format'} = substr $_[1], 0, 1;
52 $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
56 fire => [qw( 90 31 91 33 93 97 96 )],
57 fire88 => [map {"38;5;$_"} qw(
58 80 32 48 64 68 72 76 77 78 79 47
60 fire256=> [map {"38;5;$_"} qw(
62 202 208 214 220 226 227 228 229 230 231 159
64 ramp88 => [map {"38;5;$_"} qw(
65 64 65 66 67 51 35 39 23 22 26 25 28
67 whites => [qw( 1;30 0;37 1;37 )],
68 greys => [map {"38;5;$_"} 52, 235..255, 47],
69 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
76 say "barcat version $VERSION";
81 my $pod = readline *DATA;
82 $pod =~ s/^=over\K/ 22/m; # indent options list
83 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
86 my $parser = Pod::Usage->new;
87 $parser->select('SYNOPSIS', 'OPTIONS');
88 $parser->output_string(\my $contents);
89 $parser->parse_string_document($pod);
91 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
97 Pod::Usage::pod2usage(
98 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
101 ) or exit 64; # EX_USAGE
103 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
104 $opt{color} //= -t *STDOUT; # enable on tty
105 $opt{'graph-format'} //= '-';
106 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
107 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
108 $opt{anchor} //= qr/\A/;
109 $opt{'value-length'} = 6 if $opt{units};
110 $opt{'value-length'} = 1 if $opt{unmodified};
111 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
112 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
113 $opt{palette} //= $opt{color} && [31, 90, 32];
114 $opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
115 and undef $opt{interval};
117 my (@lines, @values, @order);
119 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
122 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
124 $SIG{INT} = \&show_exit;
126 if (defined $opt{interval}) {
127 $opt{interval} ||= 1;
128 alarm $opt{interval} if $opt{interval} > 0;
131 require Tie::Array::Sorted;
132 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
133 } or warn $@, "Expect slowdown with large datasets!\n";
137 $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
139 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
141 s/^\h*// unless $opt{unmodified};
142 push @values, s/$valmatch/\n/ && $1;
143 push @order, $1 if length $1;
144 if (defined $opt{trim} and defined $1) {
145 my $trimpos = abs $opt{trim};
146 $trimpos -= length $1 if $opt{unmodified};
148 $_ = substr $_, 0, 2;
150 elsif (length > $trimpos) {
151 substr($_, $trimpos - 1) = '…';
155 show_lines() if defined $opt{interval} and $opt{interval} < 0
156 and $. % $opt{interval} == 0;
159 if ($opt{'zero-missing'}) {
160 push @values, (0) x 10;
163 $SIG{INT} = 'DEFAULT';
166 $opt{color} and defined $_[0] or return '';
167 return "\e[$_[0]m" if defined wantarray;
168 $_ = color(@_) . $_ . color(0) if defined;
173 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
174 @lines and @lines > $nr or return;
176 @lines > $nr or return unless $opt{hidemin};
178 @order = sort { $b <=> $a } @order unless tied @order;
179 my $maxval = $opt{maxval} // (
180 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
183 my $minval = $opt{minval} // min $order[-1] // (), 0;
184 my $lenval = $opt{'value-length'} // max map { length } @order;
185 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
186 max map { length $values[$_] && length $lines[$_] }
187 0 .. min $#lines, $opt{hidemax} || (); # left padding
188 my $size = ($maxval - $minval) &&
189 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
192 if ($opt{markers} and $size > 0) {
193 for my $markspec (split /\h/, $opt{markers}) {
194 my ($char, $func) = split //, $markspec, 2;
196 if ($func eq 'avg') {
197 return sum(@order) / @order;
199 elsif ($func =~ /\A([0-9.]+)v\z/) {
200 my $index = $#order * $1 / 100;
201 return ($order[$index] + $order[$index + .5]) / 2;
208 color(36) for $barmark[$pos * $size] = $char;
211 state $lastmax = $maxval;
212 if ($maxval > $lastmax) {
213 print ' ' x ($lenval + $len);
216 ($lastmax - $minval) * $size + .5,
217 '-' x (($values[$nr - 1] - $minval) * $size);
219 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
225 @lines > $nr or return if $opt{hidemin};
228 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
229 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
231 $float && ($unit % 3) == ($unit < 0), # tenths
232 $_[0] / 1000 ** int($unit/3), # number
233 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
238 color(31), sprintf('%*s', $lenval, $minval),
239 color(90), '-', color(36), '+',
240 color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
241 color(90), '-', color(36), '+',
245 while ($nr <= $#lines) {
246 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
247 my $val = $values[$nr];
248 my $rel = length $val && ($val - $minval) / ($maxval - $minval);
249 my $color = !length $val || !$opt{palette} ? undef :
250 $val == $order[0] ? $opt{palette}->[-1] : # max
251 $val == $order[-1] ? $opt{palette}->[0] : # min
252 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
255 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
256 print color($color), $opt{spark}->[
258 $val == $order[0] ? -1 : # max
259 $val == $order[-1] ? 1 : # min
260 $#{$opt{spark}} < 3 ? 1 :
261 $rel * ($#{$opt{spark}} - 3) + 2.5
267 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
268 color($color) for $val;
270 my $line = $lines[$nr] =~ s/\n/$val/r;
271 printf '%-*s', $len + length($val), $line;
272 print $barmark[$_] // $opt{'graph-format'}
273 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
279 say $opt{palette} ? color(0) : '' if $opt{spark};
284 if ($opt{hidemin} or $opt{hidemax}) {
286 $opt{hidemax} ||= @lines;
287 printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
290 my $total = sum @order;
291 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
292 printf ' in %d values', scalar @order;
293 printf ' over %d lines', scalar @lines if @order != @lines;
294 printf(' (%s min, %s avg, %s max)',
295 color(31) . $order[-1] . color(0),
296 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
297 color(32) . $order[0] . color(0),
305 show_stat() if $opt{stat};
306 exit 130 if @_; # 0x80+signo
317 barcat - graph to visualize input values
321 B<barcat> [<options>] [<file>... | <numbers>]
325 Visualizes relative sizes of values read from input
326 (parameters, file(s) or STDIN).
327 Contents are concatenated similar to I<cat>,
328 but numbers are reformatted and a bar graph is appended to each line.
330 Don't worry, barcat does not drink and divide.
331 It can has various options for input and output (re)formatting,
332 but remains limited to one-dimensional charts.
333 For more complex graphing needs
334 you'll need a larger animal like I<gnuplot>.
340 =item -c, --[no-]color
342 Force colored output of values and bar markers.
343 Defaults on if output is a tty,
344 disabled otherwise such as when piped or redirected.
346 =item -f, --field=(<number>|<regexp>)
348 Compare values after a given number of whitespace separators,
349 or matching a regular expression.
351 Unspecified or I<-f0> means values are at the start of each line.
352 With I<-f1> the second word is taken instead.
353 A string can indicate the starting position of a value
354 (such as I<-f:> if preceded by colons),
355 or capture the numbers itself,
356 for example I<-f'(\d+)'> for the first digits anywhere.
360 Prepend a chart axis with minimum and maximum values labeled.
362 =item -H, --human-readable
364 Format values using SI unit prefixes,
365 turning long numbers like I<12356789> into I<12.4M>.
366 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
367 Short integers are aligned but kept without decimal point.
369 =item -t, --interval[=(<seconds>|-<lines>)]
371 Output partial progress every given number of seconds or input lines.
372 An update can also be forced by sending a I<SIGALRM> alarm signal.
374 =item -l, --length=[-]<size>[%]
376 Trim line contents (between number and bars)
377 to a maximum number of characters.
378 The exceeding part is replaced by an abbreviation sign,
379 unless C<--length=0>.
381 Prepend a dash (i.e. make negative) to enforce padding
382 regardless of encountered contents.
384 =item -L, --limit[=(<count> | <start>-[<end>])]
386 Stop output after a number of lines.
387 All input is still counted and analyzed for statistics,
388 but disregarded for padding and bar size.
390 =item --graph-format=<character>
392 Glyph to repeat for the graph line.
393 Defaults to a dash C<->.
395 =item -m, --markers=<format>
397 Statistical positions to indicate on bars.
398 A single indicator glyph precedes each position:
404 Exact value to match on the axis.
405 A vertical bar at the zero crossing is displayed by I<|0>
407 For example I<:3.14> would show a colon at pi.
409 =item <percentage>I<v>
411 Ranked value at the given percentile.
412 The default shows I<+> at I<50v> for the mean or median;
413 the middle value or average between middle values.
414 One standard deviation right of the mean is at about I<68.3v>.
415 The default includes I<< >31.73v <68.27v >>
416 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
421 the sum of all values divided by the number of counted lines.
422 Indicated by default as I<=>.
426 =item --min=<number>, --max=<number>
428 Bars extend from 0 or the minimum value if lower,
429 to the largest value encountered.
430 These options can be set to customize this range.
432 =item --palette=(<preset> | <color>...)
434 Override colors of parsed numbers.
435 Can be any CSI escape, such as I<90> for default dark grey,
436 or alternatively I<1;30> for bold black.
438 In case of additional colors,
439 the last is used for values equal to the maximum, the first for minima.
440 If unspecified, these are green and red respectively (I<31 90 32>).
442 =item --spark[=<glyphs>]
444 Replace lines by I<sparklines>,
445 single characters corresponding to input values.
446 A specified sequence of unicode characters will be used for
447 Of a specified sequence of unicode characters,
448 the first one will be used for non-values,
449 the last one for the maximum,
450 the second (if any) for the minimum,
451 and any remaining will be distributed over the range of values.
452 Unspecified, block fill glyphs U+2581-2588 will be used.
456 Total statistics after all data.
458 =item -u, --unmodified
460 Do not reformat values, keeping leading whitespace.
461 Keep original value alignment, which may be significant in some programs.
463 =item --value-length=<size>
465 Reserved space for numbers.
467 =item -w, --width=<columns>
469 Override the maximum number of columns to use.
470 Appended graphics will extend to fill up the entire screen.
474 Overview of available options.
491 seq 30 | awk '{print sin($1/10)}' | barcat
493 Compare file sizes (with human-readable numbers):
495 du -d0 -b * | barcat -H
497 Memory usage of user processes with long names truncated:
499 ps xo %mem,pid,cmd | barcat -l40
501 Monitor network latency from prefixed results:
503 ping google.com | barcat -f'time=\K' -t
505 Commonly used after counting, for example users on the current server:
507 users | tr ' ' '\n' | sort | uniq -c | barcat
509 Letter frequencies in text files:
511 cat /usr/share/games/fortunes/*.u8 |
512 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
513 sort | uniq -c | barcat
515 Number of HTTP requests per day:
517 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
519 Any kind of database query with counts, preserving returned alignment:
521 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
524 Earthquakes worldwide magnitude 1+ in the last 24 hours:
526 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
527 column -tns, | graph -f4 -u -l80%
529 External datasets, like movies per year:
531 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
532 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
534 But please get I<jq> to process JSON
535 and replace the manual selection by C<< jq '.[].year' >>.
537 Pokémon height comparison:
539 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
540 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
542 USD/EUR exchange rate from CSV provided by the ECB:
544 curl https://sdw.ecb.europa.eu/export.do \
545 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
546 grep '^[12]' | barcat -f',\K' --value-length=7
548 Total population history in XML from the World Bank:
550 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
551 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
552 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
554 And of course various Git statistics, such commit count by year:
556 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
558 Or the top 3 most frequent authors with statistics over all:
560 git shortlog -sn | barcat -L3 -s
562 Sparkline graphics of simple input given as inline parameters:
564 barcat --spark= 3 1 4 1 5 0 9 2 4
566 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
568 ( git log --pretty=%ci --since=30day | cut -b-10
569 seq 0 30 | xargs -i date +%F -d-{}day ) |
570 sort | uniq -c | awk '$1--' | graph --spark
574 Mischa POSLAWSKY <perl@shiar.org>