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