5 use List::Util qw( min max sum );
6 use open qw( :std :utf8 );
14 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
18 'M' => sub { $opt{color} = 0 },
22 s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/;
24 (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)'
26 $opt{anchor} = qr/$_/;
27 } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
33 'trim|length|l=s' => sub {
34 my ($optname, $optval) = @_;
35 $optval =~ s/%$// and $opt{trimpct}++;
36 $optval =~ m/\A-?[0-9]+\z/ or die(
37 "Value \"$optval\" invalid for option $optname",
38 " (number or percentage expected)\n"
48 my ($optname, $optval) = @_;
50 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
51 ($opt{hidemin}, $opt{hidemax}) =
52 $optval =~ m/\A (?: (-? [0-9]+)? - )? ([0-9]+)? \z/ or die(
53 "Value \"$optval\" invalid for option limit",
60 'graph-format=s' => sub {
61 $opt{'graph-format'} = substr $_[1], 0, 1;
68 fire => [qw( 90 31 91 33 93 97 96 )],
69 fire256=> [map {"38;5;$_"} qw(
71 202 208 214 220 226 227 228 229 230 231 159
73 whites => [qw( 1;30 0;37 1;37 )],
74 greys => [map {"38;5;$_"} 0, 232..255, 15],
75 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
76 rainbow=> [map {"38;5;$_"}
78 (map { 196 + $_*6 } 0..4), # +g
79 (map { 226 - $_*6*6 } 0..4), # -r
80 (map { 46 + $_ } 0..4), # +b
81 (map { 51 - $_*6 } 0..4), # -g
82 (map { 21 + $_*6*6 } 0..4), # +r
83 (map { 201 - $_ } 0..4), # -b
87 my @vals = split /[^0-9;]/, $_[1]
88 or die "Empty palette resulting from \"$_[1]\"\n";
98 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
99 say "barcat $mascot version $VERSION";
103 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
108 Pod::Usage::pod2usage(
109 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
112 ) or exit 64; # EX_USAGE
115 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
116 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
117 $opt{'graph-format'} //= '-';
118 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
119 $opt{units} = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
120 if $opt{'human-readable'};
121 $opt{anchor} //= qr/\A/;
122 $opt{'value-length'} = 4 if $opt{units};
123 $opt{'value-length'} = 1 if $opt{unmodified};
124 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
125 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
126 $opt{report} //= join(', ',
127 '${min; color(31)} min',
128 '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg',
129 '${max; color(32)} max',
131 $opt{palette} //= $opt{color} && [31, 90, 32];
132 $opt{indicators} = [split //, $opt{indicators} ||
133 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
134 ] if defined $opt{indicators} or $opt{spark};
135 $opt{hidemin} = ($opt{hidemin} || 1) - 1;
136 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
137 and undef $opt{interval};
139 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
140 $opt{'value-format'} = $opt{sexagesimal} ? sub {
141 my $s = abs($_[0]) + .5;
142 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
143 } : $opt{units} && sub {
145 log(abs $_[0] || 1) / log(10)
146 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
147 + 1e-15 # float imprecision
149 my $decimal = ($unit % 3) == ($unit < 0);
150 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
151 $decimal = ($unit % 3) == ($unit < 0);
152 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
154 3 + ($_[0] < 0), # digits plus optional negative sign
156 $_[0] / 1000 ** int($unit/3), # number
157 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
158 $opt{units}->[$unit/3] # suffix
160 } and $opt{reformat}++;
161 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
164 my (@lines, @values, @order);
166 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
169 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
171 $SIG{INT} = \&show_exit;
173 if (defined $opt{interval}) {
174 $opt{interval} ||= 1;
175 alarm $opt{interval} if $opt{interval} > 0;
178 require Tie::Array::Sorted;
179 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
180 } or warn $@, "Expect slowdown with large datasets!\n";
184 $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
186 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
188 s/\A\h*// unless $opt{unmodified};
189 my $valnum = s/$valmatch/\n/ && $1;
190 push @values, $valnum;
191 push @order, $valnum if length $valnum;
192 if (defined $opt{trim} and defined $valnum) {
193 my $trimpos = abs $opt{trim};
194 $trimpos -= length $valnum if $opt{unmodified};
196 $_ = substr $_, 0, 2;
198 elsif (length > $trimpos) {
199 # cut and replace (intentional lvalue for speed, contrary to PBP)
200 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
204 show_lines() if defined $opt{interval} and $opt{interval} < 0
205 and $. % $opt{interval} == 0;
208 $SIG{INT} = 'DEFAULT';
211 $opt{color} and defined $_[0] or return '';
212 return "\e[$_[0]m" if defined wantarray;
213 $_ = color(@_) . $_ . color(0) if defined;
219 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
221 @lines > $nr or return;
224 if (defined $opt{hidemax}) {
225 if ($opt{hidemin} and $opt{hidemin} < 0) {
226 $limit -= $opt{hidemax} - 1;
228 elsif ($opt{hidemax} <= $limit) {
229 $limit = $opt{hidemax} - 1;
233 @order = sort { $b <=> $a } @order unless tied @order;
234 my $maxval = $opt{maxval} // (
235 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
238 my $minval = $opt{minval} // min $order[-1] // (), 0;
239 my $range = $maxval - $minval;
240 $range &&= log $range if $opt{log};
241 my $lenval = $opt{'value-length'} // max map { length } @order;
242 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
243 max map { length $values[$_] && length $lines[$_] }
244 0 .. min $#lines, $opt{hidemax} || (); # left padding
245 my $size = defined $opt{width} && $range &&
246 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
249 if ($opt{markers} and $size > 0) {
250 for my $markspec (split /\h/, $opt{markers}) {
251 my ($char, $func) = split //, $markspec, 2;
253 if ($func eq 'avg') {
254 return sum(@order) / @order;
256 elsif ($func =~ /\A([0-9.]+)v\z/) {
258 "Invalid marker $char: percentile $1 out of bounds\n"
260 my $index = $#order * $1 / 100;
261 return ($order[$index] + $order[$index + .5]) / 2;
263 elsif ($func =~ /\A-?[0-9.]+\z/) {
267 die "Unknown marker $char: $func\n";
275 $pos &&= log $pos if $opt{log};
277 color(36) for $barmark[$pos / $range * $size] = $char;
280 state $lastmax = $maxval;
281 if ($maxval > $lastmax) {
282 print ' ' x ($lenval + $len);
285 ($lastmax - $minval) * $size / $range + .5,
286 '-' x (($values[$nr - 1] - $minval) * $size / $range);
288 say '+' x (($range - $lastmax) * $size / $range + .5);
295 color(31), sprintf('%*s', $lenval, $minval),
296 color(90), '-', color(36), '+',
297 color(32), sprintf('%*s', $size - 3, $maxval),
298 color(90), '-', color(36), '+',
302 while ($nr <= $limit) {
303 my $val = $values[$nr];
306 $rel = $val - $minval;
307 $rel &&= log $rel if $opt{log};
308 $rel = min(1, $rel / $range) if $range; # 0..1
310 my $color = !length $val || !$opt{palette} ? undef :
311 $val == $order[0] ? $opt{palette}->[-1] : # max
312 $val == $order[-1] ? $opt{palette}->[0] : # min
313 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
314 my $indicator = $opt{indicators} && $opt{indicators}->[
315 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
316 $#{$opt{indicators}} < 2 ? 1 :
317 $val >= $order[0] ? -1 :
318 $rel * ($#{$opt{indicators}} - 1e-14) + 1
322 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
323 print color($color), $_ for $indicator;
326 print $indicator if defined $indicator;
329 $val = sprintf("%*s", $lenval,
330 $opt{reformat} ? $opt{'value-format'}->($val) : $val
332 color($color) for $val;
334 my $line = $lines[$nr] =~ s/\n/$val/r;
335 if (not length $val) {
339 printf '%-*s', $len + length($val), $line;
340 if ($rel and $size) {
341 print $barmark[$_] // $opt{'graph-format'}
342 for 1 .. $rel * $size + .5;
349 say $opt{palette} ? color(0) : '' if $opt{spark};
355 if ($opt{hidemin} or $opt{hidemax}) {
356 my $linemin = $opt{hidemin};
357 my $linemax = ($opt{hidemax} || @lines) - 1;
360 $linemax = @lines - $linemax;
362 printf '%.8g of ', $opt{'value-format'}->(
363 sum(grep {length} @values[$linemin .. $linemax]) // 0
367 my $total = sum @order;
368 my $fmt = '${sum;color(1)} total in ${count} values';
369 $fmt .= ' over ${lines} lines' if @order != @lines;
370 $fmt .= " ($_)" for $opt{report} || ();
377 avg => $total / @order,
385 my ($fmt, $vars) = @_;
386 $fmt =~ s[\$\{( (?: [^{}]++ | \{(?1)\} )+ )\}]{
387 my ($name, $cmd) = split /\s*;/, $1, 2;
388 local $_ = $vars->{$name};
390 $_ = $opt{'value-format'}->($_) if $opt{reformat};
393 warn "Error in \$$name report: $@" if $@;
398 warn "Unknown variable \$$name in report\n";
407 show_stat() if $opt{stat};
408 exit 130 if @_; # 0x80+signo
416 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
419 -a, --[no-]ascii Restrict user interface to ASCII characters
420 -C, --[no-]color Force colored output of values and bar markers
421 -f, --field=([+]N|REGEXP)
422 Compare values after a given number of whitespace
424 --header Prepend a chart axis with minimum and maximum
426 -H, --human-readable Format values using SI unit prefixes
427 --sexagesimal Convert seconds to HH:MM:SS time format
428 -t, --interval[=(N|-LINES)]
429 Output partial progress every given number of
430 seconds or input lines
431 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
432 -L, --limit[=(N|-LAST|START-[END])]
433 Stop output after a number of lines
434 -e, --log Logarithmic (exponential) scale instead of linear
435 --graph-format=CHAR Glyph to repeat for the graph line
436 -m, --markers=FORMAT Statistical positions to indicate on bars
437 --min=N, --max=N Bars extend from 0 or the minimum value if lower
438 --palette=(PRESET|COLORS)
439 Override colors of parsed numbers
440 -_, --spark Replace lines by sparklines
441 --indicators[=CHARS] Prefix a unicode character corresponding to each
443 -s, --stat Total statistics after all data
444 -u, --unmodified Do not reformat values, keeping leading whitespace
445 --value-length=SIZE Reserved space for numbers
446 -w, --width=COLUMNS Override the maximum number of columns to use
447 -h, --usage Overview of available options
448 --help Full pod documentation
449 -V, --version Version information
455 barcat - concatenate texts with graph to visualize values
459 B<barcat> [<options>] [<file>... | <numbers>]
463 Visualizes relative sizes of values read from input
464 (parameters, file(s) or STDIN).
465 Contents are concatenated similar to I<cat>,
466 but numbers are reformatted and a bar graph is appended to each line.
468 Don't worry, barcat does not drink and divide.
469 It can has various options for input and output (re)formatting,
470 but remains limited to one-dimensional charts.
471 For more complex graphing needs
472 you'll need a larger animal like I<gnuplot>.
478 =item -a, --[no-]ascii
480 Restrict user interface to ASCII characters,
481 replacing default UTF-8 by their closest approximation.
482 Input is always interpreted as UTF-8 and shown as is.
484 =item -C, --[no-]color
486 Force colored output of values and bar markers.
487 Defaults on if output is a tty,
488 disabled otherwise such as when piped or redirected.
489 Can also be disabled by setting I<-M>
490 or the I<NO_COLOR> environment variable.
492 =item -f, --field=([+]<number> | <regexp>)
494 Compare values after a given number of whitespace separators,
495 or matching a regular expression.
497 Unspecified or I<-f0> means values are at the start of each line.
498 With I<-f1> the second word is taken instead.
499 A string can indicate the starting position of a value
500 (such as I<-f:> if preceded by colons),
501 or capture the numbers itself,
502 for example I<-f'(\d+)'> for the first digits anywhere.
503 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
507 Prepend a chart axis with minimum and maximum values labeled.
509 =item -H, --human-readable
511 Format values using SI unit prefixes,
512 turning long numbers like I<12356789> into I<12.4M>.
513 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
514 Short integers are aligned but kept without decimal point.
518 Convert seconds to HH:MM:SS time format.
520 =item -t, --interval[=(<seconds> | -<lines>)]
522 Output partial progress every given number of seconds or input lines.
523 An update can also be forced by sending a I<SIGALRM> alarm signal.
525 =item -l, --length=[-]<size>[%]
527 Trim line contents (between number and bars)
528 to a maximum number of characters.
529 The exceeding part is replaced by an abbreviation sign,
530 unless C<--length=0>.
532 Prepend a dash (i.e. make negative) to enforce padding
533 regardless of encountered contents.
535 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
537 Stop output after a number of lines.
538 A single value indicates the last line number (like C<head>),
539 or first line counting from the bottom if negative (like C<tail>).
540 A specific range can be given by two values.
542 All input is still counted and analyzed for statistics,
543 but disregarded for padding and bar size.
547 Logarithmic (I<e>xponential) scale instead of linear
548 to compare orders of magnitude.
550 =item --graph-format=<character>
552 Glyph to repeat for the graph line.
553 Defaults to a dash C<->.
555 =item -m, --markers=<format>
557 Statistical positions to indicate on bars.
558 A single indicator glyph precedes each position:
564 Exact value to match on the axis.
565 A vertical bar at the zero crossing is displayed by I<|0>
567 For example I<:3.14> would show a colon at pi.
569 =item <percentage>I<v>
571 Ranked value at the given percentile.
572 The default shows I<+> at I<50v> for the mean or median;
573 the middle value or average between middle values.
574 One standard deviation right of the mean is at about I<68.3v>.
575 The default includes I<< >31.73v <68.27v >>
576 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
581 the sum of all values divided by the number of counted lines.
582 Indicated by default as I<=>.
586 =item --min=<number>, --max=<number>
588 Bars extend from 0 or the minimum value if lower,
589 to the largest value encountered.
590 These options can be set to customize this range.
592 =item --palette=(<preset> | <color>...)
594 Override colors of parsed numbers.
595 Can be any CSI escape, such as I<90> for default dark grey,
596 or alternatively I<1;30> for bright black.
598 In case of additional colors,
599 the last is used for values equal to the maximum, the first for minima.
600 If unspecified, these are green and red respectively (I<31 90 32>).
601 Multiple intermediate colors will be distributed
602 relative to the size of values.
604 Predefined color schemes are named I<whites> and I<fire>,
605 or I<greys> and I<fire256> for 256-color variants.
609 Replace lines by I<sparklines>,
610 single characters (configured by C<--indicators>)
611 corresponding to input values.
613 =item --indicators[=<characters>]
615 Prefix a unicode character corresponding to each value.
616 The first specified character will be used for non-values,
617 the remaining sequence will be distributed over the range of values.
618 Unspecified, block fill glyphs U+2581-2588 will be used.
622 Total statistics after all data.
624 =item -u, --unmodified
626 Do not reformat values, keeping leading whitespace.
627 Keep original value alignment, which may be significant in some programs.
629 =item --value-length=<size>
631 Reserved space for numbers.
633 =item -w, --width=<columns>
635 Override the maximum number of columns to use.
636 Appended graphics will extend to fill up the entire screen,
637 otherwise determined by the environment variable I<COLUMNS>
638 or by running the C<tput> command.
642 Overview of available options.
646 Full pod documentation
647 as rendered by perldoc.
659 seq 30 | awk '{print sin($1/10)}' | barcat
661 Compare file sizes (with human-readable numbers):
663 du -d0 -b * | barcat -H
665 Same from formatted results, selecting the first numeric value:
667 tree -s --noreport | barcat -H -f+
669 Compare media metadata, like image size or play time:
671 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
673 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
675 find -type f -print0 | xargs -0 -L1 \
676 ffprobe -show_format -of json -v error |
677 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
679 Memory usage of user processes with long names truncated:
681 ps xo rss,pid,cmd | barcat -l40
683 Monitor network latency from prefixed results:
685 ping google.com | barcat -f'time=\K' -t
687 Commonly used after counting, for example users on the current server:
689 users | tr ' ' '\n' | sort | uniq -c | barcat
691 Letter frequencies in text files:
693 cat /usr/share/games/fortunes/*.u8 |
694 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
695 sort | uniq -c | barcat
697 Number of HTTP requests per day:
699 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
701 Any kind of database query results, preserving returned alignment:
703 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
706 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
708 > SELECT schemaname, relname, pg_total_relation_size(relid)
709 FROM pg_statio_user_tables ORDER BY idx_blks_hit
712 Same thing in SQLite (requires the sqlite3 client):
715 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
717 Earthquakes worldwide magnitude 1+ in the last 24 hours:
719 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
720 column -ts, -n | barcat -f4 -u -l80%
722 External datasets, like movies per year:
724 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
725 jq .[].year | uniq -c | barcat
727 Pokémon height comparison:
729 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
730 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
732 USD/EUR exchange rate from CSV provided by the ECB:
734 curl https://sdw.ecb.europa.eu/export.do \
735 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
736 barcat -f',\K' --value-length=7
738 Total population history in XML from the World Bank:
740 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
741 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
744 Population and other information for all countries:
746 curl http://download.geonames.org/export/dump/countryInfo.txt |
747 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
749 And of course various Git statistics, such commit count by year:
751 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
753 Or the top 3 most frequent authors with statistics over all:
755 git shortlog -sn | barcat -L3 -s
757 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
759 ( git log --pretty=%ci --since=30day | cut -b-10
760 seq 0 30 | xargs -i date +%F -d-{}day ) |
761 sort | uniq -c | awk '$1--' | barcat --spark
763 Sparkline graphics of simple input given as inline parameters:
765 barcat -_ 3 1 4 1 5 0 9 2 4
767 Misusing the spark functionality to draw a lolcat line:
769 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
773 Mischa POSLAWSKY <perl@shiar.org>