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",
46 'graph-format=s' => sub {
47 $opt{'graph-format'} = substr $_[1], 0, 1;
50 $opt{spark} = [split //, $_[1] || '⎽▁▂▃▄▅▆▇█'];
58 my $pod = readline *DATA;
59 $pod =~ s/^=over\K/ 22/m; # indent options list
60 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
63 my $parser = Pod::Usage->new;
64 $parser->select('SYNOPSIS', 'OPTIONS');
65 $parser->output_string(\my $contents);
66 $parser->parse_string_document($pod);
68 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
74 Pod::Usage::pod2usage(
75 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
78 ) or exit 64; # EX_USAGE
80 $opt{width} ||= $ENV{COLUMNS} || 80;
81 $opt{color} //= -t *STDOUT; # enable on tty
82 $opt{'graph-format'} //= '-';
83 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
84 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
85 $opt{anchor} //= qr/\A/;
86 $opt{'value-length'} = 6 if $opt{units};
87 $opt{'value-length'} = 1 if $opt{unmodified};
88 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
90 my (@lines, @values, @order);
92 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
95 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
98 if (defined $opt{interval}) {
100 alarm $opt{interval} if $opt{interval} > 0;
103 require Tie::Array::Sorted;
104 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
105 } or warn $@, "Expect slowdown with large datasets!\n";
109 $SIG{INT} = 'DEFAULT'; # reset for subsequent attempts
111 'IGNORE' # continue after assumed eof
114 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
117 s/^\h*// unless $opt{unmodified};
118 push @values, s/$valmatch/\n/ && $1;
119 push @order, $1 if length $1;
120 if (defined $opt{trim} and defined $1) {
121 my $trimpos = abs $opt{trim};
122 $trimpos -= length $1 if $opt{unmodified};
124 $_ = substr $_, 0, 2;
126 elsif (length > $trimpos) {
127 substr($_, $trimpos - 1) = '…';
131 show_lines() if defined $opt{interval} and $opt{interval} < 0
132 and $. % $opt{interval} == 0;
135 $SIG{INT} = 'DEFAULT';
138 $opt{color} and defined $_[0] or return '';
139 return "\e[$_[0]m" if defined wantarray;
140 $_ = color(@_) . $_ . color(0) if defined;
145 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
146 @lines and @lines > $nr or return;
148 @lines > $nr or return unless $opt{hidemin};
150 @order = sort { $b <=> $a } @order unless tied @order;
151 my $maxval = ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
152 my $minval = min $order[-1] // (), 0;
153 my $lenval = $opt{'value-length'} // max map { length } @order;
154 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
155 max map { length $values[$_] && length $lines[$_] }
156 0 .. min $#lines, $opt{hidemax} || (); # left padding
157 my $size = ($maxval - $minval) &&
158 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
161 if ($opt{markers} // 1 and $size > 0) {
162 my sub orderpos { (($order[$_[0]] + $order[$_[0] + .5]) / 2 - $minval) * $size }
163 $barmark[ (sum(@order) / @order - $minval) * $size ] = '='; # average
164 $barmark[ orderpos($#order * .31731) ] = '>';
165 $barmark[ orderpos($#order * .68269) ] = '<';
166 $barmark[ orderpos($#order / 2) ] = '+'; # mean
167 $barmark[ -$minval * $size ] = '|' if $minval < 0; # zero
168 color(36) for @barmark;
170 state $lastmax = $maxval;
171 if ($maxval > $lastmax) {
172 print ' ' x ($lenval + $len);
175 ($lastmax - $minval) * $size + .5,
176 '-' x (($values[$nr - 1] - $minval) * $size);
178 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
184 @lines > $nr or return if $opt{hidemin};
187 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
188 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
190 $float && ($unit % 3) == ($unit < 0), # tenths
191 $_[0] / 1000 ** int($unit/3), # number
192 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
196 while ($nr <= $#lines) {
197 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
198 my $val = $values[$nr];
201 print $opt{spark}->[ ($val - $minval) / $maxval * $#{$opt{spark}} ];
206 my $color = !$opt{color} ? undef :
207 $val == $order[0] ? 32 : # max
208 $val == $order[-1] ? 31 : # min
210 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
211 color($color) for $val;
213 my $line = $lines[$nr] =~ s/\n/$val/r;
214 printf '%-*s', $len + length($val), $line;
215 print $barmark[$_] // $opt{'graph-format'} for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
221 say '' if $opt{spark};
227 if ($opt{hidemin} or $opt{hidemax}) {
229 $opt{hidemax} ||= @lines;
230 printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
233 my $total = sum @order;
234 printf '%s total', color(1) . $total . color(0);
235 printf ' in %d values', scalar @values;
236 printf(' (%s min, %s avg, %s max)',
237 color(31) . $order[-1] . color(0),
238 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
239 color(32) . $order[0] . color(0),
244 show_stat() if $opt{stat};
251 barcat - graph to visualize input values
255 B<barcat> [<options>] [<input>]
259 Visualizes relative sizes of values read from input (file(s) or STDIN).
260 Contents are concatenated similar to I<cat>,
261 but numbers are reformatted and a bar graph is appended to each line.
263 Don't worry, barcat does not drink and divide.
264 It can has various options for input and output (re)formatting,
265 but remains limited to one-dimensional charts.
266 For more complex graphing needs
267 you'll need a larger animal like I<gnuplot>.
273 =item -c, --[no-]color
275 Force colored output of values and bar markers.
276 Defaults on if output is a tty,
277 disabled otherwise such as when piped or redirected.
279 =item -f, --field=(<number>|<regexp>)
281 Compare values after a given number of whitespace separators,
282 or matching a regular expression.
284 Unspecified or I<-f0> means values are at the start of each line.
285 With I<-f1> the second word is taken instead.
286 A string can indicate the starting position of a value
287 (such as I<-f:> if preceded by colons),
288 or capture the numbers itself,
289 for example I<-f'(\d+)'> for the first digits anywhere.
291 =item -H, --human-readable
293 Format values using SI unit prefixes,
294 turning long numbers like I<12356789> into I<12.4M>.
295 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
296 Short integers are aligned but kept without decimal point.
298 =item -t, --interval[=(<seconds>|-<lines>)]
300 Output partial progress every given number of seconds or input lines.
301 An update can also be forced by sending a I<SIGALRM> alarm signal.
303 =item -l, --length=[-]<size>[%]
305 Trim line contents (between number and bars)
306 to a maximum number of characters.
307 The exceeding part is replaced by an abbreviation sign,
308 unless C<--length=0>.
310 Prepend a dash (i.e. make negative) to enforce padding
311 regardless of encountered contents.
313 =item -L, --limit=(<count>|<start>-[<end>])
315 Stop output after a number of lines.
316 All input is still counted and analyzed for statistics,
317 but disregarded for padding and bar size.
319 =item --graph-format=<character>
321 Glyph to repeat for the graph line.
322 Defaults to a dash C<->.
326 Statistical positions to indicate on bars.
327 Cannot be customized yet,
328 only disabled by providing an empty argument.
330 Any value enables all marker characters:
337 the sum of all values divided by the number of counted lines.
342 the middle value or average between middle values.
346 Standard deviation left of the mean.
347 Only 16% of all values are lower.
351 Standard deviation right of the mean.
352 The part between B<< <--> >> encompass all I<normal> results,
353 or 68% of all entries.
359 Total statistics after all data.
361 =item -u, --unmodified
363 Do not reformat values, keeping leading whitespace.
364 Keep original value alignment, which may be significant in some programs.
366 =item --value-length=<size>
368 Reserved space for numbers.
370 =item -w, --width=<columns>
372 Override the maximum number of columns to use.
373 Appended graphics will extend to fill up the entire screen.
377 Overview of available options.
394 seq 30 | awk '{print sin($1/10)}' | barcat
396 Compare file sizes (with human-readable numbers):
398 du -d0 -b * | barcat -H
400 Memory usage of user processes with long names truncated:
402 ps xo %mem,pid,cmd | barcat -l40
404 Monitor network latency from prefixed results:
406 ping google.com | barcat -f'time=\K' -t
408 Commonly used after counting, for example users on the current server:
410 users | sed 's/ /\n/g' | sort | uniq -c | barcat
412 Letter frequencies in text files:
414 cat /usr/share/games/fortunes/*.u8 |
415 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
416 sort | uniq -c | barcat
418 Number of HTTP requests per day:
420 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
422 Any kind of database query with counts, preserving returned alignment:
424 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
427 Earthquakes worldwide magnitude 1+ in the last 24 hours:
429 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
430 column -tns, | graph -f4 -u -l80%
432 External datasets, like movies per year:
434 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
435 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
437 But please get I<jq> to process JSON
438 and replace the manual selection by C<< jq '.[].year' >>.
440 Pokémon height comparison:
442 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
443 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
445 USD/EUR exchange rate from CSV provided by the ECB:
447 curl https://sdw.ecb.europa.eu/export.do \
448 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
449 grep '^[12]' | barcat -f',\K' --value-length=7
451 Total population history from the World Bank dataset (XML):
452 External datasets, like total population in XML from the World Bank:
454 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
455 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
456 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
458 And of course various Git statistics, such commit count by year:
460 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
462 Or the top 3 most frequent authors with statistics over all:
464 git shortlog -sn | barcat -L3 -s
466 Activity of the last days (substitute date C<-v-{}d> on BSD):
468 ( git log --pretty=%ci --since=30day | cut -b-10
469 seq 0 30 | xargs -i date +%F -d-{}day ) |
470 sort | uniq -c | awk '$1--' | graph --spark
474 Mischa POSLAWSKY <perl@shiar.org>