5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
7 use experimental qw( lexical_subs );
11 use Getopt::Long '2.33', qw( :config gnu_getopt );
15 'C' => sub { $opt{color} = 0 },
19 $opt{anchor} = /^[0-9]+$/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
20 } or die $@ =~ s/(?: at .+)?$/ for option $_[0]/r;
24 'trim|length|l=s' => sub {
25 my ($optname, $optval) = @_;
26 $optval =~ s/%$// and $opt{trimpct}++;
27 $optval =~ m/^-?[0-9]+$/ 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/x 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 whites => [qw( 1;30 0;37 1;37 )],
59 }->{$_[1]} // [ split /\s/, $_[1] ];
66 say "barcat version $VERSION";
71 my $pod = readline *DATA;
72 $pod =~ s/^=over\K/ 22/m; # indent options list
73 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
76 my $parser = Pod::Usage->new;
77 $parser->select('SYNOPSIS', 'OPTIONS');
78 $parser->output_string(\my $contents);
79 $parser->parse_string_document($pod);
81 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
87 Pod::Usage::pod2usage(
88 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
91 ) or exit 64; # EX_USAGE
93 $opt{width} ||= $ENV{COLUMNS} || 80;
94 $opt{color} //= -t *STDOUT; # enable on tty
95 $opt{'graph-format'} //= '-';
96 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
97 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
98 $opt{anchor} //= qr/\A/;
99 $opt{'value-length'} = 6 if $opt{units};
100 $opt{'value-length'} = 1 if $opt{unmodified};
101 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
102 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
103 $opt{palette} //= $opt{color} && [31, 90, 32];
105 my (@lines, @values, @order);
107 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
110 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
112 $SIG{INT} = \&show_exit;
114 if (defined $opt{interval}) {
115 $opt{interval} ||= 1;
116 alarm $opt{interval} if $opt{interval} > 0;
119 require Tie::Array::Sorted;
120 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
121 } or warn $@, "Expect slowdown with large datasets!\n";
124 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
127 s/^\h*// unless $opt{unmodified};
128 push @values, s/$valmatch/\n/ && $1;
129 push @order, $1 if length $1;
130 if (defined $opt{trim} and defined $1) {
131 my $trimpos = abs $opt{trim};
132 $trimpos -= length $1 if $opt{unmodified};
134 $_ = substr $_, 0, 2;
136 elsif (length > $trimpos) {
137 substr($_, $trimpos - 1) = '…';
141 show_lines() if defined $opt{interval} and $opt{interval} < 0
142 and $. % $opt{interval} == 0;
145 $SIG{INT} = 'DEFAULT';
148 $opt{color} and defined $_[0] or return '';
149 return "\e[$_[0]m" if defined wantarray;
150 $_ = color(@_) . $_ . color(0) if defined;
155 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
156 @lines and @lines > $nr or return;
158 @lines > $nr or return unless $opt{hidemin};
160 @order = sort { $b <=> $a } @order unless tied @order;
161 my $maxval = $opt{maxval} // ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
162 my $minval = $opt{minval} // min $order[-1] // (), 0;
163 my $lenval = $opt{'value-length'} // max map { length } @order;
164 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
165 max map { length $values[$_] && length $lines[$_] }
166 0 .. min $#lines, $opt{hidemax} || (); # left padding
167 my $size = ($maxval - $minval) &&
168 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
171 if ($opt{markers} and $size > 0) {
172 for my $markspec (split /\h/, $opt{markers}) {
173 my ($char, $func) = split //, $markspec, 2;
175 if ($func eq 'avg') {
176 return sum(@order) / @order;
178 elsif ($func =~ /\A([0-9.]+)v\z/) {
179 my $index = $#order * $1 / 100;
180 return ($order[$index] + $order[$index + .5]) / 2;
187 color(36) for $barmark[$pos * $size] = $char;
190 state $lastmax = $maxval;
191 if ($maxval > $lastmax) {
192 print ' ' x ($lenval + $len);
195 ($lastmax - $minval) * $size + .5,
196 '-' x (($values[$nr - 1] - $minval) * $size);
198 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
204 @lines > $nr or return if $opt{hidemin};
207 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
208 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
210 $float && ($unit % 3) == ($unit < 0), # tenths
211 $_[0] / 1000 ** int($unit/3), # number
212 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
217 color(31), sprintf('%*s', $lenval, $minval),
218 color(90), '-', color(36), '+',
219 color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
220 color(90), '-', color(36), '+',
224 while ($nr <= $#lines) {
225 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
226 my $val = $values[$nr];
227 my $rel = length $val && ($val - $minval) / ($maxval - $minval);
230 print color($opt{palette}->[ $rel * $#{$opt{palette}} ]) if $opt{palette};
231 print $opt{spark}->[ $rel * $#{$opt{spark}} ];
236 my $color = !$opt{palette} ? undef :
237 $val == $order[0] ? $opt{palette}->[-1] : # max
238 $val == $order[-1] ? $opt{palette}->[0] : # min
239 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
240 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
241 color($color) for $val;
243 my $line = $lines[$nr] =~ s/\n/$val/r;
244 printf '%-*s', $len + length($val), $line;
245 print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
251 say $opt{palette} ? color(0) : '' if $opt{spark};
256 if ($opt{hidemin} or $opt{hidemax}) {
258 $opt{hidemax} ||= @lines;
259 printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
262 my $total = sum @order;
263 printf '%s total', color(1) . $total . color(0);
264 printf ' in %d values', scalar @values;
265 printf(' (%s min, %s avg, %s max)',
266 color(31) . $order[-1] . color(0),
267 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
268 color(32) . $order[0] . color(0),
276 show_stat() if $opt{stat};
277 exit 130 if @_; # 0x80+signo
288 barcat - graph to visualize input values
292 B<barcat> [<options>] [<input>]
296 Visualizes relative sizes of values read from input (file(s) or STDIN).
297 Contents are concatenated similar to I<cat>,
298 but numbers are reformatted and a bar graph is appended to each line.
300 Don't worry, barcat does not drink and divide.
301 It can has various options for input and output (re)formatting,
302 but remains limited to one-dimensional charts.
303 For more complex graphing needs
304 you'll need a larger animal like I<gnuplot>.
310 =item -c, --[no-]color
312 Force colored output of values and bar markers.
313 Defaults on if output is a tty,
314 disabled otherwise such as when piped or redirected.
316 =item -f, --field=(<number>|<regexp>)
318 Compare values after a given number of whitespace separators,
319 or matching a regular expression.
321 Unspecified or I<-f0> means values are at the start of each line.
322 With I<-f1> the second word is taken instead.
323 A string can indicate the starting position of a value
324 (such as I<-f:> if preceded by colons),
325 or capture the numbers itself,
326 for example I<-f'(\d+)'> for the first digits anywhere.
330 Prepend a chart axis with minimum and maximum values labeled.
332 =item -H, --human-readable
334 Format values using SI unit prefixes,
335 turning long numbers like I<12356789> into I<12.4M>.
336 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
337 Short integers are aligned but kept without decimal point.
339 =item -t, --interval[=(<seconds>|-<lines>)]
341 Output partial progress every given number of seconds or input lines.
342 An update can also be forced by sending a I<SIGALRM> alarm signal.
344 =item -l, --length=[-]<size>[%]
346 Trim line contents (between number and bars)
347 to a maximum number of characters.
348 The exceeding part is replaced by an abbreviation sign,
349 unless C<--length=0>.
351 Prepend a dash (i.e. make negative) to enforce padding
352 regardless of encountered contents.
354 =item -L, --limit=(<count>|<start>-[<end>])
356 Stop output after a number of lines.
357 All input is still counted and analyzed for statistics,
358 but disregarded for padding and bar size.
360 =item --graph-format=<character>
362 Glyph to repeat for the graph line.
363 Defaults to a dash C<->.
365 =item -m, --markers=<format>
367 Statistical positions to indicate on bars.
368 A single indicator glyph precedes each position:
374 Exact value to match on the axis.
375 A vertical bar at the zero crossing is displayed by I<|0>
377 For example I<:3.14> would show a colon at pi.
379 =item <percentage>I<v>
381 Ranked value at the given percentile.
382 The default shows I<+> at I<50v> for the mean or median;
383 the middle value or average between middle values.
384 One standard deviation right of the mean is at about I<68.3v>.
385 The default includes I<< >31.73v <68.27v >>
386 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
391 the sum of all values divided by the number of counted lines.
392 Indicated by default as I<=>.
396 =item --min=<number>, --max=<number>
398 Bars extend from 0 or the minimum value if lower,
399 to the largest value encountered.
400 These options can be set to customize this range.
402 =item --palette=(<preset> | <color>...)
404 Override colors of parsed numbers.
405 Can be any CSI escape, such as I<90> for default dark grey,
406 or alternatively I<1;30> for bold black.
408 In case of additional colors,
409 the last is used for values equal to the maximum, the first for minima.
410 If unspecified, these are green and red respectively (I<31 90 32>).
412 =item --spark[=<glyphs>]
414 Replace lines by I<sparklines>,
415 single characters corresponding to input values.
416 A specified sequence of unicode characters will be used for
417 Of a specified sequence of unicode characters,
418 the first one will be used for non-values,
419 the last one for the maximum,
420 the second (if any) for the minimum,
421 and any remaining will be distributed over the range of values.
422 Unspecified, block fill glyphs U+2581-2588 will be used.
426 Total statistics after all data.
428 =item -u, --unmodified
430 Do not reformat values, keeping leading whitespace.
431 Keep original value alignment, which may be significant in some programs.
433 =item --value-length=<size>
435 Reserved space for numbers.
437 =item -w, --width=<columns>
439 Override the maximum number of columns to use.
440 Appended graphics will extend to fill up the entire screen.
444 Overview of available options.
461 seq 30 | awk '{print sin($1/10)}' | barcat
463 Compare file sizes (with human-readable numbers):
465 du -d0 -b * | barcat -H
467 Memory usage of user processes with long names truncated:
469 ps xo %mem,pid,cmd | barcat -l40
471 Monitor network latency from prefixed results:
473 ping google.com | barcat -f'time=\K' -t
475 Commonly used after counting, for example users on the current server:
477 users | sed 's/ /\n/g' | sort | uniq -c | barcat
479 Letter frequencies in text files:
481 cat /usr/share/games/fortunes/*.u8 |
482 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
483 sort | uniq -c | barcat
485 Number of HTTP requests per day:
487 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
489 Any kind of database query with counts, preserving returned alignment:
491 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
494 Earthquakes worldwide magnitude 1+ in the last 24 hours:
496 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
497 column -tns, | graph -f4 -u -l80%
499 External datasets, like movies per year:
501 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
502 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
504 But please get I<jq> to process JSON
505 and replace the manual selection by C<< jq '.[].year' >>.
507 Pokémon height comparison:
509 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
510 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
512 USD/EUR exchange rate from CSV provided by the ECB:
514 curl https://sdw.ecb.europa.eu/export.do \
515 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
516 grep '^[12]' | barcat -f',\K' --value-length=7
518 Total population history from the World Bank dataset (XML):
519 External datasets, like total population in XML from the World Bank:
521 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
522 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
523 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
525 And of course various Git statistics, such commit count by year:
527 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
529 Or the top 3 most frequent authors with statistics over all:
531 git shortlog -sn | barcat -L3 -s
533 Activity of the last days (substitute date C<-v-{}d> on BSD):
535 ( git log --pretty=%ci --since=30day | cut -b-10
536 seq 0 30 | xargs -i date +%F -d-{}day ) |
537 sort | uniq -c | awk '$1--' | graph --spark
541 Mischa POSLAWSKY <perl@shiar.org>