5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
11 use Getopt::Long '2.33', qw( :config gnu_getopt );
15 'C' => sub { $opt{color} = 0 },
19 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
20 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
24 'trim|length|l=s' => sub {
25 my ($optname, $optval) = @_;
26 $optval =~ s/%$// and $opt{trimpct}++;
27 $optval =~ m/\A-?[0-9]+\z/ or die(
28 "Value \"$optval\" invalid for option $optname",
29 " (number or percentage expected)\n"
39 my ($optname, $optval) = @_;
41 ($opt{hidemin}, $opt{hidemax}) =
42 $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/ or die(
43 "Value \"$optval\" invalid for option limit",
49 'graph-format=s' => sub {
50 $opt{'graph-format'} = substr $_[1], 0, 1;
53 $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
57 fire => [qw( 90 31 91 33 93 97 96 )],
58 fire88 => [map {"38;5;$_"} qw(
59 80 32 48 64 68 72 76 77 78 79 47
61 fire256=> [map {"38;5;$_"} qw(
63 202 208 214 220 226 227 228 229 230 231 159
65 ramp88 => [map {"38;5;$_"} qw(
66 64 65 66 67 51 35 39 23 22 26 25 28
68 whites => [qw( 1;30 0;37 1;37 )],
69 greys => [map {"38;5;$_"} 52, 235..255, 47],
70 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
77 say "barcat version $VERSION";
81 local $/ = undef; # slurp
82 my $pod = readline *DATA;
83 $pod =~ s/^=over\K/ 25/; # indent options list
84 $pod =~ s/^=item\ \N*\n\n\N*\n\K (?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/g;
85 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
86 $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
87 # abbreviate <variable> indicators
88 $pod =~ s/\Q>.../s>/g;
89 $pod =~ s/<(?:number|count|seconds)>/N/g;
90 $pod =~ s/<character(s?)>/\Uchar$1/g;
92 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
95 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
96 -indent => 2, -width => 78,
98 $parser->select('SYNOPSIS', 'OPTIONS');
99 $parser->output_string(\my $contents);
100 $parser->parse_string_document($pod);
102 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
103 $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
109 Pod::Usage::pod2usage(
110 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
113 ) or exit 64; # EX_USAGE
115 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
116 $opt{color} //= -t *STDOUT; # enable on tty
117 $opt{'graph-format'} //= '-';
118 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
119 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
120 $opt{anchor} //= qr/\A/;
121 $opt{'value-length'} = 6 if $opt{units};
122 $opt{'value-length'} = 1 if $opt{unmodified};
123 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
124 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
125 $opt{palette} //= $opt{color} && [31, 90, 32];
126 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
127 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
128 and undef $opt{interval};
130 my (@lines, @values, @order);
132 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
135 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
137 $SIG{INT} = \&show_exit;
139 if (defined $opt{interval}) {
140 $opt{interval} ||= 1;
141 alarm $opt{interval} if $opt{interval} > 0;
144 require Tie::Array::Sorted;
145 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
146 } or warn $@, "Expect slowdown with large datasets!\n";
150 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
152 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
154 s/\A\h*// unless $opt{unmodified};
155 my $valnum = s/$valmatch/\n/ && $1;
156 push @values, $valnum;
157 push @order, $valnum if length $valnum;
158 if (defined $opt{trim} and defined $valnum) {
159 my $trimpos = abs $opt{trim};
160 $trimpos -= length $valnum if $opt{unmodified};
162 $_ = substr $_, 0, 2;
164 elsif (length > $trimpos) {
165 # cut and replace (intentional lvalue for speed, contrary to PBP)
166 substr($_, $trimpos - 1) = '…';
170 show_lines() if defined $opt{interval} and $opt{interval} < 0
171 and $. % $opt{interval} == 0;
174 if ($opt{'zero-missing'}) {
175 push @values, (0) x 10;
178 $SIG{INT} = 'DEFAULT';
181 $opt{color} and defined $_[0] or return '';
182 return "\e[$_[0]m" if defined wantarray;
183 $_ = color(@_) . $_ . color(0) if defined;
187 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
188 my $float = $_[0] !~ /\A0*[-0-9]{1,3}\z/;
189 return sprintf('%3.*f%1s',
190 $float && ($unit % 3) == ($unit < 0), # tenths
191 $_[0] / 1000 ** int($unit/3), # number
192 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
198 state $nr = $opt{hidemin};
200 @lines > $nr or return;
202 @order = sort { $b <=> $a } @order unless tied @order;
203 my $maxval = $opt{maxval} // (
204 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
207 my $minval = $opt{minval} // min $order[-1] // (), 0;
208 my $range = $maxval - $minval;
209 my $lenval = $opt{'value-length'} // max map { length } @order;
210 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
211 max map { length $values[$_] && length $lines[$_] }
212 0 .. min $#lines, $opt{hidemax} || (); # left padding
214 ($opt{width} - $lenval - $len) / $range; # bar multiplication
217 if ($opt{markers} and $size > 0) {
218 for my $markspec (split /\h/, $opt{markers}) {
219 my ($char, $func) = split //, $markspec, 2;
221 if ($func eq 'avg') {
222 return sum(@order) / @order;
224 elsif ($func =~ /\A([0-9.]+)v\z/) {
225 my $index = $#order * $1 / 100;
226 return ($order[$index] + $order[$index + .5]) / 2;
233 color(36) for $barmark[$pos * $size] = $char;
236 state $lastmax = $maxval;
237 if ($maxval > $lastmax) {
238 print ' ' x ($lenval + $len);
241 ($lastmax - $minval) * $size + .5,
242 '-' x (($values[$nr - 1] - $minval) * $size);
244 say '+' x (($range - $lastmax) * $size + .5);
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};
298 if ($opt{hidemin} or $opt{hidemax}) {
299 printf '%s of ', sum(grep { length }
300 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
304 my $total = sum @order;
305 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
306 printf ' in %d values', scalar @order;
307 printf ' over %d lines', scalar @lines if @order != @lines;
308 printf(' (%s min, %s avg, %s max)',
309 color(31) . $order[-1] . color(0),
310 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
311 color(32) . $order[0] . color(0),
320 show_stat() if $opt{stat};
321 exit 130 if @_; # 0x80+signo
332 barcat - graph to visualize input values
336 B<barcat> [<options>] [<file>... | <numbers>]
340 Visualizes relative sizes of values read from input
341 (parameters, file(s) or STDIN).
342 Contents are concatenated similar to I<cat>,
343 but numbers are reformatted and a bar graph is appended to each line.
345 Don't worry, barcat does not drink and divide.
346 It can has various options for input and output (re)formatting,
347 but remains limited to one-dimensional charts.
348 For more complex graphing needs
349 you'll need a larger animal like I<gnuplot>.
355 =item -c, --[no-]color
357 Force colored output of values and bar markers.
358 Defaults on if output is a tty,
359 disabled otherwise such as when piped or redirected.
361 =item -f, --field=(<number> | <regexp>)
363 Compare values after a given number of whitespace separators,
364 or matching a regular expression.
366 Unspecified or I<-f0> means values are at the start of each line.
367 With I<-f1> the second word is taken instead.
368 A string can indicate the starting position of a value
369 (such as I<-f:> if preceded by colons),
370 or capture the numbers itself,
371 for example I<-f'(\d+)'> for the first digits anywhere.
375 Prepend a chart axis with minimum and maximum values labeled.
377 =item -H, --human-readable
379 Format values using SI unit prefixes,
380 turning long numbers like I<12356789> into I<12.4M>.
381 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
382 Short integers are aligned but kept without decimal point.
384 =item -t, --interval[=(<seconds> | -<lines>)]
386 Output partial progress every given number of seconds or input lines.
387 An update can also be forced by sending a I<SIGALRM> alarm signal.
389 =item -l, --length=[-]<size>[%]
391 Trim line contents (between number and bars)
392 to a maximum number of characters.
393 The exceeding part is replaced by an abbreviation sign,
394 unless C<--length=0>.
396 Prepend a dash (i.e. make negative) to enforce padding
397 regardless of encountered contents.
399 =item -L, --limit[=(<count> | <start>-[<end>])]
401 Stop output after a number of lines.
402 All input is still counted and analyzed for statistics,
403 but disregarded for padding and bar size.
405 =item --graph-format=<character>
407 Glyph to repeat for the graph line.
408 Defaults to a dash C<->.
410 =item -m, --markers=<format>
412 Statistical positions to indicate on bars.
413 A single indicator glyph precedes each position:
419 Exact value to match on the axis.
420 A vertical bar at the zero crossing is displayed by I<|0>
422 For example I<:3.14> would show a colon at pi.
424 =item <percentage>I<v>
426 Ranked value at the given percentile.
427 The default shows I<+> at I<50v> for the mean or median;
428 the middle value or average between middle values.
429 One standard deviation right of the mean is at about I<68.3v>.
430 The default includes I<< >31.73v <68.27v >>
431 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
436 the sum of all values divided by the number of counted lines.
437 Indicated by default as I<=>.
441 =item --min=<number>, --max=<number>
443 Bars extend from 0 or the minimum value if lower,
444 to the largest value encountered.
445 These options can be set to customize this range.
447 =item --palette=(<preset> | <color>...)
449 Override colors of parsed numbers.
450 Can be any CSI escape, such as I<90> for default dark grey,
451 or alternatively I<1;30> for bright black.
453 In case of additional colors,
454 the last is used for values equal to the maximum, the first for minima.
455 If unspecified, these are green and red respectively (I<31 90 32>).
456 Multiple intermediate colors will be distributed
457 relative to the size of values.
459 Predefined color schemes are named I<whites> and I<fire>,
460 or I<greys> and I<fire256> for 256-color variants.
462 =item --spark[=<characters>]
464 Replace lines by I<sparklines>,
465 single characters corresponding to input values.
466 A specified sequence of unicode characters will be used for
467 Of a specified sequence of unicode characters,
468 the first one will be used for non-values,
469 the last one for the maximum,
470 the second (if any) for the minimum,
471 and any remaining will be distributed over the range of values.
472 Unspecified, block fill glyphs U+2581-2588 will be used.
476 Total statistics after all data.
478 =item -u, --unmodified
480 Do not reformat values, keeping leading whitespace.
481 Keep original value alignment, which may be significant in some programs.
483 =item --value-length=<size>
485 Reserved space for numbers.
487 =item -w, --width=<columns>
489 Override the maximum number of columns to use.
490 Appended graphics will extend to fill up the entire screen.
494 Overview of available options.
511 seq 30 | awk '{print sin($1/10)}' | barcat
513 Compare file sizes (with human-readable numbers):
515 du -d0 -b * | barcat -H
517 Memory usage of user processes with long names truncated:
519 ps xo %mem,pid,cmd | barcat -l40
521 Monitor network latency from prefixed results:
523 ping google.com | barcat -f'time=\K' -t
525 Commonly used after counting, for example users on the current server:
527 users | tr ' ' '\n' | sort | uniq -c | barcat
529 Letter frequencies in text files:
531 cat /usr/share/games/fortunes/*.u8 |
532 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
533 sort | uniq -c | barcat
535 Number of HTTP requests per day:
537 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
539 Any kind of database query with counts, preserving returned alignment:
541 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
544 In PostgreSQL from within the client:
546 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
548 Earthquakes worldwide magnitude 1+ in the last 24 hours:
550 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
551 column -tns, | barcat -f4 -u -l80%
553 External datasets, like movies per year:
555 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
556 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
558 But please get I<jq> to process JSON
559 and replace the manual selection by C<< jq '.[].year' >>.
561 Pokémon height comparison:
563 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
564 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
566 USD/EUR exchange rate from CSV provided by the ECB:
568 curl https://sdw.ecb.europa.eu/export.do \
569 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
570 grep '^[12]' | barcat -f',\K' --value-length=7
572 Total population history in XML from the World Bank:
574 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
575 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
576 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
578 And of course various Git statistics, such commit count by year:
580 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
582 Or the top 3 most frequent authors with statistics over all:
584 git shortlog -sn | barcat -L3 -s
586 Sparkline graphics of simple input given as inline parameters:
588 barcat --spark= 3 1 4 1 5 0 9 2 4
590 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
592 ( git log --pretty=%ci --since=30day | cut -b-10
593 seq 0 30 | xargs -i date +%F -d-{}day ) |
594 sort | uniq -c | awk '$1--' | barcat --spark
598 Mischa POSLAWSKY <perl@shiar.org>