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 $opt{spark} = [split //, $_[1] || '⎽▁▂▃▄▅▆▇█'];
54 my $pod = readline *DATA;
55 $pod =~ s/^=over\K/ 22/m; # indent options list
56 $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
59 my $parser = Pod::Usage->new;
60 $parser->select('SYNOPSIS', 'OPTIONS');
61 $parser->output_string(\my $contents);
62 $parser->parse_string_document($pod);
64 $contents =~ s/\n(?=\n\h)//msg; # strip space between items
70 Pod::Usage::pod2usage(
71 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
74 ) or exit 64; # EX_USAGE
76 $opt{width} ||= $ENV{COLUMNS} || 80;
77 $opt{color} //= -t *STDOUT; # enable on tty
78 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
79 $opt{units} = [split //, ' kMGTPEZYyzafpnμm'] if $opt{'human-readable'};
80 $opt{anchor} //= qr/\A/;
81 $opt{'value-length'} = 6 if $opt{units};
82 $opt{'value-length'} = 1 if $opt{unmodified};
84 my (@lines, @values, @order);
88 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
91 if (defined $opt{interval}) {
93 alarm $opt{interval} if $opt{interval} > 0;
96 require Tie::Array::Sorted;
97 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
98 } or warn $@, "Expect slowdown with large datasets!\n";
102 $SIG{INT} = 'DEFAULT'; # reset for subsequent attempts
104 'IGNORE' # continue after assumed eof
107 my $valmatch = qr/$opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)/x;
110 s/^\h*// unless $opt{unmodified};
111 push @values, s/$valmatch/\n/ && $1;
112 push @order, $1 if length $1;
113 if (defined $opt{trim} and defined $1) {
114 my $trimpos = abs $opt{trim};
115 $trimpos -= length $1 if $opt{unmodified};
117 $_ = substr $_, 0, 2;
119 elsif (length > $trimpos) {
120 substr($_, $trimpos - 1) = '…';
124 show_lines() if defined $opt{interval} and $opt{interval} < 0
125 and $. % $opt{interval} == 0;
128 $SIG{INT} = 'DEFAULT';
131 $opt{color} and defined $_[0] or return '';
132 return "\e[$_[0]m" if defined wantarray;
133 $_ = color(@_) . $_ . color(0) if defined;
138 state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
139 @lines and @lines > $nr or return;
141 @lines > $nr or return unless $opt{hidemin};
143 @order = sort { $b <=> $a } @order unless tied @order;
144 my $maxval = ($opt{hidemax} ? max grep { length } @values[0 .. $opt{hidemax} - 1] : $order[0]) // 0;
145 my $minval = min $order[-1] // (), 0;
146 my $lenval = $opt{'value-length'} // max map { length } @order;
147 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
148 max map { length $values[$_] && length $lines[$_] }
149 0 .. min $#lines, $opt{hidemax} || (); # left padding
150 my $size = ($maxval - $minval) &&
151 ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
154 if ($opt{markers} // 1 and $size > 0) {
155 my sub orderpos { (($order[$_[0]] + $order[$_[0] + .5]) / 2 - $minval) * $size }
156 $barmark[ (sum(@order) / @order - $minval) * $size ] = '='; # average
157 $barmark[ orderpos($#order * .31731) ] = '>';
158 $barmark[ orderpos($#order * .68269) ] = '<';
159 $barmark[ orderpos($#order / 2) ] = '+'; # mean
160 $barmark[ -$minval * $size ] = '|' if $minval < 0; # zero
161 color(36) for @barmark;
163 state $lastmax = $maxval;
164 if ($maxval > $lastmax) {
165 print ' ' x ($lenval + $len);
168 ($lastmax - $minval) * $size + .5,
169 '-' x (($values[$nr - 1] - $minval) * $size);
171 say '+' x (($maxval - $lastmax - $minval) * $size + .5);
177 @lines > $nr or return if $opt{hidemin};
180 my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
181 my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
183 $float && ($unit % 3) == ($unit < 0), # tenths
184 $_[0] / 1000 ** int($unit/3), # number
185 $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
189 while ($nr <= $#lines) {
190 $nr >= $opt{hidemax} and last if defined $opt{hidemax};
191 my $val = $values[$nr];
194 print $opt{spark}->[ ($val - $minval) / $maxval * $#{$opt{spark}} ];
199 my $color = !$opt{color} ? undef :
200 $val == $order[0] ? 32 : # max
201 $val == $order[-1] ? 31 : # min
203 $val = $opt{units} ? sival($val) : sprintf "%*s", $lenval, $val;
204 color($color) for $val;
206 my $line = $lines[$nr] =~ s/\n/$val/r;
207 printf '%-*s', $len + length($val), $line;
208 print $barmark[$_] // '-' for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
214 say '' if $opt{spark};
220 if ($opt{hidemin} or $opt{hidemax}) {
222 $opt{hidemax} ||= @lines;
223 printf '%s of ', sum(@values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
226 my $total = sum @order;
227 printf '%s total', color(1) . $total . color(0);
228 printf ' in %d values', scalar @values;
229 printf(' (%s min, %s avg, %s max)',
230 color(31) . $order[-1] . color(0),
231 color(36) . (sprintf '%*.*f', 0, 2, $total / @order) . color(0),
232 color(32) . $order[0] . color(0),
243 barcat - graph to visualize input values
247 B<barcat> [<options>] [<input>]
251 Visualizes relative sizes of values read from input (file(s) or STDIN).
252 Contents are concatenated similar to I<cat>,
253 but numbers are reformatted and a bar graph is appended to each line.
255 Don't worry, barcat does not drink and divide.
256 It can has various options for input and output (re)formatting,
257 but remains limited to one-dimensional charts.
258 For more complex graphing needs
259 you'll need a larger animal like I<gnuplot>.
265 =item -c, --[no-]color
267 Force colored output of values and bar markers.
268 Defaults on if output is a tty,
269 disabled otherwise such as when piped or redirected.
271 =item -f, --field=(<number>|<regexp>)
273 Compare values after a given number of whitespace separators,
274 or matching a regular expression.
276 Unspecified or I<-f0> means values are at the start of each line.
277 With I<-f1> the second word is taken instead.
278 A string can indicate the starting position of a value
279 (such as I<-f:> if preceded by colons),
280 or capture the numbers itself,
281 for example I<-f'(\d+)'> for the first digits anywhere.
283 =item -H, --human-readable
285 Format values using SI unit prefixes,
286 turning long numbers like I<12356789> into I<12.4M>.
287 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
288 Short integers are aligned but kept without decimal point.
290 =item -t, --interval[=(<seconds>|-<lines>)]
292 Output partial progress every given number of seconds or input lines.
293 An update can also be forced by sending a I<SIGALRM> alarm signal.
295 =item -l, --length=[-]<size>[%]
297 Trim line contents (between number and bars)
298 to a maximum number of characters.
299 The exceeding part is replaced by an abbreviation sign,
300 unless C<--length=0>.
302 Prepend a dash (i.e. make negative) to enforce padding
303 regardless of encountered contents.
305 =item -L, --limit=(<count>|<start>-[<end>])
307 Stop output after a number of lines.
308 All input is still counted and analyzed for statistics,
309 but disregarded for padding and bar size.
313 Statistical positions to indicate on bars.
314 Cannot be customized yet,
315 only disabled by providing an empty argument.
317 Any value enables all marker characters:
324 the sum of all values divided by the number of counted lines.
329 the middle value or average between middle values.
333 Standard deviation left of the mean.
334 Only 16% of all values are lower.
338 Standard deviation right of the mean.
339 The part between B<< <--> >> encompass all I<normal> results,
340 or 68% of all entries.
346 Total statistics after all data.
348 =item -u, --unmodified
350 Do not reformat values, keeping leading whitespace.
351 Keep original value alignment, which may be significant in some programs.
353 =item --value-length=<size>
355 Reserved space for numbers.
357 =item -w, --width=<columns>
359 Override the maximum number of columns to use.
360 Appended graphics will extend to fill up the entire screen.
364 Overview of available options.
381 seq 30 | awk '{print sin($1/10)}' | barcat
383 Compare file sizes (with human-readable numbers):
385 du -d0 -b * | barcat -H
387 Memory usage of user processes with long names truncated:
389 ps xo %mem,pid,cmd | barcat -l40
391 Monitor network latency from prefixed results:
393 ping google.com | barcat -f'time=\K' -t
395 Commonly used after counting, for example users on the current server:
397 users | sed 's/ /\n/g' | sort | uniq -c | barcat
399 Letter frequencies in text files:
401 cat /usr/share/games/fortunes/*.u8 |
402 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
403 sort | uniq -c | barcat
405 Number of HTTP requests per day:
407 cat log/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
409 Any kind of database query with counts, preserving returned alignment:
411 echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
414 Earthquakes worldwide magnitude 1+ in the last 24 hours:
416 https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
417 column -tns, | graph -f4 -u -l80%
419 External datasets, like movies per year:
421 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
422 perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
424 But please get I<jq> to process JSON
425 and replace the manual selection by C<< jq '.[].year' >>.
427 Pokémon height comparison:
429 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
430 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
432 USD/EUR exchange rate from CSV provided by the ECB:
434 curl https://sdw.ecb.europa.eu/export.do \
435 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
436 grep '^[12]' | barcat -f',\K' --value-length=7
438 Total population history from the World Bank dataset (XML):
439 External datasets, like total population in XML from the World Bank:
441 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
442 xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
443 sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
445 And of course various Git statistics, such commit count by year:
447 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
449 Or the top 3 most frequent authors with statistics over all:
451 git shortlog -sn | barcat -L3 -s
453 Activity of the last days (substitute date C<-v-{}d> on BSD):
455 ( git log --pretty=%ci --since=30day | cut -b-10
456 seq 0 30 | xargs -i date +%F -d-{}day ) |
457 sort | uniq -c | awk '$1--' | graph --spark
461 Mischa POSLAWSKY <perl@shiar.org>