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/ 22/m; # indent options list
83 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
86 my $parser = Pod::Usage->new;
87 $parser->select('SYNOPSIS', 'OPTIONS');
88 $parser->output_string(\my $contents);
89 $parser->parse_string_document($pod);
91 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
97 Pod::Usage::pod2usage(
98 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
101 ) or exit 64; # EX_USAGE
103 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
104 $opt{color} //= -t *STDOUT; # enable on tty
105 $opt{'graph-format'} //= '-';
106 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
107 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
108 $opt{anchor} //= qr/\A/;
109 $opt{'value-length'} = 6 if $opt{units};
110 $opt{'value-length'} = 1 if $opt{unmodified};
111 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
112 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
113 $opt{palette} //= $opt{color} && [31, 90, 32];
114 $opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
115 and undef $opt{interval};
117 my (@lines, @values, @order);
119 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
122 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
124 $SIG{INT} = \&show_exit;
126 if (defined $opt{interval}) {
127 $opt{interval} ||= 1;
128 alarm $opt{interval} if $opt{interval} > 0;
131 require Tie::Array::Sorted;
132 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
133 } or warn $@, "Expect slowdown with large datasets!\n";
136 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
137 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
139 s/^\h*// unless $opt{unmodified};
140 push @values, s/$valmatch/\n/ && $1;
141 push @order, $1 if length $1;
142 if (defined $opt{trim} and defined $1) {
143 my $trimpos = abs $opt{trim};
144 $trimpos -= length $1 if $opt{unmodified};
146 $_ = substr $_, 0, 2;
148 elsif (length > $trimpos) {
149 substr($_, $trimpos - 1) = '…';
153 show_lines() if defined $opt{interval} and $opt{interval} < 0
154 and $. % $opt{interval} == 0;
157 if ($opt{'zero-missing'}) {
158 push @values, (0) x 10;
161 $SIG{INT} = 'DEFAULT';
164 $opt{color} and defined $_[0] or return '';
165 return "\e[$_[0]m" if defined wantarray;
166 $_ = color(@_) . $_ . color(0) if defined;
171 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
172 @lines and @lines > $nr or return;
174 @lines > $nr or return unless $opt{hidemin};
176 @order = sort { $b <=> $a } @order unless tied @order;
177 my $maxval = $opt{maxval} // ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
178 my $minval = $opt{minval} // min $order[-1] // (), 0;
179 my $lenval = $opt{'value-length'} // max map { length } @order;
180 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
181 max map { length $values[$_] && length $lines[$_] }
182 0 .. min $#lines, $opt{hidemax} || (); # left padding
183 my $size = ($maxval - $minval) &&
184 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
187 if ($opt{markers} and $size > 0) {
188 for my $markspec (split /\h/, $opt{markers}) {
189 my ($char, $func) = split //, $markspec, 2;
191 if ($func eq 'avg') {
192 return sum(@order) / @order;
194 elsif ($func =~ /\A([0-9.]+)v\z/) {
195 my $index = $#order * $1 / 100;
196 return ($order[$index] + $order[$index + .5]) / 2;
203 color(36) for $barmark[$pos * $size] = $char;
206 state $lastmax = $maxval;
207 if ($maxval > $lastmax) {
208 print ' ' x ($lenval + $len);
211 ($lastmax - $minval) * $size + .5,
212 '-' x (($values[$nr - 1] - $minval) * $size);
214 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
220 @lines > $nr or return if $opt{hidemin};
223 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
224 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
226 $float && ($unit % 3) == ($unit < 0), # tenths
227 $_[0] / 1000 ** int($unit/3), # number
228 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
233 color(31), sprintf('%*s', $lenval, $minval),
234 color(90), '-', color(36), '+',
235 color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
236 color(90), '-', color(36), '+',
240 while ($nr <= $#lines) {
241 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
242 my $val = $values[$nr];
243 my $rel = length $val && ($val - $minval) / ($maxval - $minval);
244 my $color = !length $val || !$opt{palette} ? undef :
245 $val == $order[0] ? $opt{palette}->[-1] : # max
246 $val == $order[-1] ? $opt{palette}->[0] : # min
247 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
250 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
251 print color($color), $opt{spark}->[
253 $val == $order[0] ? -1 : # max
254 $val == $order[-1] ? 1 : # min
255 $#{$opt{spark}} < 3 ? 1 :
256 $rel * ($#{$opt{spark}} - 3) + 2.5
262 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
263 color($color) for $val;
265 my $line = $lines[$nr] =~ s/\n/$val/r;
266 printf '%-*s', $len + length($val), $line;
267 print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
273 say $opt{palette} ? color(0) : '' if $opt{spark};
278 if ($opt{hidemin} or $opt{hidemax}) {
280 $opt{hidemax} ||= @lines;
281 printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
284 my $total = sum @order;
285 printf '%s total', color(1) . sprintf('%.8g', $total) . color(0);
286 printf ' in %d values', scalar @order;
287 printf ' over %d lines', scalar @lines if @order != @lines;
288 printf(' (%s min, %s avg, %s max)',
289 color(31) . $order[-1] . color(0),
290 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
291 color(32) . $order[0] . color(0),
299 show_stat() if $opt{stat};
300 exit 130 if @_; # 0x80+signo
311 barcat - graph to visualize input values
315 B<barcat> [<options>] [<file>... | <numbers>]
319 Visualizes relative sizes of values read from input
320 (parameters, file(s) or STDIN).
321 Contents are concatenated similar to I<cat>,
322 but numbers are reformatted and a bar graph is appended to each line.
324 Don't worry, barcat does not drink and divide.
325 It can has various options for input and output (re)formatting,
326 but remains limited to one-dimensional charts.
327 For more complex graphing needs
328 you'll need a larger animal like I<gnuplot>.
334 =item -c, --[no-]color
336 Force colored output of values and bar markers.
337 Defaults on if output is a tty,
338 disabled otherwise such as when piped or redirected.
340 =item -f, --field=(<number>|<regexp>)
342 Compare values after a given number of whitespace separators,
343 or matching a regular expression.
345 Unspecified or I<-f0> means values are at the start of each line.
346 With I<-f1> the second word is taken instead.
347 A string can indicate the starting position of a value
348 (such as I<-f:> if preceded by colons),
349 or capture the numbers itself,
350 for example I<-f'(\d+)'> for the first digits anywhere.
354 Prepend a chart axis with minimum and maximum values labeled.
356 =item -H, --human-readable
358 Format values using SI unit prefixes,
359 turning long numbers like I<12356789> into I<12.4M>.
360 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
361 Short integers are aligned but kept without decimal point.
363 =item -t, --interval[=(<seconds>|-<lines>)]
365 Output partial progress every given number of seconds or input lines.
366 An update can also be forced by sending a I<SIGALRM> alarm signal.
368 =item -l, --length=[-]<size>[%]
370 Trim line contents (between number and bars)
371 to a maximum number of characters.
372 The exceeding part is replaced by an abbreviation sign,
373 unless C<--length=0>.
375 Prepend a dash (i.e. make negative) to enforce padding
376 regardless of encountered contents.
378 =item -L, --limit[=(<count> | <start>-[<end>])]
380 Stop output after a number of lines.
381 All input is still counted and analyzed for statistics,
382 but disregarded for padding and bar size.
384 =item --graph-format=<character>
386 Glyph to repeat for the graph line.
387 Defaults to a dash C<->.
389 =item -m, --markers=<format>
391 Statistical positions to indicate on bars.
392 A single indicator glyph precedes each position:
398 Exact value to match on the axis.
399 A vertical bar at the zero crossing is displayed by I<|0>
401 For example I<:3.14> would show a colon at pi.
403 =item <percentage>I<v>
405 Ranked value at the given percentile.
406 The default shows I<+> at I<50v> for the mean or median;
407 the middle value or average between middle values.
408 One standard deviation right of the mean is at about I<68.3v>.
409 The default includes I<< >31.73v <68.27v >>
410 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
415 the sum of all values divided by the number of counted lines.
416 Indicated by default as I<=>.
420 =item --min=<number>, --max=<number>
422 Bars extend from 0 or the minimum value if lower,
423 to the largest value encountered.
424 These options can be set to customize this range.
426 =item --palette=(<preset> | <color>...)
428 Override colors of parsed numbers.
429 Can be any CSI escape, such as I<90> for default dark grey,
430 or alternatively I<1;30> for bold black.
432 In case of additional colors,
433 the last is used for values equal to the maximum, the first for minima.
434 If unspecified, these are green and red respectively (I<31 90 32>).
436 =item --spark[=<glyphs>]
438 Replace lines by I<sparklines>,
439 single characters corresponding to input values.
440 A specified sequence of unicode characters will be used for
441 Of a specified sequence of unicode characters,
442 the first one will be used for non-values,
443 the last one for the maximum,
444 the second (if any) for the minimum,
445 and any remaining will be distributed over the range of values.
446 Unspecified, block fill glyphs U+2581-2588 will be used.
450 Total statistics after all data.
452 =item -u, --unmodified
454 Do not reformat values, keeping leading whitespace.
455 Keep original value alignment, which may be significant in some programs.
457 =item --value-length=<size>
459 Reserved space for numbers.
461 =item -w, --width=<columns>
463 Override the maximum number of columns to use.
464 Appended graphics will extend to fill up the entire screen.
468 Overview of available options.
485 seq 30 | awk '{print sin($1/10)}' | barcat
487 Compare file sizes (with human-readable numbers):
489 du -d0 -b * | barcat -H
491 Memory usage of user processes with long names truncated:
493 ps xo %mem,pid,cmd | barcat -l40
495 Monitor network latency from prefixed results:
497 ping google.com | barcat -f'time=\K' -t
499 Commonly used after counting, for example users on the current server:
501 users | tr ' ' '\n' | sort | uniq -c | barcat
503 Letter frequencies in text files:
505 cat /usr/share/games/fortunes/*.u8 |
506 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
507 sort | uniq -c | barcat
509 Number of HTTP requests per day:
511 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
513 Any kind of database query with counts, preserving returned alignment:
515 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
518 Earthquakes worldwide magnitude 1+ in the last 24 hours:
520 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
521 column -tns, | graph -f4 -u -l80%
523 External datasets, like movies per year:
525 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
526 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
528 But please get I<jq> to process JSON
529 and replace the manual selection by C<< jq '.[].year' >>.
531 Pokémon height comparison:
533 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
534 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
536 USD/EUR exchange rate from CSV provided by the ECB:
538 curl https://sdw.ecb.europa.eu/export.do \
539 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
540 grep '^[12]' | barcat -f',\K' --value-length=7
542 Total population history in XML from the World Bank:
544 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
545 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
546 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
548 And of course various Git statistics, such commit count by year:
550 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
552 Or the top 3 most frequent authors with statistics over all:
554 git shortlog -sn | barcat -L3 -s
556 Activity of the last days (substitute date C<-v-{}d> on BSD):
558 ( git log --pretty=%ci --since=30day | cut -b-10
559 seq 0 30 | xargs -i date +%F -d-{}day ) |
560 sort | uniq -c | awk '$1--' | graph --spark
564 Mischa POSLAWSKY <perl@shiar.org>