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"
37 my ($optname, $optval) = @_;
39 ($opt{hidemin}, $opt{hidemax}) =
40 $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/x or die(
41 "Value \"$optval\" invalid for option limit",
47 'graph-format=s' => sub {
48 $opt{'graph-format'} = substr $_[1], 0, 1;
51 $opt{spark} = [split //, $_[1] || '▁▂▃▄▅▆▇█'];
54 $opt{palette} = [ split /\s/, $_[1] ];
61 say "barcat version $VERSION";
66 my $pod = readline *DATA;
67 $pod =~ s/^=over\K/ 22/m; # indent options list
68 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
71 my $parser = Pod::Usage->new;
72 $parser->select('SYNOPSIS', 'OPTIONS');
73 $parser->output_string(\my $contents);
74 $parser->parse_string_document($pod);
76 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
82 Pod::Usage::pod2usage(
83 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
86 ) or exit 64; # EX_USAGE
88 $opt{width} ||= $ENV{COLUMNS} || 80;
89 $opt{color} //= -t *STDOUT; # enable on tty
90 $opt{'graph-format'} //= '-';
91 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
92 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
93 $opt{anchor} //= qr/\A/;
94 $opt{'value-length'} = 6 if $opt{units};
95 $opt{'value-length'} = 1 if $opt{unmodified};
96 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
97 $opt{palette} //= $opt{color} && [31, 90, 32];
99 my (@lines, @values, @order);
101 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
104 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
106 $SIG{INT} = \&show_exit;
108 if (defined $opt{interval}) {
109 $opt{interval} ||= 1;
110 alarm $opt{interval} if $opt{interval} > 0;
113 require Tie::Array::Sorted;
114 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
115 } or warn $@, "Expect slowdown with large datasets!\n";
118 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
121 s/^\h*// unless $opt{unmodified};
122 push @values, s/$valmatch/\n/ && $1;
123 push @order, $1 if length $1;
124 if (defined $opt{trim} and defined $1) {
125 my $trimpos = abs $opt{trim};
126 $trimpos -= length $1 if $opt{unmodified};
128 $_ = substr $_, 0, 2;
130 elsif (length > $trimpos) {
131 substr($_, $trimpos - 1) = '…';
135 show_lines() if defined $opt{interval} and $opt{interval} < 0
136 and $. % $opt{interval} == 0;
139 $SIG{INT} = 'DEFAULT';
142 $opt{color} and defined $_[0] or return '';
143 return "\e[$_[0]m" if defined wantarray;
144 $_ = color(@_) . $_ . color(0) if defined;
149 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
150 @lines and @lines > $nr or return;
152 @lines > $nr or return unless $opt{hidemin};
154 @order = sort { $b <=> $a } @order unless tied @order;
155 my $maxval = ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
156 my $minval = min $order[-1] // (), 0;
157 my $lenval = $opt{'value-length'} // max map { length } @order;
158 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
159 max map { length $values[$_] && length $lines[$_] }
160 0 .. min $#lines, $opt{hidemax} || (); # left padding
161 my $size = ($maxval - $minval) &&
162 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
165 if ($opt{markers} // 1 and $size > 0) {
166 my sub orderpos { (($order[$_[0]] + $order[$_[0] + .5]) / 2 - $minval) * $size }
167 $barmark[ (sum(@order) / @order - $minval) * $size ] = '='; # average
168 $barmark[ orderpos($#order * .31731) ] = '>';
169 $barmark[ orderpos($#order * .68269) ] = '<';
170 $barmark[ orderpos($#order / 2) ] = '+'; # mean
171 $barmark[ -$minval * $size ] = '|' if $minval < 0; # zero
172 color(36) for @barmark;
174 state $lastmax = $maxval;
175 if ($maxval > $lastmax) {
176 print ' ' x ($lenval + $len);
179 ($lastmax - $minval) * $size + .5,
180 '-' x (($values[$nr - 1] - $minval) * $size);
182 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
188 @lines > $nr or return if $opt{hidemin};
191 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
192 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
194 $float && ($unit % 3) == ($unit < 0), # tenths
195 $_[0] / 1000 ** int($unit/3), # number
196 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
201 color(31), sprintf('%*s', $lenval, $minval),
202 color(90), '-', color(36), '+',
203 color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
204 color(90), '-', color(36), '+',
208 while ($nr <= $#lines) {
209 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
210 my $val = $values[$nr];
213 print $opt{spark}->[ ($val - $minval) / $maxval * $#{$opt{spark}} ];
218 my $color = !$opt{palette} ? undef :
219 $val == $order[0] ? $opt{palette}->[-1] : # max
220 $val == $order[-1] ? $opt{palette}->[0] : # min
221 $opt{palette}->[1] // $opt{palette}->[0];
222 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
223 color($color) for $val;
225 my $line = $lines[$nr] =~ s/\n/$val/r;
226 printf '%-*s', $len + length($val), $line;
227 print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
233 say '' if $opt{spark};
238 if ($opt{hidemin} or $opt{hidemax}) {
240 $opt{hidemax} ||= @lines;
241 printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
244 my $total = sum @order;
245 printf '%s total', color(1) . $total . color(0);
246 printf ' in %d values', scalar @values;
247 printf(' (%s min, %s avg, %s max)',
248 color(31) . $order[-1] . color(0),
249 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
250 color(32) . $order[0] . color(0),
258 show_stat() if $opt{stat};
259 exit 130 if @_; # 0x80+signo
270 barcat - graph to visualize input values
274 B<barcat> [<options>] [<input>]
278 Visualizes relative sizes of values read from input (file(s) or STDIN).
279 Contents are concatenated similar to I<cat>,
280 but numbers are reformatted and a bar graph is appended to each line.
282 Don't worry, barcat does not drink and divide.
283 It can has various options for input and output (re)formatting,
284 but remains limited to one-dimensional charts.
285 For more complex graphing needs
286 you'll need a larger animal like I<gnuplot>.
292 =item -c, --[no-]color
294 Force colored output of values and bar markers.
295 Defaults on if output is a tty,
296 disabled otherwise such as when piped or redirected.
298 =item -f, --field=(<number>|<regexp>)
300 Compare values after a given number of whitespace separators,
301 or matching a regular expression.
303 Unspecified or I<-f0> means values are at the start of each line.
304 With I<-f1> the second word is taken instead.
305 A string can indicate the starting position of a value
306 (such as I<-f:> if preceded by colons),
307 or capture the numbers itself,
308 for example I<-f'(\d+)'> for the first digits anywhere.
312 Prepend a chart axis with minimum and maximum values labeled.
314 =item -H, --human-readable
316 Format values using SI unit prefixes,
317 turning long numbers like I<12356789> into I<12.4M>.
318 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
319 Short integers are aligned but kept without decimal point.
321 =item -t, --interval[=(<seconds>|-<lines>)]
323 Output partial progress every given number of seconds or input lines.
324 An update can also be forced by sending a I<SIGALRM> alarm signal.
326 =item -l, --length=[-]<size>[%]
328 Trim line contents (between number and bars)
329 to a maximum number of characters.
330 The exceeding part is replaced by an abbreviation sign,
331 unless C<--length=0>.
333 Prepend a dash (i.e. make negative) to enforce padding
334 regardless of encountered contents.
336 =item -L, --limit=(<count>|<start>-[<end>])
338 Stop output after a number of lines.
339 All input is still counted and analyzed for statistics,
340 but disregarded for padding and bar size.
342 =item --graph-format=<character>
344 Glyph to repeat for the graph line.
345 Defaults to a dash C<->.
349 Statistical positions to indicate on bars.
350 Cannot be customized yet,
351 only disabled by providing an empty argument.
353 Any value enables all marker characters:
360 the sum of all values divided by the number of counted lines.
365 the middle value or average between middle values.
369 Standard deviation left of the mean.
370 Only 16% of all values are lower.
374 Standard deviation right of the mean.
375 The part between B<< <--> >> encompass all I<normal> results,
376 or 68% of all entries.
380 =item --palette=<color>...
382 Override colors of parsed numbers.
383 Can be any CSI escape, such as I<90> for default dark grey,
384 or alternatively I<1;30> for bold black.
386 In case of additional colors,
387 the last is used for values equal to the maximum, the first for minima.
388 If unspecified, these are green and red respectively (I<31 90 32>).
390 =item --spark[=<glyphs>]
392 Replace lines by I<sparklines>,
393 single characters corresponding to input values.
394 A specified sequence of unicode characters will be used for
395 Of a specified sequence of unicode characters,
396 the first one will be used for non-values,
397 the last one for the maximum,
398 the second (if any) for the minimum,
399 and any remaining will be distributed over the range of values.
400 Unspecified, block fill glyphs U+2581-2588 will be used.
404 Total statistics after all data.
406 =item -u, --unmodified
408 Do not reformat values, keeping leading whitespace.
409 Keep original value alignment, which may be significant in some programs.
411 =item --value-length=<size>
413 Reserved space for numbers.
415 =item -w, --width=<columns>
417 Override the maximum number of columns to use.
418 Appended graphics will extend to fill up the entire screen.
422 Overview of available options.
439 seq 30 | awk '{print sin($1/10)}' | barcat
441 Compare file sizes (with human-readable numbers):
443 du -d0 -b * | barcat -H
445 Memory usage of user processes with long names truncated:
447 ps xo %mem,pid,cmd | barcat -l40
449 Monitor network latency from prefixed results:
451 ping google.com | barcat -f'time=\K' -t
453 Commonly used after counting, for example users on the current server:
455 users | sed 's/ /\n/g' | sort | uniq -c | barcat
457 Letter frequencies in text files:
459 cat /usr/share/games/fortunes/*.u8 |
460 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
461 sort | uniq -c | barcat
463 Number of HTTP requests per day:
465 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
467 Any kind of database query with counts, preserving returned alignment:
469 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
472 Earthquakes worldwide magnitude 1+ in the last 24 hours:
474 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
475 column -tns, | graph -f4 -u -l80%
477 External datasets, like movies per year:
479 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
480 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
482 But please get I<jq> to process JSON
483 and replace the manual selection by C<< jq '.[].year' >>.
485 Pokémon height comparison:
487 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
488 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
490 USD/EUR exchange rate from CSV provided by the ECB:
492 curl https://sdw.ecb.europa.eu/export.do \
493 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
494 grep '^[12]' | barcat -f',\K' --value-length=7
496 Total population history from the World Bank dataset (XML):
497 External datasets, like total population in XML from the World Bank:
499 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
500 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
501 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
503 And of course various Git statistics, such commit count by year:
505 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
507 Or the top 3 most frequent authors with statistics over all:
509 git shortlog -sn | barcat -L3 -s
511 Activity of the last days (substitute date C<-v-{}d> on BSD):
513 ( git log --pretty=%ci --since=30day | cut -b-10
514 seq 0 30 | xargs -i date +%F -d-{}day ) |
515 sort | uniq -c | awk '$1--' | graph --spark
519 Mischa POSLAWSKY <perl@shiar.org>