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";
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 push @values, s/$valmatch/\n/ && $1;
156 push @order, $1 if length $1;
157 if (defined $opt{trim} and defined $1) {
158 my $trimpos = abs $opt{trim};
159 $trimpos -= length $1 if $opt{unmodified};
161 $_ = substr $_, 0, 2;
163 elsif (length > $trimpos) {
164 substr($_, $trimpos - 1) = '…';
168 show_lines() if defined $opt{interval} and $opt{interval} < 0
169 and $. % $opt{interval} == 0;
172 if ($opt{'zero-missing'}) {
173 push @values, (0) x 10;
176 $SIG{INT} = 'DEFAULT';
179 $opt{color} and defined $_[0] or return '';
180 return "\e[$_[0]m" if defined wantarray;
181 $_ = color(@_) . $_ . color(0) if defined;
185 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
186 my $float = $_[0] !~ /\A0*[-0-9]{1,3}\z/;
188 $float && ($unit % 3) == ($unit < 0), # tenths
189 $_[0] / 1000 ** int($unit/3), # number
190 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
196 state $nr = $opt{hidemin};
198 @lines > $nr or return;
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);
249 color(31), sprintf('%*s', $lenval, $minval),
250 color(90), '-', color(36), '+',
251 color(32), sprintf('%*s', $size * $range - 3, $maxval),
252 color(90), '-', color(36), '+',
256 while ($nr <= $#lines) {
257 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
258 my $val = $values[$nr];
259 my $rel = length $val && $range && ($val - $minval) / $range;
260 my $color = !length $val || !$opt{palette} ? undef :
261 $val == $order[0] ? $opt{palette}->[-1] : # max
262 $val == $order[-1] ? $opt{palette}->[0] : # min
263 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
266 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
267 print color($color), $opt{spark}->[
269 $val == $order[0] ? -1 : # max
270 $val == $order[-1] ? 1 : # min
271 $#{$opt{spark}} < 3 ? 1 :
272 $rel * ($#{$opt{spark}} - 3) + 2.5
278 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
279 color($color) for $val;
281 my $line = $lines[$nr] =~ s/\n/$val/r;
282 printf '%-*s', $len + length($val), $line;
283 print $barmark[$_] // $opt{'graph-format'}
284 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
290 say $opt{palette} ? color(0) : '' if $opt{spark};
295 if ($opt{hidemin} or $opt{hidemax}) {
296 printf '%s of ', sum(grep { length }
297 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
301 my $total = sum @order;
302 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
303 printf ' in %d values', scalar @order;
304 printf ' over %d lines', scalar @lines if @order != @lines;
305 printf(' (%s min, %s avg, %s max)',
306 color(31) . $order[-1] . color(0),
307 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
308 color(32) . $order[0] . color(0),
316 show_stat() if $opt{stat};
317 exit 130 if @_; # 0x80+signo
328 barcat - graph to visualize input values
332 B<barcat> [<options>] [<file>... | <numbers>]
336 Visualizes relative sizes of values read from input
337 (parameters, file(s) or STDIN).
338 Contents are concatenated similar to I<cat>,
339 but numbers are reformatted and a bar graph is appended to each line.
341 Don't worry, barcat does not drink and divide.
342 It can has various options for input and output (re)formatting,
343 but remains limited to one-dimensional charts.
344 For more complex graphing needs
345 you'll need a larger animal like I<gnuplot>.
351 =item -c, --[no-]color
353 Force colored output of values and bar markers.
354 Defaults on if output is a tty,
355 disabled otherwise such as when piped or redirected.
357 =item -f, --field=(<number> | <regexp>)
359 Compare values after a given number of whitespace separators,
360 or matching a regular expression.
362 Unspecified or I<-f0> means values are at the start of each line.
363 With I<-f1> the second word is taken instead.
364 A string can indicate the starting position of a value
365 (such as I<-f:> if preceded by colons),
366 or capture the numbers itself,
367 for example I<-f'(\d+)'> for the first digits anywhere.
371 Prepend a chart axis with minimum and maximum values labeled.
373 =item -H, --human-readable
375 Format values using SI unit prefixes,
376 turning long numbers like I<12356789> into I<12.4M>.
377 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
378 Short integers are aligned but kept without decimal point.
380 =item -t, --interval[=(<seconds> | -<lines>)]
382 Output partial progress every given number of seconds or input lines.
383 An update can also be forced by sending a I<SIGALRM> alarm signal.
385 =item -l, --length=[-]<size>[%]
387 Trim line contents (between number and bars)
388 to a maximum number of characters.
389 The exceeding part is replaced by an abbreviation sign,
390 unless C<--length=0>.
392 Prepend a dash (i.e. make negative) to enforce padding
393 regardless of encountered contents.
395 =item -L, --limit[=(<count> | <start>-[<end>])]
397 Stop output after a number of lines.
398 All input is still counted and analyzed for statistics,
399 but disregarded for padding and bar size.
401 =item --graph-format=<character>
403 Glyph to repeat for the graph line.
404 Defaults to a dash C<->.
406 =item -m, --markers=<format>
408 Statistical positions to indicate on bars.
409 A single indicator glyph precedes each position:
415 Exact value to match on the axis.
416 A vertical bar at the zero crossing is displayed by I<|0>
418 For example I<:3.14> would show a colon at pi.
420 =item <percentage>I<v>
422 Ranked value at the given percentile.
423 The default shows I<+> at I<50v> for the mean or median;
424 the middle value or average between middle values.
425 One standard deviation right of the mean is at about I<68.3v>.
426 The default includes I<< >31.73v <68.27v >>
427 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
432 the sum of all values divided by the number of counted lines.
433 Indicated by default as I<=>.
437 =item --min=<number>, --max=<number>
439 Bars extend from 0 or the minimum value if lower,
440 to the largest value encountered.
441 These options can be set to customize this range.
443 =item --palette=(<preset> | <color>...)
445 Override colors of parsed numbers.
446 Can be any CSI escape, such as I<90> for default dark grey,
447 or alternatively I<1;30> for bright black.
449 In case of additional colors,
450 the last is used for values equal to the maximum, the first for minima.
451 If unspecified, these are green and red respectively (I<31 90 32>).
452 Multiple intermediate colors will be distributed
453 relative to the size of values.
455 Predefined color schemes are named I<whites> and I<fire>,
456 or I<greys> and I<fire256> for 256-color variants.
458 =item --spark[=<characters>]
460 Replace lines by I<sparklines>,
461 single characters corresponding to input values.
462 A specified sequence of unicode characters will be used for
463 Of a specified sequence of unicode characters,
464 the first one will be used for non-values,
465 the last one for the maximum,
466 the second (if any) for the minimum,
467 and any remaining will be distributed over the range of values.
468 Unspecified, block fill glyphs U+2581-2588 will be used.
472 Total statistics after all data.
474 =item -u, --unmodified
476 Do not reformat values, keeping leading whitespace.
477 Keep original value alignment, which may be significant in some programs.
479 =item --value-length=<size>
481 Reserved space for numbers.
483 =item -w, --width=<columns>
485 Override the maximum number of columns to use.
486 Appended graphics will extend to fill up the entire screen.
490 Overview of available options.
507 seq 30 | awk '{print sin($1/10)}' | barcat
509 Compare file sizes (with human-readable numbers):
511 du -d0 -b * | barcat -H
513 Memory usage of user processes with long names truncated:
515 ps xo %mem,pid,cmd | barcat -l40
517 Monitor network latency from prefixed results:
519 ping google.com | barcat -f'time=\K' -t
521 Commonly used after counting, for example users on the current server:
523 users | tr ' ' '\n' | sort | uniq -c | barcat
525 Letter frequencies in text files:
527 cat /usr/share/games/fortunes/*.u8 |
528 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
529 sort | uniq -c | barcat
531 Number of HTTP requests per day:
533 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
535 Any kind of database query with counts, preserving returned alignment:
537 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
540 In PostgreSQL from within the client:
542 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
544 Earthquakes worldwide magnitude 1+ in the last 24 hours:
546 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
547 column -tns, | barcat -f4 -u -l80%
549 External datasets, like movies per year:
551 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
552 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
554 But please get I<jq> to process JSON
555 and replace the manual selection by C<< jq '.[].year' >>.
557 Pokémon height comparison:
559 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
560 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
562 USD/EUR exchange rate from CSV provided by the ECB:
564 curl https://sdw.ecb.europa.eu/export.do \
565 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
566 grep '^[12]' | barcat -f',\K' --value-length=7
568 Total population history in XML from the World Bank:
570 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
571 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
572 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
574 And of course various Git statistics, such commit count by year:
576 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
578 Or the top 3 most frequent authors with statistics over all:
580 git shortlog -sn | barcat -L3 -s
582 Sparkline graphics of simple input given as inline parameters:
584 barcat --spark= 3 1 4 1 5 0 9 2 4
586 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
588 ( git log --pretty=%ci --since=30day | cut -b-10
589 seq 0 30 | xargs -i date +%F -d-{}day ) |
590 sort | uniq -c | awk '$1--' | barcat --spark
594 Mischa POSLAWSKY <perl@shiar.org>