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
85 ^=item \h \N*\n\n \N*\n \K # first line
86 (?: (?: ^=over .*? ^=back\n )? (?!=) \N*\n )*
87 }{\n}g; # abbreviate options
88 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
89 $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
90 # abbreviate <variable> indicators
91 $pod =~ s/\Q>.../s>/g;
92 $pod =~ s/<(?:number|count|seconds)>/N/g;
93 $pod =~ s/<character(s?)>/\Uchar$1/g;
95 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
98 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
99 -indent => 2, -width => 78,
101 $parser->select('SYNOPSIS', 'OPTIONS');
102 $parser->output_string(\my $contents);
103 $parser->parse_string_document($pod);
105 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
106 $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
112 Pod::Usage::pod2usage(
113 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
116 ) or exit 64; # EX_USAGE
118 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
119 $opt{color} //= -t *STDOUT; # enable on tty
120 $opt{'graph-format'} //= '-';
121 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
122 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
123 $opt{anchor} //= qr/\A/;
124 $opt{'value-length'} = 6 if $opt{units};
125 $opt{'value-length'} = 1 if $opt{unmodified};
126 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
127 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
128 $opt{palette} //= $opt{color} && [31, 90, 32];
129 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
130 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
131 and undef $opt{interval};
133 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
134 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
135 $opt{'value-format'} = $opt{units} && sub {
136 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
137 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
139 $float && ($unit % 3) == ($unit < 0), # tenths
140 $_[0] / 1000 ** int($unit/3), # number
141 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
146 my (@lines, @values, @order);
148 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
151 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
153 $SIG{INT} = \&show_exit;
155 if (defined $opt{interval}) {
156 $opt{interval} ||= 1;
157 alarm $opt{interval} if $opt{interval} > 0;
160 require Tie::Array::Sorted;
161 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
162 } or warn $@, "Expect slowdown with large datasets!\n";
166 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
168 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
170 s/\A\h*// unless $opt{unmodified};
171 my $valnum = s/$valmatch/\n/ && $1;
172 push @values, $valnum;
173 push @order, $valnum if length $valnum;
174 if (defined $opt{trim} and defined $valnum) {
175 my $trimpos = abs $opt{trim};
176 $trimpos -= length $valnum if $opt{unmodified};
178 $_ = substr $_, 0, 2;
180 elsif (length > $trimpos) {
181 # cut and replace (intentional lvalue for speed, contrary to PBP)
182 substr($_, $trimpos - 1) = '…';
186 show_lines() if defined $opt{interval} and $opt{interval} < 0
187 and $. % $opt{interval} == 0;
190 if ($opt{'zero-missing'}) {
191 push @values, (0) x 10;
194 $SIG{INT} = 'DEFAULT';
197 $opt{color} and defined $_[0] or return '';
198 return "\e[$_[0]m" if defined wantarray;
199 $_ = color(@_) . $_ . color(0) if defined;
204 state $nr = $opt{hidemin};
206 @lines > $nr or return;
208 @order = sort { $b <=> $a } @order unless tied @order;
209 my $maxval = $opt{maxval} // (
210 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
213 my $minval = $opt{minval} // min $order[-1] // (), 0;
214 my $range = $maxval - $minval;
215 my $lenval = $opt{'value-length'} // max map { length } @order;
216 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
217 max map { length $values[$_] && length $lines[$_] }
218 0 .. min $#lines, $opt{hidemax} || (); # left padding
219 my $size = defined $opt{width} && $range &&
220 ($opt{width} - $lenval - $len) / $range; # bar multiplication
223 if ($opt{markers} and $size > 0) {
224 for my $markspec (split /\h/, $opt{markers}) {
225 my ($char, $func) = split //, $markspec, 2;
227 if ($func eq 'avg') {
228 return sum(@order) / @order;
230 elsif ($func =~ /\A([0-9.]+)v\z/) {
231 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
232 my $index = $#order * $1 / 100;
233 return ($order[$index] + $order[$index + .5]) / 2;
235 elsif ($func =~ /\A-?[0-9.]+\z/) {
239 die "Unknown marker $char: $func\n";
248 color(36) for $barmark[$pos * $size] = $char;
251 state $lastmax = $maxval;
252 if ($maxval > $lastmax) {
253 print ' ' x ($lenval + $len);
256 ($lastmax - $minval) * $size + .5,
257 '-' x (($values[$nr - 1] - $minval) * $size);
259 say '+' x (($range - $lastmax) * $size + .5);
266 color(31), sprintf('%*s', $lenval, $minval),
267 color(90), '-', color(36), '+',
268 color(32), sprintf('%*s', $size * $range - 3, $maxval),
269 color(90), '-', color(36), '+',
273 while ($nr <= $#lines) {
274 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
275 my $val = $values[$nr];
276 my $rel = length $val && $range && ($val - $minval) / $range;
277 my $color = !length $val || !$opt{palette} ? undef :
278 $val == $order[0] ? $opt{palette}->[-1] : # max
279 $val == $order[-1] ? $opt{palette}->[0] : # min
280 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
283 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
284 print color($color), $opt{spark}->[
285 !$val || !$#{$opt{spark}} ? 0 : # blank
286 $val == $order[0] ? -1 : # max
287 $val == $order[-1] ? 1 : # min
288 $#{$opt{spark}} < 3 ? 1 :
289 $rel * ($#{$opt{spark}} - 3) + 2.5
295 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
296 sprintf "%*s", $lenval, $val;
297 color($color) for $val;
299 my $line = $lines[$nr] =~ s/\n/$val/r;
300 printf '%-*s', $len + length($val), $line;
301 print $barmark[$_] // $opt{'graph-format'}
302 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
308 say $opt{palette} ? color(0) : '' if $opt{spark};
314 if ($opt{hidemin} or $opt{hidemax}) {
315 printf '%.8g of ', $opt{'sum-format'}->(sum(grep { length }
316 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
320 my $total = sum @order;
321 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
322 printf ' in %d values', scalar @order;
323 printf ' over %d lines', scalar @lines if @order != @lines;
324 printf(' (%s min, %s avg, %s max)',
325 color(31) . $order[-1] . color(0),
326 color(36) . $opt{'calc-format'}->($total / @order) . color(0),
327 color(32) . $order[0] . color(0),
336 show_stat() if $opt{stat};
337 exit 130 if @_; # 0x80+signo
348 barcat - graph to visualize input values
352 B<barcat> [<options>] [<file>... | <numbers>]
356 Visualizes relative sizes of values read from input
357 (parameters, file(s) or STDIN).
358 Contents are concatenated similar to I<cat>,
359 but numbers are reformatted and a bar graph is appended to each line.
361 Don't worry, barcat does not drink and divide.
362 It can has various options for input and output (re)formatting,
363 but remains limited to one-dimensional charts.
364 For more complex graphing needs
365 you'll need a larger animal like I<gnuplot>.
371 =item -c, --[no-]color
373 Force colored output of values and bar markers.
374 Defaults on if output is a tty,
375 disabled otherwise such as when piped or redirected.
377 =item -f, --field=(<number> | <regexp>)
379 Compare values after a given number of whitespace separators,
380 or matching a regular expression.
382 Unspecified or I<-f0> means values are at the start of each line.
383 With I<-f1> the second word is taken instead.
384 A string can indicate the starting position of a value
385 (such as I<-f:> if preceded by colons),
386 or capture the numbers itself,
387 for example I<-f'(\d+)'> for the first digits anywhere.
391 Prepend a chart axis with minimum and maximum values labeled.
393 =item -H, --human-readable
395 Format values using SI unit prefixes,
396 turning long numbers like I<12356789> into I<12.4M>.
397 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
398 Short integers are aligned but kept without decimal point.
400 =item -t, --interval[=(<seconds> | -<lines>)]
402 Output partial progress every given number of seconds or input lines.
403 An update can also be forced by sending a I<SIGALRM> alarm signal.
405 =item -l, --length=[-]<size>[%]
407 Trim line contents (between number and bars)
408 to a maximum number of characters.
409 The exceeding part is replaced by an abbreviation sign,
410 unless C<--length=0>.
412 Prepend a dash (i.e. make negative) to enforce padding
413 regardless of encountered contents.
415 =item -L, --limit[=(<count> | <start>-[<end>])]
417 Stop output after a number of lines.
418 All input is still counted and analyzed for statistics,
419 but disregarded for padding and bar size.
421 =item --graph-format=<character>
423 Glyph to repeat for the graph line.
424 Defaults to a dash C<->.
426 =item -m, --markers=<format>
428 Statistical positions to indicate on bars.
429 A single indicator glyph precedes each position:
435 Exact value to match on the axis.
436 A vertical bar at the zero crossing is displayed by I<|0>
438 For example I<:3.14> would show a colon at pi.
440 =item <percentage>I<v>
442 Ranked value at the given percentile.
443 The default shows I<+> at I<50v> for the mean or median;
444 the middle value or average between middle values.
445 One standard deviation right of the mean is at about I<68.3v>.
446 The default includes I<< >31.73v <68.27v >>
447 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
452 the sum of all values divided by the number of counted lines.
453 Indicated by default as I<=>.
457 =item --min=<number>, --max=<number>
459 Bars extend from 0 or the minimum value if lower,
460 to the largest value encountered.
461 These options can be set to customize this range.
463 =item --palette=(<preset> | <color>...)
465 Override colors of parsed numbers.
466 Can be any CSI escape, such as I<90> for default dark grey,
467 or alternatively I<1;30> for bright black.
469 In case of additional colors,
470 the last is used for values equal to the maximum, the first for minima.
471 If unspecified, these are green and red respectively (I<31 90 32>).
472 Multiple intermediate colors will be distributed
473 relative to the size of values.
475 Predefined color schemes are named I<whites> and I<fire>,
476 or I<greys> and I<fire256> for 256-color variants.
478 =item --spark[=<characters>]
480 Replace lines by I<sparklines>,
481 single characters corresponding to input values.
482 A specified sequence of unicode characters will be used for
483 Of a specified sequence of unicode characters,
484 the first one will be used for non-values,
485 the last one for the maximum,
486 the second (if any) for the minimum,
487 and any remaining will be distributed over the range of values.
488 Unspecified, block fill glyphs U+2581-2588 will be used.
492 Total statistics after all data.
494 =item -u, --unmodified
496 Do not reformat values, keeping leading whitespace.
497 Keep original value alignment, which may be significant in some programs.
499 =item --value-length=<size>
501 Reserved space for numbers.
503 =item -w, --width=<columns>
505 Override the maximum number of columns to use.
506 Appended graphics will extend to fill up the entire screen.
510 Overview of available options.
527 seq 30 | awk '{print sin($1/10)}' | barcat
529 Compare file sizes (with human-readable numbers):
531 du -d0 -b * | barcat -H
533 Memory usage of user processes with long names truncated:
535 ps xo %mem,pid,cmd | barcat -l40
537 Monitor network latency from prefixed results:
539 ping google.com | barcat -f'time=\K' -t
541 Commonly used after counting, for example users on the current server:
543 users | tr ' ' '\n' | sort | uniq -c | barcat
545 Letter frequencies in text files:
547 cat /usr/share/games/fortunes/*.u8 |
548 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
549 sort | uniq -c | barcat
551 Number of HTTP requests per day:
553 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
555 Any kind of database query with counts, preserving returned alignment:
557 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
560 In PostgreSQL from within the client:
562 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
564 Earthquakes worldwide magnitude 1+ in the last 24 hours:
566 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
567 column -tns, | barcat -f4 -u -l80%
569 External datasets, like movies per year:
571 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
572 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
574 But please get I<jq> to process JSON
575 and replace the manual selection by C<< jq '.[].year' >>.
577 Pokémon height comparison:
579 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
580 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
582 USD/EUR exchange rate from CSV provided by the ECB:
584 curl https://sdw.ecb.europa.eu/export.do \
585 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
586 grep '^[12]' | barcat -f',\K' --value-length=7
588 Total population history in XML from the World Bank:
590 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
591 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
592 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
594 And of course various Git statistics, such commit count by year:
596 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
598 Or the top 3 most frequent authors with statistics over all:
600 git shortlog -sn | barcat -L3 -s
602 Sparkline graphics of simple input given as inline parameters:
604 barcat --spark= 3 1 4 1 5 0 9 2 4
606 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
608 ( git log --pretty=%ci --since=30day | cut -b-10
609 seq 0 30 | xargs -i date +%F -d-{}day ) |
610 sort | uniq -c | awk '$1--' | barcat --spark
614 Mischa POSLAWSKY <perl@shiar.org>