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 my (@lines, @values, @order);
135 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
138 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
140 $SIG{INT} = \&show_exit;
142 if (defined $opt{interval}) {
143 $opt{interval} ||= 1;
144 alarm $opt{interval} if $opt{interval} > 0;
147 require Tie::Array::Sorted;
148 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
149 } or warn $@, "Expect slowdown with large datasets!\n";
153 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
155 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
157 s/\A\h*// unless $opt{unmodified};
158 my $valnum = s/$valmatch/\n/ && $1;
159 push @values, $valnum;
160 push @order, $valnum if length $valnum;
161 if (defined $opt{trim} and defined $valnum) {
162 my $trimpos = abs $opt{trim};
163 $trimpos -= length $valnum if $opt{unmodified};
165 $_ = substr $_, 0, 2;
167 elsif (length > $trimpos) {
168 # cut and replace (intentional lvalue for speed, contrary to PBP)
169 substr($_, $trimpos - 1) = '…';
173 show_lines() if defined $opt{interval} and $opt{interval} < 0
174 and $. % $opt{interval} == 0;
177 if ($opt{'zero-missing'}) {
178 push @values, (0) x 10;
181 $SIG{INT} = 'DEFAULT';
184 $opt{color} and defined $_[0] or return '';
185 return "\e[$_[0]m" if defined wantarray;
186 $_ = color(@_) . $_ . color(0) if defined;
190 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
191 my $float = $_[0] !~ /\A0*[-0-9]{1,3}\z/;
192 return sprintf('%3.*f%1s',
193 $float && ($unit % 3) == ($unit < 0), # tenths
194 $_[0] / 1000 ** int($unit/3), # number
195 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
201 state $nr = $opt{hidemin};
203 @lines > $nr or return;
205 @order = sort { $b <=> $a } @order unless tied @order;
206 my $maxval = $opt{maxval} // (
207 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
210 my $minval = $opt{minval} // min $order[-1] // (), 0;
211 my $range = $maxval - $minval;
212 my $lenval = $opt{'value-length'} // max map { length } @order;
213 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
214 max map { length $values[$_] && length $lines[$_] }
215 0 .. min $#lines, $opt{hidemax} || (); # left padding
216 my $size = defined $opt{width} && $range &&
217 ($opt{width} - $lenval - $len) / $range; # bar multiplication
220 if ($opt{markers} and $size > 0) {
221 for my $markspec (split /\h/, $opt{markers}) {
222 my ($char, $func) = split //, $markspec, 2;
224 if ($func eq 'avg') {
225 return sum(@order) / @order;
227 elsif ($func =~ /\A([0-9.]+)v\z/) {
228 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
229 my $index = $#order * $1 / 100;
230 return ($order[$index] + $order[$index + .5]) / 2;
232 elsif ($func =~ /\A-?[0-9.]+\z/) {
236 die "Unknown marker $char: $func\n";
245 color(36) for $barmark[$pos * $size] = $char;
248 state $lastmax = $maxval;
249 if ($maxval > $lastmax) {
250 print ' ' x ($lenval + $len);
253 ($lastmax - $minval) * $size + .5,
254 '-' x (($values[$nr - 1] - $minval) * $size);
256 say '+' x (($range - $lastmax) * $size + .5);
263 color(31), sprintf('%*s', $lenval, $minval),
264 color(90), '-', color(36), '+',
265 color(32), sprintf('%*s', $size * $range - 3, $maxval),
266 color(90), '-', color(36), '+',
270 while ($nr <= $#lines) {
271 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
272 my $val = $values[$nr];
273 my $rel = length $val && $range && ($val - $minval) / $range;
274 my $color = !length $val || !$opt{palette} ? undef :
275 $val == $order[0] ? $opt{palette}->[-1] : # max
276 $val == $order[-1] ? $opt{palette}->[0] : # min
277 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
280 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
281 print color($color), $opt{spark}->[
282 !$val || !$#{$opt{spark}} ? 0 : # blank
283 $val == $order[0] ? -1 : # max
284 $val == $order[-1] ? 1 : # min
285 $#{$opt{spark}} < 3 ? 1 :
286 $rel * ($#{$opt{spark}} - 3) + 2.5
292 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
293 color($color) for $val;
295 my $line = $lines[$nr] =~ s/\n/$val/r;
296 printf '%-*s', $len + length($val), $line;
297 print $barmark[$_] // $opt{'graph-format'}
298 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
304 say $opt{palette} ? color(0) : '' if $opt{spark};
310 if ($opt{hidemin} or $opt{hidemax}) {
311 printf '%.8g of ', sum(grep { length }
312 @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
316 my $total = sum @order;
317 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
318 printf ' in %d values', scalar @order;
319 printf ' over %d lines', scalar @lines if @order != @lines;
320 printf(' (%s min, %s avg, %s max)',
321 color(31) . $order[-1] . color(0),
322 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
323 color(32) . $order[0] . color(0),
332 show_stat() if $opt{stat};
333 exit 130 if @_; # 0x80+signo
344 barcat - graph to visualize input values
348 B<barcat> [<options>] [<file>... | <numbers>]
352 Visualizes relative sizes of values read from input
353 (parameters, file(s) or STDIN).
354 Contents are concatenated similar to I<cat>,
355 but numbers are reformatted and a bar graph is appended to each line.
357 Don't worry, barcat does not drink and divide.
358 It can has various options for input and output (re)formatting,
359 but remains limited to one-dimensional charts.
360 For more complex graphing needs
361 you'll need a larger animal like I<gnuplot>.
367 =item -c, --[no-]color
369 Force colored output of values and bar markers.
370 Defaults on if output is a tty,
371 disabled otherwise such as when piped or redirected.
373 =item -f, --field=(<number> | <regexp>)
375 Compare values after a given number of whitespace separators,
376 or matching a regular expression.
378 Unspecified or I<-f0> means values are at the start of each line.
379 With I<-f1> the second word is taken instead.
380 A string can indicate the starting position of a value
381 (such as I<-f:> if preceded by colons),
382 or capture the numbers itself,
383 for example I<-f'(\d+)'> for the first digits anywhere.
387 Prepend a chart axis with minimum and maximum values labeled.
389 =item -H, --human-readable
391 Format values using SI unit prefixes,
392 turning long numbers like I<12356789> into I<12.4M>.
393 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
394 Short integers are aligned but kept without decimal point.
396 =item -t, --interval[=(<seconds> | -<lines>)]
398 Output partial progress every given number of seconds or input lines.
399 An update can also be forced by sending a I<SIGALRM> alarm signal.
401 =item -l, --length=[-]<size>[%]
403 Trim line contents (between number and bars)
404 to a maximum number of characters.
405 The exceeding part is replaced by an abbreviation sign,
406 unless C<--length=0>.
408 Prepend a dash (i.e. make negative) to enforce padding
409 regardless of encountered contents.
411 =item -L, --limit[=(<count> | <start>-[<end>])]
413 Stop output after a number of lines.
414 All input is still counted and analyzed for statistics,
415 but disregarded for padding and bar size.
417 =item --graph-format=<character>
419 Glyph to repeat for the graph line.
420 Defaults to a dash C<->.
422 =item -m, --markers=<format>
424 Statistical positions to indicate on bars.
425 A single indicator glyph precedes each position:
431 Exact value to match on the axis.
432 A vertical bar at the zero crossing is displayed by I<|0>
434 For example I<:3.14> would show a colon at pi.
436 =item <percentage>I<v>
438 Ranked value at the given percentile.
439 The default shows I<+> at I<50v> for the mean or median;
440 the middle value or average between middle values.
441 One standard deviation right of the mean is at about I<68.3v>.
442 The default includes I<< >31.73v <68.27v >>
443 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
448 the sum of all values divided by the number of counted lines.
449 Indicated by default as I<=>.
453 =item --min=<number>, --max=<number>
455 Bars extend from 0 or the minimum value if lower,
456 to the largest value encountered.
457 These options can be set to customize this range.
459 =item --palette=(<preset> | <color>...)
461 Override colors of parsed numbers.
462 Can be any CSI escape, such as I<90> for default dark grey,
463 or alternatively I<1;30> for bright black.
465 In case of additional colors,
466 the last is used for values equal to the maximum, the first for minima.
467 If unspecified, these are green and red respectively (I<31 90 32>).
468 Multiple intermediate colors will be distributed
469 relative to the size of values.
471 Predefined color schemes are named I<whites> and I<fire>,
472 or I<greys> and I<fire256> for 256-color variants.
474 =item --spark[=<characters>]
476 Replace lines by I<sparklines>,
477 single characters corresponding to input values.
478 A specified sequence of unicode characters will be used for
479 Of a specified sequence of unicode characters,
480 the first one will be used for non-values,
481 the last one for the maximum,
482 the second (if any) for the minimum,
483 and any remaining will be distributed over the range of values.
484 Unspecified, block fill glyphs U+2581-2588 will be used.
488 Total statistics after all data.
490 =item -u, --unmodified
492 Do not reformat values, keeping leading whitespace.
493 Keep original value alignment, which may be significant in some programs.
495 =item --value-length=<size>
497 Reserved space for numbers.
499 =item -w, --width=<columns>
501 Override the maximum number of columns to use.
502 Appended graphics will extend to fill up the entire screen.
506 Overview of available options.
523 seq 30 | awk '{print sin($1/10)}' | barcat
525 Compare file sizes (with human-readable numbers):
527 du -d0 -b * | barcat -H
529 Memory usage of user processes with long names truncated:
531 ps xo %mem,pid,cmd | barcat -l40
533 Monitor network latency from prefixed results:
535 ping google.com | barcat -f'time=\K' -t
537 Commonly used after counting, for example users on the current server:
539 users | tr ' ' '\n' | sort | uniq -c | barcat
541 Letter frequencies in text files:
543 cat /usr/share/games/fortunes/*.u8 |
544 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
545 sort | uniq -c | barcat
547 Number of HTTP requests per day:
549 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
551 Any kind of database query with counts, preserving returned alignment:
553 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
556 In PostgreSQL from within the client:
558 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
560 Earthquakes worldwide magnitude 1+ in the last 24 hours:
562 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
563 column -tns, | barcat -f4 -u -l80%
565 External datasets, like movies per year:
567 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
568 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
570 But please get I<jq> to process JSON
571 and replace the manual selection by C<< jq '.[].year' >>.
573 Pokémon height comparison:
575 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
576 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
578 USD/EUR exchange rate from CSV provided by the ECB:
580 curl https://sdw.ecb.europa.eu/export.do \
581 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
582 grep '^[12]' | barcat -f',\K' --value-length=7
584 Total population history in XML from the World Bank:
586 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
587 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
588 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
590 And of course various Git statistics, such commit count by year:
592 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
594 Or the top 3 most frequent authors with statistics over all:
596 git shortlog -sn | barcat -L3 -s
598 Sparkline graphics of simple input given as inline parameters:
600 barcat --spark= 3 1 4 1 5 0 9 2 4
602 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
604 ( git log --pretty=%ci --since=30day | cut -b-10
605 seq 0 30 | xargs -i date +%F -d-{}day ) |
606 sort | uniq -c | awk '$1--' | barcat --spark
610 Mischa POSLAWSKY <perl@shiar.org>