5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
11 use Getopt::Long '2.33', qw( :config gnu_getopt );
16 'C' => sub { $opt{color} = 0 },
20 $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
21 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
25 'trim|length|l=s' => sub {
26 my ($optname, $optval) = @_;
27 $optval =~ s/%$// and $opt{trimpct}++;
28 $optval =~ m/\A-?[0-9]+\z/ or die(
29 "Value \"$optval\" invalid for option $optname",
30 " (number or percentage expected)\n"
40 my ($optname, $optval) = @_;
42 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
43 ($opt{hidemin}, $opt{hidemax}) =
44 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
45 "Value \"$optval\" invalid for option limit",
51 'graph-format=s' => sub {
52 $opt{'graph-format'} = substr $_[1], 0, 1;
55 $opt{spark} = [split //,
56 $_[1] || ($opt{ascii} ? ' ..oOO' : ' ▁▂▃▄▅▆▇█')
61 fire => [qw( 90 31 91 33 93 97 96 )],
62 fire88 => [map {"38;5;$_"} qw(
63 80 32 48 64 68 72 76 77 78 79 47
65 fire256=> [map {"38;5;$_"} qw(
67 202 208 214 220 226 227 228 229 230 231 159
69 ramp88 => [map {"38;5;$_"} qw(
70 64 65 66 67 51 35 39 23 22 26 25 28
72 whites => [qw( 1;30 0;37 1;37 )],
73 greys => [map {"38;5;$_"} 52, 235..255, 47],
74 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
81 say "barcat version $VERSION";
85 local $/ = undef; # slurp
86 my $pod = readline *DATA;
87 $pod =~ s/^=over\K/ 25/; # indent options list
89 ^=item \h \N*\n\n \N*\n \K # first line
90 (?: (?: ^=over .*? ^=back\n )? (?!=) \N*\n )*
91 }{\n}g; # abbreviate options
92 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
93 $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
94 # abbreviate <variable> indicators
95 $pod =~ s/\Q>.../s>/g;
96 $pod =~ s/<(?:number|count|seconds)>/N/g;
97 $pod =~ s/<character(s?)>/\Uchar$1/g;
99 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
102 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
103 -indent => 2, -width => 78,
105 $parser->select('SYNOPSIS', 'OPTIONS');
106 $parser->output_string(\my $contents);
107 $parser->parse_string_document($pod);
109 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
110 $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
116 Pod::Usage::pod2usage(
117 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
120 ) or exit 64; # EX_USAGE
122 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
123 $opt{color} //= -t *STDOUT; # enable on tty
124 $opt{'graph-format'} //= '-';
125 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
126 $opt{units} = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
127 if $opt{'human-readable'};
128 $opt{anchor} //= qr/\A/;
129 $opt{'value-length'} = 6 if $opt{units};
130 $opt{'value-length'} = 1 if $opt{unmodified};
131 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
132 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
133 $opt{palette} //= $opt{color} && [31, 90, 32];
134 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
135 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
136 and undef $opt{interval};
138 $opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
139 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
140 $opt{'value-format'} = $opt{units} && sub {
141 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
142 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
144 $float && ($unit % 3) == ($unit < 0), # tenths
145 $_[0] / 1000 ** int($unit/3), # number
146 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
151 my (@lines, @values, @order);
153 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
156 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
158 $SIG{INT} = \&show_exit;
160 if (defined $opt{interval}) {
161 $opt{interval} ||= 1;
162 alarm $opt{interval} if $opt{interval} > 0;
165 require Tie::Array::Sorted;
166 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
167 } or warn $@, "Expect slowdown with large datasets!\n";
171 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
173 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
175 s/\A\h*// unless $opt{unmodified};
176 my $valnum = s/$valmatch/\n/ && $1;
177 push @values, $valnum;
178 push @order, $valnum if length $valnum;
179 if (defined $opt{trim} and defined $valnum) {
180 my $trimpos = abs $opt{trim};
181 $trimpos -= length $valnum if $opt{unmodified};
183 $_ = substr $_, 0, 2;
185 elsif (length > $trimpos) {
186 # cut and replace (intentional lvalue for speed, contrary to PBP)
187 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
191 show_lines() if defined $opt{interval} and $opt{interval} < 0
192 and $. % $opt{interval} == 0;
195 if ($opt{'zero-missing'}) {
196 push @values, (0) x 10;
199 $SIG{INT} = 'DEFAULT';
202 $opt{color} and defined $_[0] or return '';
203 return "\e[$_[0]m" if defined wantarray;
204 $_ = color(@_) . $_ . color(0) if defined;
210 $opt{hidemin} < 0 ? @lines + $opt{hidemin} + 1 :
213 @lines > $nr or return;
215 @order = sort { $b <=> $a } @order unless tied @order;
216 my $maxval = $opt{maxval} // (
217 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
220 my $minval = $opt{minval} // min $order[-1] // (), 0;
221 my $range = $maxval - $minval;
222 my $lenval = $opt{'value-length'} // max map { length } @order;
223 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
224 max map { length $values[$_] && length $lines[$_] }
225 0 .. min $#lines, $opt{hidemax} || (); # left padding
226 my $size = defined $opt{width} && $range &&
227 ($opt{width} - $lenval - $len) / $range; # bar multiplication
230 if ($opt{markers} and $size > 0) {
231 for my $markspec (split /\h/, $opt{markers}) {
232 my ($char, $func) = split //, $markspec, 2;
234 if ($func eq 'avg') {
235 return sum(@order) / @order;
237 elsif ($func =~ /\A([0-9.]+)v\z/) {
238 die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
239 my $index = $#order * $1 / 100;
240 return ($order[$index] + $order[$index + .5]) / 2;
242 elsif ($func =~ /\A-?[0-9.]+\z/) {
246 die "Unknown marker $char: $func\n";
255 color(36) for $barmark[$pos * $size] = $char;
258 state $lastmax = $maxval;
259 if ($maxval > $lastmax) {
260 print ' ' x ($lenval + $len);
263 ($lastmax - $minval) * $size + .5,
264 '-' x (($values[$nr - 1] - $minval) * $size);
266 say '+' x (($range - $lastmax) * $size + .5);
273 color(31), sprintf('%*s', $lenval, $minval),
274 color(90), '-', color(36), '+',
275 color(32), sprintf('%*s', $size * $range - 3, $maxval),
276 color(90), '-', color(36), '+',
280 while ($nr <= $#lines) {
281 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
282 my $val = $values[$nr];
283 my $rel = length $val && $range && ($val - $minval) / $range;
284 my $color = !length $val || !$opt{palette} ? undef :
285 $val == $order[0] ? $opt{palette}->[-1] : # max
286 $val == $order[-1] ? $opt{palette}->[0] : # min
287 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
290 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
291 print color($color), $opt{spark}->[
292 !$val || !$#{$opt{spark}} ? 0 : # blank
293 $val == $order[0] ? -1 : # max
294 $val == $order[-1] ? 1 : # min
295 $#{$opt{spark}} < 3 ? 1 :
296 $rel * ($#{$opt{spark}} - 3) + 2.5
302 $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
303 sprintf "%*s", $lenval, $val;
304 color($color) for $val;
306 my $line = $lines[$nr] =~ s/\n/$val/r;
307 printf '%-*s', $len + length($val), $line;
308 print $barmark[$_] // $opt{'graph-format'}
309 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
315 say $opt{palette} ? color(0) : '' if $opt{spark};
321 if ($opt{hidemin} or $opt{hidemax}) {
322 my $linemin = $opt{hidemin};
323 $linemin += @lines if $linemin < 0;
324 my $linemax = ($opt{hidemax} || @lines) - 1;
325 printf '%.8g of ', $opt{'sum-format'}->(
326 sum(grep {length} @values[$linemin .. $linemax]) // 0
330 my $total = sum @order;
331 printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
332 printf ' in %d values', scalar @order;
333 printf ' over %d lines', scalar @lines if @order != @lines;
334 printf(' (%s min, %s avg, %s max)',
335 color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
336 color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
337 color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
346 show_stat() if $opt{stat};
347 exit 130 if @_; # 0x80+signo
358 barcat - graph to visualize input values
362 B<barcat> [<options>] [<file>... | <numbers>]
366 Visualizes relative sizes of values read from input
367 (parameters, file(s) or STDIN).
368 Contents are concatenated similar to I<cat>,
369 but numbers are reformatted and a bar graph is appended to each line.
371 Don't worry, barcat does not drink and divide.
372 It can has various options for input and output (re)formatting,
373 but remains limited to one-dimensional charts.
374 For more complex graphing needs
375 you'll need a larger animal like I<gnuplot>.
381 =item -a, --[no-]ascii
383 Restrict user interface to ASCII characters,
384 replacing default UTF-8 by their closest approximation.
385 Input is always interpreted as UTF-8 and shown as is.
387 =item -c, --[no-]color
389 Force colored output of values and bar markers.
390 Defaults on if output is a tty,
391 disabled otherwise such as when piped or redirected.
393 =item -f, --field=(<number> | <regexp>)
395 Compare values after a given number of whitespace separators,
396 or matching a regular expression.
398 Unspecified or I<-f0> means values are at the start of each line.
399 With I<-f1> the second word is taken instead.
400 A string can indicate the starting position of a value
401 (such as I<-f:> if preceded by colons),
402 or capture the numbers itself,
403 for example I<-f'(\d+)'> for the first digits anywhere.
407 Prepend a chart axis with minimum and maximum values labeled.
409 =item -H, --human-readable
411 Format values using SI unit prefixes,
412 turning long numbers like I<12356789> into I<12.4M>.
413 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
414 Short integers are aligned but kept without decimal point.
416 =item -t, --interval[=(<seconds> | -<lines>)]
418 Output partial progress every given number of seconds or input lines.
419 An update can also be forced by sending a I<SIGALRM> alarm signal.
421 =item -l, --length=[-]<size>[%]
423 Trim line contents (between number and bars)
424 to a maximum number of characters.
425 The exceeding part is replaced by an abbreviation sign,
426 unless C<--length=0>.
428 Prepend a dash (i.e. make negative) to enforce padding
429 regardless of encountered contents.
431 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
433 Stop output after a number of lines.
434 A single value indicates the last line number (like C<head>),
435 or first line counting from the bottom if negative (like C<tail>).
436 A specific range can be given by two values.
438 All input is still counted and analyzed for statistics,
439 but disregarded for padding and bar size.
441 =item --graph-format=<character>
443 Glyph to repeat for the graph line.
444 Defaults to a dash C<->.
446 =item -m, --markers=<format>
448 Statistical positions to indicate on bars.
449 A single indicator glyph precedes each position:
455 Exact value to match on the axis.
456 A vertical bar at the zero crossing is displayed by I<|0>
458 For example I<:3.14> would show a colon at pi.
460 =item <percentage>I<v>
462 Ranked value at the given percentile.
463 The default shows I<+> at I<50v> for the mean or median;
464 the middle value or average between middle values.
465 One standard deviation right of the mean is at about I<68.3v>.
466 The default includes I<< >31.73v <68.27v >>
467 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
472 the sum of all values divided by the number of counted lines.
473 Indicated by default as I<=>.
477 =item --min=<number>, --max=<number>
479 Bars extend from 0 or the minimum value if lower,
480 to the largest value encountered.
481 These options can be set to customize this range.
483 =item --palette=(<preset> | <color>...)
485 Override colors of parsed numbers.
486 Can be any CSI escape, such as I<90> for default dark grey,
487 or alternatively I<1;30> for bright black.
489 In case of additional colors,
490 the last is used for values equal to the maximum, the first for minima.
491 If unspecified, these are green and red respectively (I<31 90 32>).
492 Multiple intermediate colors will be distributed
493 relative to the size of values.
495 Predefined color schemes are named I<whites> and I<fire>,
496 or I<greys> and I<fire256> for 256-color variants.
498 =item --spark[=<characters>]
500 Replace lines by I<sparklines>,
501 single characters corresponding to input values.
502 A specified sequence of unicode characters will be used for
503 Of a specified sequence of unicode characters,
504 the first one will be used for non-values,
505 the last one for the maximum,
506 the second (if any) for the minimum,
507 and any remaining will be distributed over the range of values.
508 Unspecified, block fill glyphs U+2581-2588 will be used.
512 Total statistics after all data.
514 =item -u, --unmodified
516 Do not reformat values, keeping leading whitespace.
517 Keep original value alignment, which may be significant in some programs.
519 =item --value-length=<size>
521 Reserved space for numbers.
523 =item -w, --width=<columns>
525 Override the maximum number of columns to use.
526 Appended graphics will extend to fill up the entire screen.
530 Overview of available options.
547 seq 30 | awk '{print sin($1/10)}' | barcat
549 Compare file sizes (with human-readable numbers):
551 du -d0 -b * | barcat -H
553 Memory usage of user processes with long names truncated:
555 ps xo %mem,pid,cmd | barcat -l40
557 Monitor network latency from prefixed results:
559 ping google.com | barcat -f'time=\K' -t
561 Commonly used after counting, for example users on the current server:
563 users | tr ' ' '\n' | sort | uniq -c | barcat
565 Letter frequencies in text files:
567 cat /usr/share/games/fortunes/*.u8 |
568 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
569 sort | uniq -c | barcat
571 Number of HTTP requests per day:
573 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
575 Any kind of database query with counts, preserving returned alignment:
577 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
580 In PostgreSQL from within the client:
582 postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
584 Earthquakes worldwide magnitude 1+ in the last 24 hours:
586 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
587 column -tns, | barcat -f4 -u -l80%
589 External datasets, like movies per year:
591 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
592 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
594 But please get I<jq> to process JSON
595 and replace the manual selection by C<< jq '.[].year' >>.
597 Pokémon height comparison:
599 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
600 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
602 USD/EUR exchange rate from CSV provided by the ECB:
604 curl https://sdw.ecb.europa.eu/export.do \
605 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
606 grep '^[12]' | barcat -f',\K' --value-length=7
608 Total population history in XML from the World Bank:
610 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
611 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
612 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
614 And of course various Git statistics, such commit count by year:
616 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
618 Or the top 3 most frequent authors with statistics over all:
620 git shortlog -sn | barcat -L3 -s
622 Sparkline graphics of simple input given as inline parameters:
624 barcat --spark= 3 1 4 1 5 0 9 2 4
626 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
628 ( git log --pretty=%ci --since=30day | cut -b-10
629 seq 0 30 | xargs -i date +%F -d-{}day ) |
630 sort | uniq -c | awk '$1--' | barcat --spark
634 Mischa POSLAWSKY <perl@shiar.org>