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;
116 my (@lines, @values, @order);
118 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
121 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
123 $SIG{INT} = \&show_exit;
125 if (defined $opt{interval}) {
126 $opt{interval} ||= 1;
127 alarm $opt{interval} if $opt{interval} > 0;
130 require Tie::Array::Sorted;
131 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
132 } or warn $@, "Expect slowdown with large datasets!\n";
135 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
136 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
138 s/^\h*// unless $opt{unmodified};
139 push @values, s/$valmatch/\n/ && $1;
140 push @order, $1 if length $1;
141 if (defined $opt{trim} and defined $1) {
142 my $trimpos = abs $opt{trim};
143 $trimpos -= length $1 if $opt{unmodified};
145 $_ = substr $_, 0, 2;
147 elsif (length > $trimpos) {
148 substr($_, $trimpos - 1) = '…';
152 show_lines() if defined $opt{interval} and $opt{interval} < 0
153 and $. % $opt{interval} == 0;
156 if ($opt{'zero-missing'}) {
157 push @values, (0) x 10;
160 $SIG{INT} = 'DEFAULT';
163 $opt{color} and defined $_[0] or return '';
164 return "\e[$_[0]m" if defined wantarray;
165 $_ = color(@_) . $_ . color(0) if defined;
170 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
171 @lines and @lines > $nr or return;
173 @lines > $nr or return unless $opt{hidemin};
175 @order = sort { $b <=> $a } @order unless tied @order;
176 my $maxval = $opt{maxval} // ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
177 my $minval = $opt{minval} // min $order[-1] // (), 0;
178 my $lenval = $opt{'value-length'} // max map { length } @order;
179 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
180 max map { length $values[$_] && length $lines[$_] }
181 0 .. min $#lines, $opt{hidemax} || (); # left padding
182 my $size = ($maxval - $minval) &&
183 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
186 if ($opt{markers} and $size > 0) {
187 for my $markspec (split /\h/, $opt{markers}) {
188 my ($char, $func) = split //, $markspec, 2;
190 if ($func eq 'avg') {
191 return sum(@order) / @order;
193 elsif ($func =~ /\A([0-9.]+)v\z/) {
194 my $index = $#order * $1 / 100;
195 return ($order[$index] + $order[$index + .5]) / 2;
202 color(36) for $barmark[$pos * $size] = $char;
205 state $lastmax = $maxval;
206 if ($maxval > $lastmax) {
207 print ' ' x ($lenval + $len);
210 ($lastmax - $minval) * $size + .5,
211 '-' x (($values[$nr - 1] - $minval) * $size);
213 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
219 @lines > $nr or return if $opt{hidemin};
222 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
223 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
225 $float && ($unit % 3) == ($unit < 0), # tenths
226 $_[0] / 1000 ** int($unit/3), # number
227 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
232 color(31), sprintf('%*s', $lenval, $minval),
233 color(90), '-', color(36), '+',
234 color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
235 color(90), '-', color(36), '+',
239 while ($nr <= $#lines) {
240 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
241 my $val = $values[$nr];
242 my $rel = length $val && ($val - $minval) / ($maxval - $minval);
243 my $color = !length $val || !$opt{palette} ? undef :
244 $val == $order[0] ? $opt{palette}->[-1] : # max
245 $val == $order[-1] ? $opt{palette}->[0] : # min
246 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
249 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
250 print color($color), $opt{spark}->[
252 $val == $order[0] ? -1 : # max
253 $val == $order[-1] ? 1 : # min
254 $#{$opt{spark}} < 3 ? 1 :
255 $rel * ($#{$opt{spark}} - 3) + 2.5
261 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
262 color($color) for $val;
264 my $line = $lines[$nr] =~ s/\n/$val/r;
265 printf '%-*s', $len + length($val), $line;
266 print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
272 say $opt{palette} ? color(0) : '' if $opt{spark};
277 if ($opt{hidemin} or $opt{hidemax}) {
279 $opt{hidemax} ||= @lines;
280 printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
283 my $total = sum @order;
284 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
285 printf ' in %d values', scalar @order;
286 printf ' over %d lines', scalar @lines if @order != @lines;
287 printf(' (%s min, %s avg, %s max)',
288 color(31) . $order[-1] . color(0),
289 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
290 color(32) . $order[0] . color(0),
298 show_stat() if $opt{stat};
299 exit 130 if @_; # 0x80+signo
310 barcat - graph to visualize input values
314 B<barcat> [<options>] [<file>... | <numbers>]
318 Visualizes relative sizes of values read from input
319 (parameters, file(s) or STDIN).
320 Contents are concatenated similar to I<cat>,
321 but numbers are reformatted and a bar graph is appended to each line.
323 Don't worry, barcat does not drink and divide.
324 It can has various options for input and output (re)formatting,
325 but remains limited to one-dimensional charts.
326 For more complex graphing needs
327 you'll need a larger animal like I<gnuplot>.
333 =item -c, --[no-]color
335 Force colored output of values and bar markers.
336 Defaults on if output is a tty,
337 disabled otherwise such as when piped or redirected.
339 =item -f, --field=(<number>|<regexp>)
341 Compare values after a given number of whitespace separators,
342 or matching a regular expression.
344 Unspecified or I<-f0> means values are at the start of each line.
345 With I<-f1> the second word is taken instead.
346 A string can indicate the starting position of a value
347 (such as I<-f:> if preceded by colons),
348 or capture the numbers itself,
349 for example I<-f'(\d+)'> for the first digits anywhere.
353 Prepend a chart axis with minimum and maximum values labeled.
355 =item -H, --human-readable
357 Format values using SI unit prefixes,
358 turning long numbers like I<12356789> into I<12.4M>.
359 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
360 Short integers are aligned but kept without decimal point.
362 =item -t, --interval[=(<seconds>|-<lines>)]
364 Output partial progress every given number of seconds or input lines.
365 An update can also be forced by sending a I<SIGALRM> alarm signal.
367 =item -l, --length=[-]<size>[%]
369 Trim line contents (between number and bars)
370 to a maximum number of characters.
371 The exceeding part is replaced by an abbreviation sign,
372 unless C<--length=0>.
374 Prepend a dash (i.e. make negative) to enforce padding
375 regardless of encountered contents.
377 =item -L, --limit[=(<count> | <start>-[<end>])]
379 Stop output after a number of lines.
380 All input is still counted and analyzed for statistics,
381 but disregarded for padding and bar size.
383 =item --graph-format=<character>
385 Glyph to repeat for the graph line.
386 Defaults to a dash C<->.
388 =item -m, --markers=<format>
390 Statistical positions to indicate on bars.
391 A single indicator glyph precedes each position:
397 Exact value to match on the axis.
398 A vertical bar at the zero crossing is displayed by I<|0>
400 For example I<:3.14> would show a colon at pi.
402 =item <percentage>I<v>
404 Ranked value at the given percentile.
405 The default shows I<+> at I<50v> for the mean or median;
406 the middle value or average between middle values.
407 One standard deviation right of the mean is at about I<68.3v>.
408 The default includes I<< >31.73v <68.27v >>
409 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
414 the sum of all values divided by the number of counted lines.
415 Indicated by default as I<=>.
419 =item --min=<number>, --max=<number>
421 Bars extend from 0 or the minimum value if lower,
422 to the largest value encountered.
423 These options can be set to customize this range.
425 =item --palette=(<preset> | <color>...)
427 Override colors of parsed numbers.
428 Can be any CSI escape, such as I<90> for default dark grey,
429 or alternatively I<1;30> for bold black.
431 In case of additional colors,
432 the last is used for values equal to the maximum, the first for minima.
433 If unspecified, these are green and red respectively (I<31 90 32>).
435 =item --spark[=<glyphs>]
437 Replace lines by I<sparklines>,
438 single characters corresponding to input values.
439 A specified sequence of unicode characters will be used for
440 Of a specified sequence of unicode characters,
441 the first one will be used for non-values,
442 the last one for the maximum,
443 the second (if any) for the minimum,
444 and any remaining will be distributed over the range of values.
445 Unspecified, block fill glyphs U+2581-2588 will be used.
449 Total statistics after all data.
451 =item -u, --unmodified
453 Do not reformat values, keeping leading whitespace.
454 Keep original value alignment, which may be significant in some programs.
456 =item --value-length=<size>
458 Reserved space for numbers.
460 =item -w, --width=<columns>
462 Override the maximum number of columns to use.
463 Appended graphics will extend to fill up the entire screen.
467 Overview of available options.
484 seq 30 | awk '{print sin($1/10)}' | barcat
486 Compare file sizes (with human-readable numbers):
488 du -d0 -b * | barcat -H
490 Memory usage of user processes with long names truncated:
492 ps xo %mem,pid,cmd | barcat -l40
494 Monitor network latency from prefixed results:
496 ping google.com | barcat -f'time=\K' -t
498 Commonly used after counting, for example users on the current server:
500 users | tr ' ' '\n' | sort | uniq -c | barcat
502 Letter frequencies in text files:
504 cat /usr/share/games/fortunes/*.u8 |
505 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
506 sort | uniq -c | barcat
508 Number of HTTP requests per day:
510 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
512 Any kind of database query with counts, preserving returned alignment:
514 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
517 Earthquakes worldwide magnitude 1+ in the last 24 hours:
519 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
520 column -tns, | graph -f4 -u -l80%
522 External datasets, like movies per year:
524 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
525 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
527 But please get I<jq> to process JSON
528 and replace the manual selection by C<< jq '.[].year' >>.
530 Pokémon height comparison:
532 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
533 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
535 USD/EUR exchange rate from CSV provided by the ECB:
537 curl https://sdw.ecb.europa.eu/export.do \
538 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
539 grep '^[12]' | barcat -f',\K' --value-length=7
541 Total population history in XML from the World Bank:
543 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
544 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
545 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
547 And of course various Git statistics, such commit count by year:
549 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
551 Or the top 3 most frequent authors with statistics over all:
553 git shortlog -sn | barcat -L3 -s
555 Activity of the last days (substitute date C<-v-{}d> on BSD):
557 ( git log --pretty=%ci --since=30day | cut -b-10
558 seq 0 30 | xargs -i date +%F -d-{}day ) |
559 sort | uniq -c | awk '$1--' | graph --spark
563 Mischa POSLAWSKY <perl@shiar.org>