5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
10 use Getopt::Long '2.33', qw( :config gnu_getopt );
14 'C' => sub { $opt{color} = 0 },
18 $opt{anchor} = /^[0-9]+$/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
19 } or die $@ =~ s/(?: at .+)?$/ for option $_[0]/r;
23 'trim|length|l=s' => sub {
24 my ($optname, $optval) = @_;
25 $optval =~ s/%$// and $opt{trimpct}++;
26 $optval =~ m/^-?[0-9]+$/ or die(
27 "Value \"$optval\" invalid for option $optname",
28 " (number or percentage expected)\n"
38 my ($optname, $optval) = @_;
40 ($opt{hidemin}, $opt{hidemax}) =
41 $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/x or die(
42 "Value \"$optval\" invalid for option limit",
48 'graph-format=s' => sub {
49 $opt{'graph-format'} = substr $_[1], 0, 1;
52 $opt{spark} = [split //, $_[1] || ' ▁▂▃▄▅▆▇█'];
56 fire => [qw( 90 31 91 33 93 97 96 )],
57 fire88 => [map {"38;5;$_"} qw(
58 80 32 48 64 68 72 76 77 78 79 47
60 fire256=> [map {"38;5;$_"} qw(
62 202 208 214 220 226 227 228 229 230 231 159
64 ramp88 => [map {"38;5;$_"} qw(
65 64 65 66 67 51 35 39 23 22 26 25 28
67 whites => [qw( 1;30 0;37 1;37 )],
68 greys => [map {"38;5;$_"} 52, 235..255, 47],
69 }->{$_[1]} // [ split /[^0-9;]/, $_[1] ];
76 say "barcat version $VERSION";
81 my $pod = readline *DATA;
82 $pod =~ s/^=over\K/ 25/m; # indent options list
83 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
84 $pod =~ s/[.,](?=\n)//g; # trailing punctuation
85 $pod =~ s/^=item \K(?=--)/____/gm; # align long options
86 # abbreviate <variable> indicators
87 $pod =~ s/\Q>.../s>/g;
88 $pod =~ s/<(?:number|count|seconds)>/N/g;
89 $pod =~ s/<character(s?)>/\Uchar$1/g;
91 $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
94 my $parser = Pod::Usage->new(USAGE_OPTIONS => {
95 -indent => 2, -width => 78,
97 $parser->select('SYNOPSIS', 'OPTIONS');
98 $parser->output_string(\my $contents);
99 $parser->parse_string_document($pod);
101 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
102 $contents =~ s/^ \K____/ /gm; # nbsp substitute
108 Pod::Usage::pod2usage(
109 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
112 ) or exit 64; # EX_USAGE
114 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
115 $opt{color} //= -t *STDOUT; # enable on tty
116 $opt{'graph-format'} //= '-';
117 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
118 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
119 $opt{anchor} //= qr/\A/;
120 $opt{'value-length'} = 6 if $opt{units};
121 $opt{'value-length'} = 1 if $opt{unmodified};
122 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
123 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
124 $opt{palette} //= $opt{color} && [31, 90, 32];
125 $opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
126 and undef $opt{interval};
128 my (@lines, @values, @order);
130 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
133 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
135 $SIG{INT} = \&show_exit;
137 if (defined $opt{interval}) {
138 $opt{interval} ||= 1;
139 alarm $opt{interval} if $opt{interval} > 0;
142 require Tie::Array::Sorted;
143 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
144 } or warn $@, "Expect slowdown with large datasets!\n";
148 $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
150 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
152 s/^\h*// unless $opt{unmodified};
153 push @values, s/$valmatch/\n/ && $1;
154 push @order, $1 if length $1;
155 if (defined $opt{trim} and defined $1) {
156 my $trimpos = abs $opt{trim};
157 $trimpos -= length $1 if $opt{unmodified};
159 $_ = substr $_, 0, 2;
161 elsif (length > $trimpos) {
162 substr($_, $trimpos - 1) = '…';
166 show_lines() if defined $opt{interval} and $opt{interval} < 0
167 and $. % $opt{interval} == 0;
170 if ($opt{'zero-missing'}) {
171 push @values, (0) x 10;
174 $SIG{INT} = 'DEFAULT';
177 $opt{color} and defined $_[0] or return '';
178 return "\e[$_[0]m" if defined wantarray;
179 $_ = color(@_) . $_ . color(0) if defined;
183 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
184 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
186 $float && ($unit % 3) == ($unit < 0), # tenths
187 $_[0] / 1000 ** int($unit/3), # number
188 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
194 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
195 @lines and @lines > $nr or return;
197 @lines > $nr or return unless $opt{hidemin};
199 @order = sort { $b <=> $a } @order unless tied @order;
200 my $maxval = $opt{maxval} // (
201 $opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] :
204 my $minval = $opt{minval} // min $order[-1] // (), 0;
205 my $range = $maxval - $minval;
206 my $lenval = $opt{'value-length'} // max map { length } @order;
207 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
208 max map { length $values[$_] && length $lines[$_] }
209 0 .. min $#lines, $opt{hidemax} || (); # left padding
211 ($opt{width} - $lenval - $len) / $range; # bar multiplication
214 if ($opt{markers} and $size > 0) {
215 for my $markspec (split /\h/, $opt{markers}) {
216 my ($char, $func) = split //, $markspec, 2;
218 if ($func eq 'avg') {
219 return sum(@order) / @order;
221 elsif ($func =~ /\A([0-9.]+)v\z/) {
222 my $index = $#order * $1 / 100;
223 return ($order[$index] + $order[$index + .5]) / 2;
230 color(36) for $barmark[$pos * $size] = $char;
233 state $lastmax = $maxval;
234 if ($maxval > $lastmax) {
235 print ' ' x ($lenval + $len);
238 ($lastmax - $minval) * $size + .5,
239 '-' x (($values[$nr - 1] - $minval) * $size);
241 say '+' x (($range - $lastmax) * $size + .5);
247 @lines > $nr or return if $opt{hidemin};
250 color(31), sprintf('%*s', $lenval, $minval),
251 color(90), '-', color(36), '+',
252 color(32), sprintf('%*s', $size * $range - 3, $maxval),
253 color(90), '-', color(36), '+',
257 while ($nr <= $#lines) {
258 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
259 my $val = $values[$nr];
260 my $rel = length $val && $range && ($val - $minval) / $range;
261 my $color = !length $val || !$opt{palette} ? undef :
262 $val == $order[0] ? $opt{palette}->[-1] : # max
263 $val == $order[-1] ? $opt{palette}->[0] : # min
264 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
267 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
268 print color($color), $opt{spark}->[
270 $val == $order[0] ? -1 : # max
271 $val == $order[-1] ? 1 : # min
272 $#{$opt{spark}} < 3 ? 1 :
273 $rel * ($#{$opt{spark}} - 3) + 2.5
279 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
280 color($color) for $val;
282 my $line = $lines[$nr] =~ s/\n/$val/r;
283 printf '%-*s', $len + length($val), $line;
284 print $barmark[$_] // $opt{'graph-format'}
285 for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
291 say $opt{palette} ? color(0) : '' if $opt{spark};
296 if ($opt{hidemin} or $opt{hidemax}) {
298 $opt{hidemax} ||= @lines;
299 printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
302 my $total = sum @order;
303 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
304 printf ' in %d values', scalar @order;
305 printf ' over %d lines', scalar @lines if @order != @lines;
306 printf(' (%s min, %s avg, %s max)',
307 color(31) . $order[-1] . color(0),
308 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
309 color(32) . $order[0] . color(0),
317 show_stat() if $opt{stat};
318 exit 130 if @_; # 0x80+signo
329 barcat - graph to visualize input values
333 B<barcat> [<options>] [<file>... | <numbers>]
337 Visualizes relative sizes of values read from input
338 (parameters, file(s) or STDIN).
339 Contents are concatenated similar to I<cat>,
340 but numbers are reformatted and a bar graph is appended to each line.
342 Don't worry, barcat does not drink and divide.
343 It can has various options for input and output (re)formatting,
344 but remains limited to one-dimensional charts.
345 For more complex graphing needs
346 you'll need a larger animal like I<gnuplot>.
352 =item -c, --[no-]color
354 Force colored output of values and bar markers.
355 Defaults on if output is a tty,
356 disabled otherwise such as when piped or redirected.
358 =item -f, --field=(<number> | <regexp>)
360 Compare values after a given number of whitespace separators,
361 or matching a regular expression.
363 Unspecified or I<-f0> means values are at the start of each line.
364 With I<-f1> the second word is taken instead.
365 A string can indicate the starting position of a value
366 (such as I<-f:> if preceded by colons),
367 or capture the numbers itself,
368 for example I<-f'(\d+)'> for the first digits anywhere.
372 Prepend a chart axis with minimum and maximum values labeled.
374 =item -H, --human-readable
376 Format values using SI unit prefixes,
377 turning long numbers like I<12356789> into I<12.4M>.
378 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
379 Short integers are aligned but kept without decimal point.
381 =item -t, --interval[=(<seconds> | -<lines>)]
383 Output partial progress every given number of seconds or input lines.
384 An update can also be forced by sending a I<SIGALRM> alarm signal.
386 =item -l, --length=[-]<size>[%]
388 Trim line contents (between number and bars)
389 to a maximum number of characters.
390 The exceeding part is replaced by an abbreviation sign,
391 unless C<--length=0>.
393 Prepend a dash (i.e. make negative) to enforce padding
394 regardless of encountered contents.
396 =item -L, --limit[=(<count> | <start>-[<end>])]
398 Stop output after a number of lines.
399 All input is still counted and analyzed for statistics,
400 but disregarded for padding and bar size.
402 =item --graph-format=<character>
404 Glyph to repeat for the graph line.
405 Defaults to a dash C<->.
407 =item -m, --markers=<format>
409 Statistical positions to indicate on bars.
410 A single indicator glyph precedes each position:
416 Exact value to match on the axis.
417 A vertical bar at the zero crossing is displayed by I<|0>
419 For example I<:3.14> would show a colon at pi.
421 =item <percentage>I<v>
423 Ranked value at the given percentile.
424 The default shows I<+> at I<50v> for the mean or median;
425 the middle value or average between middle values.
426 One standard deviation right of the mean is at about I<68.3v>.
427 The default includes I<< >31.73v <68.27v >>
428 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
433 the sum of all values divided by the number of counted lines.
434 Indicated by default as I<=>.
438 =item --min=<number>, --max=<number>
440 Bars extend from 0 or the minimum value if lower,
441 to the largest value encountered.
442 These options can be set to customize this range.
444 =item --palette=(<preset> | <color>...)
446 Override colors of parsed numbers.
447 Can be any CSI escape, such as I<90> for default dark grey,
448 or alternatively I<1;30> for bright black.
450 In case of additional colors,
451 the last is used for values equal to the maximum, the first for minima.
452 If unspecified, these are green and red respectively (I<31 90 32>).
454 =item --spark[=<characters>]
456 Replace lines by I<sparklines>,
457 single characters corresponding to input values.
458 A specified sequence of unicode characters will be used for
459 Of a specified sequence of unicode characters,
460 the first one will be used for non-values,
461 the last one for the maximum,
462 the second (if any) for the minimum,
463 and any remaining will be distributed over the range of values.
464 Unspecified, block fill glyphs U+2581-2588 will be used.
468 Total statistics after all data.
470 =item -u, --unmodified
472 Do not reformat values, keeping leading whitespace.
473 Keep original value alignment, which may be significant in some programs.
475 =item --value-length=<size>
477 Reserved space for numbers.
479 =item -w, --width=<columns>
481 Override the maximum number of columns to use.
482 Appended graphics will extend to fill up the entire screen.
486 Overview of available options.
503 seq 30 | awk '{print sin($1/10)}' | barcat
505 Compare file sizes (with human-readable numbers):
507 du -d0 -b * | barcat -H
509 Memory usage of user processes with long names truncated:
511 ps xo %mem,pid,cmd | barcat -l40
513 Monitor network latency from prefixed results:
515 ping google.com | barcat -f'time=\K' -t
517 Commonly used after counting, for example users on the current server:
519 users | tr ' ' '\n' | sort | uniq -c | barcat
521 Letter frequencies in text files:
523 cat /usr/share/games/fortunes/*.u8 |
524 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
525 sort | uniq -c | barcat
527 Number of HTTP requests per day:
529 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
531 Any kind of database query with counts, preserving returned alignment:
533 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
536 Earthquakes worldwide magnitude 1+ in the last 24 hours:
538 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
539 column -tns, | graph -f4 -u -l80%
541 External datasets, like movies per year:
543 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
544 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
546 But please get I<jq> to process JSON
547 and replace the manual selection by C<< jq '.[].year' >>.
549 Pokémon height comparison:
551 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
552 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
554 USD/EUR exchange rate from CSV provided by the ECB:
556 curl https://sdw.ecb.europa.eu/export.do \
557 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
558 grep '^[12]' | barcat -f',\K' --value-length=7
560 Total population history in XML from the World Bank:
562 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
563 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
564 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
566 And of course various Git statistics, such commit count by year:
568 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
570 Or the top 3 most frequent authors with statistics over all:
572 git shortlog -sn | barcat -L3 -s
574 Sparkline graphics of simple input given as inline parameters:
576 barcat --spark= 3 1 4 1 5 0 9 2 4
578 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
580 ( git log --pretty=%ci --since=30day | cut -b-10
581 seq 0 30 | xargs -i date +%F -d-{}day ) |
582 sort | uniq -c | awk '$1--' | graph --spark
586 Mischa POSLAWSKY <perl@shiar.org>