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/ 25/m; # indent options list
83 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
84 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
85 $pod =~ s/^=item \K(?=--)/____/gm; # align long options
86 # abbreviate <variable> indicators
87 $pod =~ s/\Q>.../s>/g;
88 $pod =~ s/<(?:number|count|seconds)>/N/g;
89 $pod =~ s/<character(s?)>/\Uchar$1/g;
91 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
94 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
95 -indent => 2, -width => 78,
97 $parser->select('SYNOPSIS', 'OPTIONS');
98 $parser->output_string(\my $contents);
99 $parser->parse_string_document($pod);
101 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
102 $contents =~ s/^ \K____/ /gm; # nbsp substitute
108 Pod::Usage::pod2usage(
109 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
112 ) or exit 64; # EX_USAGE
114 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
115 $opt{color} //= -t *STDOUT; # enable on tty
116 $opt{'graph-format'} //= '-';
117 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
118 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
119 $opt{anchor} //= qr/\A/;
120 $opt{'value-length'} = 6 if $opt{units};
121 $opt{'value-length'} = 1 if $opt{unmodified};
122 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
123 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
124 $opt{palette} //= $opt{color} && [31, 90, 32];
125 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
126 $opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
127 and undef $opt{interval};
129 my (@lines, @values, @order);
131 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
134 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
136 $SIG{INT} = \&show_exit;
138 if (defined $opt{interval}) {
139 $opt{interval} ||= 1;
140 alarm $opt{interval} if $opt{interval} > 0;
143 require Tie::Array::Sorted;
144 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
145 } or warn $@, "Expect slowdown with large datasets!\n";
149 $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
151 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
153 s/^\h*// unless $opt{unmodified};
154 push @values, s/$valmatch/\n/ && $1;
155 push @order, $1 if length $1;
156 if (defined $opt{trim} and defined $1) {
157 my $trimpos = abs $opt{trim};
158 $trimpos -= length $1 if $opt{unmodified};
160 $_ = substr $_, 0, 2;
162 elsif (length > $trimpos) {
163 substr($_, $trimpos - 1) = '…';
167 show_lines() if defined $opt{interval} and $opt{interval} < 0
168 and $. % $opt{interval} == 0;
171 if ($opt{'zero-missing'}) {
172 push @values, (0) x 10;
175 $SIG{INT} = 'DEFAULT';
178 $opt{color} and defined $_[0] or return '';
179 return "\e[$_[0]m" if defined wantarray;
180 $_ = color(@_) . $_ . color(0) if defined;
184 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
185 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
187 $float && ($unit % 3) == ($unit < 0), # tenths
188 $_[0] / 1000 ** int($unit/3), # number
189 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
195 state $nr = $opt{hidemin};
196 @lines and @lines > $nr or return;
198 @lines > $nr or return unless $opt{hidemin};
200 @order = sort { $b <=> $a } @order unless tied @order;
201 my $maxval = $opt{maxval} // (
202 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
205 my $minval = $opt{minval} // min $order[-1] // (), 0;
206 my $range = $maxval - $minval;
207 my $lenval = $opt{'value-length'} // max map { length } @order;
208 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
209 max map { length $values[$_] && length $lines[$_] }
210 0 .. min $#lines, $opt{hidemax} || (); # left padding
212 ($opt{width} - $lenval - $len) / $range; # bar multiplication
215 if ($opt{markers} and $size > 0) {
216 for my $markspec (split /\h/, $opt{markers}) {
217 my ($char, $func) = split //, $markspec, 2;
219 if ($func eq 'avg') {
220 return sum(@order) / @order;
222 elsif ($func =~ /\A([0-9.]+)v\z/) {
223 my $index = $#order * $1 / 100;
224 return ($order[$index] + $order[$index + .5]) / 2;
231 color(36) for $barmark[$pos * $size] = $char;
234 state $lastmax = $maxval;
235 if ($maxval > $lastmax) {
236 print ' ' x ($lenval + $len);
239 ($lastmax - $minval) * $size + .5,
240 '-' x (($values[$nr - 1] - $minval) * $size);
242 say '+' x (($range - $lastmax) * $size + .5);
248 @lines > $nr or return if $opt{hidemin};
251 color(31), sprintf('%*s', $lenval, $minval),
252 color(90), '-', color(36), '+',
253 color(32), sprintf('%*s', $size * $range - 3, $maxval),
254 color(90), '-', color(36), '+',
258 while ($nr <= $#lines) {
259 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
260 my $val = $values[$nr];
261 my $rel = length $val && $range && ($val - $minval) / $range;
262 my $color = !length $val || !$opt{palette} ? undef :
263 $val == $order[0] ? $opt{palette}->[-1] : # max
264 $val == $order[-1] ? $opt{palette}->[0] : # min
265 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
268 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
269 print color($color), $opt{spark}->[
271 $val == $order[0] ? -1 : # max
272 $val == $order[-1] ? 1 : # min
273 $#{$opt{spark}} < 3 ? 1 :
274 $rel * ($#{$opt{spark}} - 3) + 2.5
280 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
281 color($color) for $val;
283 my $line = $lines[$nr] =~ s/\n/$val/r;
284 printf '%-*s', $len + length($val), $line;
285 print $barmark[$_] // $opt{'graph-format'}
286 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
292 say $opt{palette} ? color(0) : '' if $opt{spark};
297 if ($opt{hidemin} or $opt{hidemax}) {
298 printf '%s of ', sum(grep { length }
299 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
303 my $total = sum @order;
304 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
305 printf ' in %d values', scalar @order;
306 printf ' over %d lines', scalar @lines if @order != @lines;
307 printf(' (%s min, %s avg, %s max)',
308 color(31) . $order[-1] . color(0),
309 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
310 color(32) . $order[0] . color(0),
318 show_stat() if $opt{stat};
319 exit 130 if @_; # 0x80+signo
330 barcat - graph to visualize input values
334 B<barcat> [<options>] [<file>... | <numbers>]
338 Visualizes relative sizes of values read from input
339 (parameters, file(s) or STDIN).
340 Contents are concatenated similar to I<cat>,
341 but numbers are reformatted and a bar graph is appended to each line.
343 Don't worry, barcat does not drink and divide.
344 It can has various options for input and output (re)formatting,
345 but remains limited to one-dimensional charts.
346 For more complex graphing needs
347 you'll need a larger animal like I<gnuplot>.
353 =item -c, --[no-]color
355 Force colored output of values and bar markers.
356 Defaults on if output is a tty,
357 disabled otherwise such as when piped or redirected.
359 =item -f, --field=(<number> | <regexp>)
361 Compare values after a given number of whitespace separators,
362 or matching a regular expression.
364 Unspecified or I<-f0> means values are at the start of each line.
365 With I<-f1> the second word is taken instead.
366 A string can indicate the starting position of a value
367 (such as I<-f:> if preceded by colons),
368 or capture the numbers itself,
369 for example I<-f'(\d+)'> for the first digits anywhere.
373 Prepend a chart axis with minimum and maximum values labeled.
375 =item -H, --human-readable
377 Format values using SI unit prefixes,
378 turning long numbers like I<12356789> into I<12.4M>.
379 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
380 Short integers are aligned but kept without decimal point.
382 =item -t, --interval[=(<seconds> | -<lines>)]
384 Output partial progress every given number of seconds or input lines.
385 An update can also be forced by sending a I<SIGALRM> alarm signal.
387 =item -l, --length=[-]<size>[%]
389 Trim line contents (between number and bars)
390 to a maximum number of characters.
391 The exceeding part is replaced by an abbreviation sign,
392 unless C<--length=0>.
394 Prepend a dash (i.e. make negative) to enforce padding
395 regardless of encountered contents.
397 =item -L, --limit[=(<count> | <start>-[<end>])]
399 Stop output after a number of lines.
400 All input is still counted and analyzed for statistics,
401 but disregarded for padding and bar size.
403 =item --graph-format=<character>
405 Glyph to repeat for the graph line.
406 Defaults to a dash C<->.
408 =item -m, --markers=<format>
410 Statistical positions to indicate on bars.
411 A single indicator glyph precedes each position:
417 Exact value to match on the axis.
418 A vertical bar at the zero crossing is displayed by I<|0>
420 For example I<:3.14> would show a colon at pi.
422 =item <percentage>I<v>
424 Ranked value at the given percentile.
425 The default shows I<+> at I<50v> for the mean or median;
426 the middle value or average between middle values.
427 One standard deviation right of the mean is at about I<68.3v>.
428 The default includes I<< >31.73v <68.27v >>
429 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
434 the sum of all values divided by the number of counted lines.
435 Indicated by default as I<=>.
439 =item --min=<number>, --max=<number>
441 Bars extend from 0 or the minimum value if lower,
442 to the largest value encountered.
443 These options can be set to customize this range.
445 =item --palette=(<preset> | <color>...)
447 Override colors of parsed numbers.
448 Can be any CSI escape, such as I<90> for default dark grey,
449 or alternatively I<1;30> for bright black.
451 In case of additional colors,
452 the last is used for values equal to the maximum, the first for minima.
453 If unspecified, these are green and red respectively (I<31 90 32>).
455 =item --spark[=<characters>]
457 Replace lines by I<sparklines>,
458 single characters corresponding to input values.
459 A specified sequence of unicode characters will be used for
460 Of a specified sequence of unicode characters,
461 the first one will be used for non-values,
462 the last one for the maximum,
463 the second (if any) for the minimum,
464 and any remaining will be distributed over the range of values.
465 Unspecified, block fill glyphs U+2581-2588 will be used.
469 Total statistics after all data.
471 =item -u, --unmodified
473 Do not reformat values, keeping leading whitespace.
474 Keep original value alignment, which may be significant in some programs.
476 =item --value-length=<size>
478 Reserved space for numbers.
480 =item -w, --width=<columns>
482 Override the maximum number of columns to use.
483 Appended graphics will extend to fill up the entire screen.
487 Overview of available options.
504 seq 30 | awk '{print sin($1/10)}' | barcat
506 Compare file sizes (with human-readable numbers):
508 du -d0 -b * | barcat -H
510 Memory usage of user processes with long names truncated:
512 ps xo %mem,pid,cmd | barcat -l40
514 Monitor network latency from prefixed results:
516 ping google.com | barcat -f'time=\K' -t
518 Commonly used after counting, for example users on the current server:
520 users | tr ' ' '\n' | sort | uniq -c | barcat
522 Letter frequencies in text files:
524 cat /usr/share/games/fortunes/*.u8 |
525 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
526 sort | uniq -c | barcat
528 Number of HTTP requests per day:
530 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
532 Any kind of database query with counts, preserving returned alignment:
534 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
537 Earthquakes worldwide magnitude 1+ in the last 24 hours:
539 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
540 column -tns, | graph -f4 -u -l80%
542 External datasets, like movies per year:
544 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
545 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
547 But please get I<jq> to process JSON
548 and replace the manual selection by C<< jq '.[].year' >>.
550 Pokémon height comparison:
552 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
553 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
555 USD/EUR exchange rate from CSV provided by the ECB:
557 curl https://sdw.ecb.europa.eu/export.do \
558 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
559 grep '^[12]' | barcat -f',\K' --value-length=7
561 Total population history in XML from the World Bank:
563 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
564 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
565 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
567 And of course various Git statistics, such commit count by year:
569 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
571 Or the top 3 most frequent authors with statistics over all:
573 git shortlog -sn | barcat -L3 -s
575 Sparkline graphics of simple input given as inline parameters:
577 barcat --spark= 3 1 4 1 5 0 9 2 4
579 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
581 ( git log --pretty=%ci --since=30day | cut -b-10
582 seq 0 30 | xargs -i date +%F -d-{}day ) |
583 sort | uniq -c | awk '$1--' | graph --spark
587 Mischa POSLAWSKY <perl@shiar.org>