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
213 my $size = defined $opt{width} && $range &&
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 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
226 my $index = $#order * $1 / 100;
227 return ($order[$index] + $order[$index + .5]) / 2;
229 elsif ($func =~ /\A-?[0-9.]+\z/) {
233 die "Unknown marker $char: $func\n";
242 color(36) for $barmark[$pos * $size] = $char;
245 state $lastmax = $maxval;
246 if ($maxval > $lastmax) {
247 print ' ' x ($lenval + $len);
250 ($lastmax - $minval) * $size + .5,
251 '-' x (($values[$nr - 1] - $minval) * $size);
253 say '+' x (($range - $lastmax) * $size + .5);
260 color(31), sprintf('%*s', $lenval, $minval),
261 color(90), '-', color(36), '+',
262 color(32), sprintf('%*s', $size * $range - 3, $maxval),
263 color(90), '-', color(36), '+',
267 while ($nr <= $#lines) {
268 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
269 my $val = $values[$nr];
270 my $rel = length $val && $range && ($val - $minval) / $range;
271 my $color = !length $val || !$opt{palette} ? undef :
272 $val == $order[0] ? $opt{palette}->[-1] : # max
273 $val == $order[-1] ? $opt{palette}->[0] : # min
274 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
277 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
278 print color($color), $opt{spark}->[
279 !$val || !$#{$opt{spark}} ? 0 : # blank
280 $val == $order[0] ? -1 : # max
281 $val == $order[-1] ? 1 : # min
282 $#{$opt{spark}} < 3 ? 1 :
283 $rel * ($#{$opt{spark}} - 3) + 2.5
289 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
290 color($color) for $val;
292 my $line = $lines[$nr] =~ s/\n/$val/r;
293 printf '%-*s', $len + length($val), $line;
294 print $barmark[$_] // $opt{'graph-format'}
295 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
301 say $opt{palette} ? color(0) : '' if $opt{spark};
307 if ($opt{hidemin} or $opt{hidemax}) {
308 printf '%s of ', sum(grep { length }
309 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
313 my $total = sum @order;
314 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
315 printf ' in %d values', scalar @order;
316 printf ' over %d lines', scalar @lines if @order != @lines;
317 printf(' (%s min, %s avg, %s max)',
318 color(31) . $order[-1] . color(0),
319 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
320 color(32) . $order[0] . color(0),
329 show_stat() if $opt{stat};
330 exit 130 if @_; # 0x80+signo
341 barcat - graph to visualize input values
345 B<barcat> [<options>] [<file>... | <numbers>]
349 Visualizes relative sizes of values read from input
350 (parameters, file(s) or STDIN).
351 Contents are concatenated similar to I<cat>,
352 but numbers are reformatted and a bar graph is appended to each line.
354 Don't worry, barcat does not drink and divide.
355 It can has various options for input and output (re)formatting,
356 but remains limited to one-dimensional charts.
357 For more complex graphing needs
358 you'll need a larger animal like I<gnuplot>.
364 =item -c, --[no-]color
366 Force colored output of values and bar markers.
367 Defaults on if output is a tty,
368 disabled otherwise such as when piped or redirected.
370 =item -f, --field=(<number> | <regexp>)
372 Compare values after a given number of whitespace separators,
373 or matching a regular expression.
375 Unspecified or I<-f0> means values are at the start of each line.
376 With I<-f1> the second word is taken instead.
377 A string can indicate the starting position of a value
378 (such as I<-f:> if preceded by colons),
379 or capture the numbers itself,
380 for example I<-f'(\d+)'> for the first digits anywhere.
384 Prepend a chart axis with minimum and maximum values labeled.
386 =item -H, --human-readable
388 Format values using SI unit prefixes,
389 turning long numbers like I<12356789> into I<12.4M>.
390 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
391 Short integers are aligned but kept without decimal point.
393 =item -t, --interval[=(<seconds> | -<lines>)]
395 Output partial progress every given number of seconds or input lines.
396 An update can also be forced by sending a I<SIGALRM> alarm signal.
398 =item -l, --length=[-]<size>[%]
400 Trim line contents (between number and bars)
401 to a maximum number of characters.
402 The exceeding part is replaced by an abbreviation sign,
403 unless C<--length=0>.
405 Prepend a dash (i.e. make negative) to enforce padding
406 regardless of encountered contents.
408 =item -L, --limit[=(<count> | <start>-[<end>])]
410 Stop output after a number of lines.
411 All input is still counted and analyzed for statistics,
412 but disregarded for padding and bar size.
414 =item --graph-format=<character>
416 Glyph to repeat for the graph line.
417 Defaults to a dash C<->.
419 =item -m, --markers=<format>
421 Statistical positions to indicate on bars.
422 A single indicator glyph precedes each position:
428 Exact value to match on the axis.
429 A vertical bar at the zero crossing is displayed by I<|0>
431 For example I<:3.14> would show a colon at pi.
433 =item <percentage>I<v>
435 Ranked value at the given percentile.
436 The default shows I<+> at I<50v> for the mean or median;
437 the middle value or average between middle values.
438 One standard deviation right of the mean is at about I<68.3v>.
439 The default includes I<< >31.73v <68.27v >>
440 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
445 the sum of all values divided by the number of counted lines.
446 Indicated by default as I<=>.
450 =item --min=<number>, --max=<number>
452 Bars extend from 0 or the minimum value if lower,
453 to the largest value encountered.
454 These options can be set to customize this range.
456 =item --palette=(<preset> | <color>...)
458 Override colors of parsed numbers.
459 Can be any CSI escape, such as I<90> for default dark grey,
460 or alternatively I<1;30> for bright black.
462 In case of additional colors,
463 the last is used for values equal to the maximum, the first for minima.
464 If unspecified, these are green and red respectively (I<31 90 32>).
465 Multiple intermediate colors will be distributed
466 relative to the size of values.
468 Predefined color schemes are named I<whites> and I<fire>,
469 or I<greys> and I<fire256> for 256-color variants.
471 =item --spark[=<characters>]
473 Replace lines by I<sparklines>,
474 single characters corresponding to input values.
475 A specified sequence of unicode characters will be used for
476 Of a specified sequence of unicode characters,
477 the first one will be used for non-values,
478 the last one for the maximum,
479 the second (if any) for the minimum,
480 and any remaining will be distributed over the range of values.
481 Unspecified, block fill glyphs U+2581-2588 will be used.
485 Total statistics after all data.
487 =item -u, --unmodified
489 Do not reformat values, keeping leading whitespace.
490 Keep original value alignment, which may be significant in some programs.
492 =item --value-length=<size>
494 Reserved space for numbers.
496 =item -w, --width=<columns>
498 Override the maximum number of columns to use.
499 Appended graphics will extend to fill up the entire screen.
503 Overview of available options.
520 seq 30 | awk '{print sin($1/10)}' | barcat
522 Compare file sizes (with human-readable numbers):
524 du -d0 -b * | barcat -H
526 Memory usage of user processes with long names truncated:
528 ps xo %mem,pid,cmd | barcat -l40
530 Monitor network latency from prefixed results:
532 ping google.com | barcat -f'time=\K' -t
534 Commonly used after counting, for example users on the current server:
536 users | tr ' ' '\n' | sort | uniq -c | barcat
538 Letter frequencies in text files:
540 cat /usr/share/games/fortunes/*.u8 |
541 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
542 sort | uniq -c | barcat
544 Number of HTTP requests per day:
546 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
548 Any kind of database query with counts, preserving returned alignment:
550 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
553 In PostgreSQL from within the client:
555 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
557 Earthquakes worldwide magnitude 1+ in the last 24 hours:
559 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
560 column -tns, | barcat -f4 -u -l80%
562 External datasets, like movies per year:
564 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
565 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
567 But please get I<jq> to process JSON
568 and replace the manual selection by C<< jq '.[].year' >>.
570 Pokémon height comparison:
572 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
573 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
575 USD/EUR exchange rate from CSV provided by the ECB:
577 curl https://sdw.ecb.europa.eu/export.do \
578 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
579 grep '^[12]' | barcat -f',\K' --value-length=7
581 Total population history in XML from the World Bank:
583 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
584 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
585 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
587 And of course various Git statistics, such commit count by year:
589 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
591 Or the top 3 most frequent authors with statistics over all:
593 git shortlog -sn | barcat -L3 -s
595 Sparkline graphics of simple input given as inline parameters:
597 barcat --spark= 3 1 4 1 5 0 9 2 4
599 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
601 ( git log --pretty=%ci --since=30day | cut -b-10
602 seq 0 30 | xargs -i date +%F -d-{}day ) |
603 sort | uniq -c | awk '$1--' | barcat --spark
607 Mischa POSLAWSKY <perl@shiar.org>