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"
46 my ($optname, $optval) = @_;
48 $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand
51 $optval =~ m/\A (?: (-? [0-9]+)? - )? (-? [0-9]+)? \z/ or die(
52 "Value \"$optval\" invalid for option limit",
57 s/\A-0*\z// and $_ ||= undef for $end // ();
62 return max(0, $lines + $start + 2);
67 my ($limit, $offset) = @_;
69 return $offset - $end - 1; # count
72 return $limit - $end + 1; # bottom
74 elsif ($end <= $limit) {
75 return $end - 1; # less
83 'graph-format=s' => sub {
84 $opt{'graph-format'} = substr $_[1], 0, 1;
91 fire => [qw( 90 31 91 33 93 97 96 )],
92 fire256=> [map {"38;5;$_"} qw(
94 202 208 214 220 226 227 228 229 230 231 159
96 whites => [qw( 1;30 0;37 1;37 )],
97 greys => [map {"38;5;$_"} 0, 232..255, 15],
98 random => [map {"38;5;$_"} List::Util::shuffle(17..231)],
99 rainbow=> [map {"38;5;$_"}
101 (map { 196 + $_*6 } 0..4), # +g
102 (map { 226 - $_*6*6 } 0..4), # -r
103 (map { 46 + $_ } 0..4), # +b
104 (map { 51 - $_*6 } 0..4), # -g
105 (map { 21 + $_*6*6 } 0..4), # +r
106 (map { 201 - $_ } 0..4), # -b
110 my @vals = split /[^0-9;]/, $_[1]
111 or die "Empty palette resulting from \"$_[1]\"\n";
121 my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ';
122 say "barcat $mascot version $VERSION";
126 /^=/ ? last : print for readline *DATA; # text between __END__ and pod
131 Pod::Usage::pod2usage(
132 -exitval => 0, -perldocopt => '-oman', -verbose => 2,
135 ) or exit 64; # EX_USAGE
138 $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark};
139 $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty
140 $opt{'graph-format'} //= '-';
141 $opt{trim} *= $opt{width} / 100 if $opt{trimpct};
142 $opt{units} = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
143 if $opt{'human-readable'};
144 $opt{anchor} //= qr/\A/;
145 $opt{'value-length'} = 4 if $opt{units};
146 $opt{'value-length'} = 1 if $opt{unmodified};
147 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
148 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
149 $opt{report} //= join(', ',
150 '${min; color(31)} min',
151 '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg',
152 '${max; color(32)} max',
154 $opt{palette} //= $opt{color} && [31, 90, 32];
155 $opt{indicators} = [split //, $opt{indicators} ||
156 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
157 ] if defined $opt{indicators} or $opt{spark};
158 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
159 and undef $opt{interval};
161 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
162 $opt{'value-format'} = $opt{sexagesimal} ? sub {
163 my $s = abs($_[0]) + .5;
164 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
165 } : $opt{units} && sub {
167 log(abs $_[0] || 1) / log(10)
168 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
169 + 1e-15 # float imprecision
171 my $decimal = ($unit % 3) == ($unit < 0);
172 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
173 $decimal = ($unit % 3) == ($unit < 0);
174 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
176 3 + ($_[0] < 0), # digits plus optional negative sign
178 $_[0] / 1000 ** int($unit/3), # number
179 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
180 $opt{units}->[$unit/3] # suffix
182 } and $opt{reformat}++;
183 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
186 my (@lines, @values, @order);
188 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
191 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
193 $SIG{INT} = \&show_exit;
195 if (defined $opt{interval}) {
196 $opt{interval} ||= 1;
197 alarm $opt{interval} if $opt{interval} > 0;
200 require Tie::Array::Sorted;
201 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
202 } or warn $@, "Expect slowdown with large datasets!\n";
205 my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish
206 my $valmatch = qr< $opt{anchor} ( \h* -? $float |) >x;
207 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
209 s/\A\h*// unless $opt{unmodified};
210 my $valnum = s/$valmatch/\n/ && $1;
211 push @values, $valnum;
212 push @order, $valnum if length $valnum;
213 if (defined $opt{trim} and defined $valnum) {
214 my $trimpos = abs $opt{trim};
215 $trimpos -= length $valnum if $opt{unmodified};
217 $_ = substr $_, 0, 2;
219 elsif (length > $trimpos) {
220 # cut and replace (intentional lvalue for speed, contrary to PBP)
221 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
225 show_lines() if defined $opt{interval} and $opt{interval} < 0
226 and $. % $opt{interval} == 0;
229 $SIG{INT} = 'DEFAULT';
232 $opt{color} and defined $_[0] or return '';
233 return "\e[$_[0]m" if defined wantarray;
234 $_ = color(@_) . $_ . color(0) if defined;
239 state $nr = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0;
240 @lines > $nr or return;
242 my $limit = $opt{hidemax} ? $opt{hidemax}->($#lines, $nr) : $#lines;
244 @order = sort { $b <=> $a } @order unless tied @order;
245 my $maxval = $opt{maxval} // (
246 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
249 my $minval = $opt{minval} // min $order[-1] // (), 0;
250 my $range = $maxval - $minval;
251 $range &&= log $range if $opt{log};
252 my $lenval = $opt{'value-length'} // max map { length } @order;
253 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
254 max(map { length $values[$_] && length $lines[$_] } $nr .. $limit)
256 my $size = defined $opt{width} && $range &&
257 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
260 if ($opt{markers} and $size > 0) {
261 for my $markspec (split /\h/, $opt{markers}) {
262 my ($char, $func) = split //, $markspec, 2;
264 if ($func eq 'avg') {
265 return sum(@order) / @order;
267 elsif ($func =~ /\A([0-9.]+)v\z/) {
269 "Invalid marker $char: percentile $1 out of bounds\n"
271 my $index = $#order * $1 / 100;
272 return ($order[$index] + $order[$index + .5]) / 2;
274 elsif ($func =~ /\A-?[0-9.]+\z/) {
277 elsif ($func =~ /\A\/($float)\z/) {
278 my @range = my $multiple = my $next = $1;
279 while ($next < $maxval) {
280 $multiple *= 10 if $opt{log};
281 push @range, $next += $multiple;
286 die "Unknown marker $char: $func\n";
295 $pos &&= log $pos if $opt{log};
297 color(36) for $barmark[$pos / $range * $size] = $char;
301 state $lastmax = $maxval;
302 if ($maxval > $lastmax) {
303 print ' ' x ($lenval + $len);
306 ($lastmax - $minval) * $size / $range + .5,
307 '-' x (($values[$nr - 1] - $minval) * $size / $range);
309 say '+' x (($range - $lastmax) * $size / $range + .5);
316 color(31), sprintf('%*s', $lenval, $minval),
317 color(90), '-', color(36), '+',
318 color(32), sprintf('%*s', $size - 3, $maxval),
319 color(90), '-', color(36), '+',
323 while ($nr <= $limit) {
324 my $val = $values[$nr];
327 $rel = $val - $minval;
328 $rel &&= log $rel if $opt{log};
329 $rel = min(1, $rel / $range) if $range; # 0..1
331 my $color = !length $val || !$opt{palette} ? undef :
332 $val == $order[0] ? $opt{palette}->[-1] : # max
333 $val == $order[-1] ? $opt{palette}->[0] : # min
334 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
335 my $indicator = $opt{indicators} && $opt{indicators}->[
336 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
337 $#{$opt{indicators}} < 2 ? 1 :
338 $val >= $order[0] ? -1 :
339 $rel * ($#{$opt{indicators}} - 1e-14) + 1
343 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
344 print color($color), $_ for $indicator;
347 print $indicator if defined $indicator;
350 $val = sprintf("%*s", $lenval,
351 $opt{reformat} ? $opt{'value-format'}->($val) : $val
353 color($color) for $val;
355 my $line = $lines[$nr] =~ s/\n/$val/r;
356 if (not length $val) {
360 printf '%-*s', $len + length($val), $line;
361 if ($rel and $size) {
362 print $barmark[$_] // $opt{'graph-format'}
363 for 1 .. $rel * $size + .5;
370 say $opt{palette} ? color(0) : '' if $opt{spark};
376 if ($opt{hidemin} or $opt{hidemax}) {
377 my $linemin = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0;
378 my $linemax = $opt{hidemax} ? $opt{hidemax}->($#lines, $linemin) : $#lines;
379 print varfmt('${sum+} of ', {
380 lines => $linemax - $linemin + 1,
381 sum => sum(0, grep {length} @values[$linemin .. $linemax]),
382 }) if $linemin <= $linemax;
385 my $total = sum @order;
386 my $fmt = '${sum+;color(1)} total in ${count#} values';
387 $fmt .= ' over ${lines#} lines' if @order != @lines;
388 $fmt .= " ($_)" for $opt{report} || ();
395 avg => $total / @order,
403 my ($fmt, $vars) = @_;
404 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
405 my ($name, $cmd) = split /\s*;/, $1, 2;
406 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
407 local $_ = $vars->{$name};
409 $_ = $opt{'value-format'}->($_) if $format;
412 warn "Error in \$$name report: $@" if $@;
417 warn "Unknown variable \$$name in report\n";
426 show_stat() if $opt{stat};
427 exit 130 if @_; # 0x80+signo
435 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
438 -a, --[no-]ascii Restrict user interface to ASCII characters
439 -C, --[no-]color Force colored output of values and bar markers
440 -f, --field=([+]N|REGEXP)
441 Compare values after a given number of whitespace
443 --header Prepend a chart axis with minimum and maximum
445 -H, --human-readable Format values using SI unit prefixes
446 --sexagesimal Convert seconds to HH:MM:SS time format
447 -t, --interval[=(N|-LINES)]
448 Output partial progress every given number of
449 seconds or input lines
450 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
451 -L, --limit=[N|[-]START(-[END]|+N)]
452 Select a range of lines to display
453 -e, --log Logarithmic (exponential) scale instead of linear
454 --graph-format=CHAR Glyph to repeat for the graph line
455 -m, --markers=FORMAT Statistical positions to indicate on bars
456 --min=N, --max=N Bars extend from 0 or the minimum value if lower
457 --palette=(PRESET|COLORS)
458 Override colors of parsed numbers
459 -_, --spark Replace lines by sparklines
460 --indicators[=CHARS] Prefix a unicode character corresponding to each
462 -s, --stat Total statistics after all data
463 -u, --unmodified Do not reformat values, keeping leading whitespace
464 --value-length=SIZE Reserved space for numbers
465 -w, --width=COLUMNS Override the maximum number of columns to use
466 -h, --usage Overview of available options
467 --help Full pod documentation
468 -V, --version Version information
474 barcat - concatenate texts with graph to visualize values
478 B<barcat> [<options>] [<file>... | <numbers>]
482 Visualizes relative sizes of values read from input
483 (parameters, file(s) or STDIN).
484 Contents are concatenated similar to I<cat>,
485 but numbers are reformatted and a bar graph is appended to each line.
487 Don't worry, barcat does not drink and divide.
488 It can has various options for input and output (re)formatting,
489 but remains limited to one-dimensional charts.
490 For more complex graphing needs
491 you'll need a larger animal like I<gnuplot>.
497 =item -a, --[no-]ascii
499 Restrict user interface to ASCII characters,
500 replacing default UTF-8 by their closest approximation.
501 Input is always interpreted as UTF-8 and shown as is.
503 =item -C, --[no-]color
505 Force colored output of values and bar markers.
506 Defaults on if output is a tty,
507 disabled otherwise such as when piped or redirected.
508 Can also be disabled by setting I<-M>
509 or the I<NO_COLOR> environment variable.
511 =item -f, --field=([+]<number> | <regexp>)
513 Compare values after a given number of whitespace separators,
514 or matching a regular expression.
516 Unspecified or I<-f0> means values are at the start of each line.
517 With I<-f1> the second word is taken instead.
518 A string can indicate the starting position of a value
519 (such as I<-f:> if preceded by colons),
520 or capture the numbers itself,
521 for example I<-f'(\d+)'> for the first digits anywhere.
522 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
526 Prepend a chart axis with minimum and maximum values labeled.
528 =item -H, --human-readable
530 Format values using SI unit prefixes,
531 turning long numbers like I<12356789> into I<12.4M>.
532 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
533 Short integers are aligned but kept without decimal point.
537 Convert seconds to HH:MM:SS time format.
539 =item -t, --interval[=(<seconds> | -<lines>)]
541 Output partial progress every given number of seconds or input lines.
542 An update can also be forced by sending a I<SIGALRM> alarm signal.
544 =item -l, --length=[-]<size>[%]
546 Trim line contents (between number and bars)
547 to a maximum number of characters.
548 The exceeding part is replaced by an abbreviation sign,
549 unless C<--length=0>.
551 Prepend a dash (i.e. make negative) to enforce padding
552 regardless of encountered contents.
554 =item -L, --limit=[<count> | [-]<start>(-[<end>] | +<count>)]
556 Select a range of lines to display.
557 A single integer indicates the last line number (like C<head>),
558 or first line counting from the bottom if negative (like C<tail>).
560 A range consists of a starting line number followed by either
561 a dash C<-> to an optional end, or plus sign C<+> with count.
563 All hidden input is still counted and analyzed for statistics,
564 but disregarded for padding and bar size.
568 Logarithmic (I<e>xponential) scale instead of linear
569 to compare orders of magnitude.
571 =item --graph-format=<character>
573 Glyph to repeat for the graph line.
574 Defaults to a dash C<->.
576 =item -m, --markers=<format>
578 Statistical positions to indicate on bars.
579 A single indicator glyph precedes each position:
585 Exact value to match on the axis.
586 A vertical bar at the zero crossing is displayed by I<|0>
588 For example I<π3.14> would locate pi.
592 Repeated at every multiple of a number.
593 For example I<:/1> for a grid at every integer.
595 =item <percentage>I<v>
597 Ranked value at the given percentile.
598 The default shows I<+> at I<50v> for the mean or median;
599 the middle value or average between middle values.
600 One standard deviation right of the mean is at about I<68.3v>.
601 The default includes I<< >31.73v <68.27v >>
602 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
607 the sum of all values divided by the number of counted lines.
608 Indicated by default as I<=>.
612 =item --min=<number>, --max=<number>
614 Bars extend from 0 or the minimum value if lower,
615 to the largest value encountered.
616 These options can be set to customize this range.
618 =item --palette=(<preset> | <color>...)
620 Override colors of parsed numbers.
621 Can be any CSI escape, such as I<90> for default dark grey,
622 or alternatively I<1;30> for bright black.
624 In case of additional colors,
625 the last is used for values equal to the maximum, the first for minima.
626 If unspecified, these are green and red respectively (I<31 90 32>).
627 Multiple intermediate colors will be distributed
628 relative to the size of values.
630 Predefined color schemes are named I<whites> and I<fire>,
631 or I<greys> and I<fire256> for 256-color variants.
635 Replace lines by I<sparklines>,
636 single characters (configured by C<--indicators>)
637 corresponding to input values.
639 =item --indicators[=<characters>]
641 Prefix a unicode character corresponding to each value.
642 The first specified character will be used for non-values,
643 the remaining sequence will be distributed over the range of values.
644 Unspecified, block fill glyphs U+2581-2588 will be used.
648 Total statistics after all data.
650 =item -u, --unmodified
652 Do not reformat values, keeping leading whitespace.
653 Keep original value alignment, which may be significant in some programs.
655 =item --value-length=<size>
657 Reserved space for numbers.
659 =item -w, --width=<columns>
661 Override the maximum number of columns to use.
662 Appended graphics will extend to fill up the entire screen,
663 otherwise determined by the environment variable I<COLUMNS>
664 or by running the C<tput> command.
668 Overview of available options.
672 Full pod documentation
673 as rendered by perldoc.
685 seq 30 | awk '{print sin($1/10)}' | barcat
687 Compare file sizes (with human-readable numbers):
689 du -d0 -b * | barcat -H
691 Same from formatted results, selecting the first numeric value:
693 tree -s --noreport | barcat -H -f+
695 Compare media metadata, like image size or play time:
697 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
699 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
701 find -type f -print0 | xargs -0 -L1 \
702 ffprobe -show_format -of json -v error |
703 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
705 Memory usage of user processes with long names truncated:
707 ps xo rss,pid,cmd | barcat -l40
709 Monitor network latency from prefixed results:
711 ping google.com | barcat -f'time=\K' -t
713 Commonly used after counting, for example users on the current server:
715 users | tr ' ' '\n' | sort | uniq -c | barcat
717 Letter frequencies in text files:
719 cat /usr/share/games/fortunes/*.u8 |
720 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
721 sort | uniq -c | barcat
723 Number of HTTP requests per day:
725 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
727 Any kind of database query results, preserving returned alignment:
729 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
732 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
734 > SELECT schemaname, relname, pg_total_relation_size(relid)
735 FROM pg_statio_user_tables ORDER BY idx_blks_hit
738 Same thing in SQLite (requires the sqlite3 client):
741 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
743 Earthquakes worldwide magnitude 1+ in the last 24 hours:
745 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
746 column -ts, -n | barcat -f4 -u -l80%
748 External datasets, like movies per year:
750 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
751 jq .[].year | uniq -c | barcat
753 Pokémon height comparison:
755 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
756 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
758 USD/EUR exchange rate from CSV provided by the ECB:
760 curl https://sdw.ecb.europa.eu/export.do \
761 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
762 barcat -f',\K' --value-length=7
764 Total population history in XML from the World Bank:
766 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
767 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
768 barcat -f1 -H --markers=+/1e9
770 Population and other information for all countries:
772 curl http://download.geonames.org/export/dump/countryInfo.txt |
773 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
775 And of course various Git statistics, such commit count by year:
777 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
779 Or the top 3 most frequent authors with statistics over all:
781 git shortlog -sn | barcat -L3 -s
783 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
785 ( git log --pretty=%ci --since=30day | cut -b-10
786 seq 0 30 | xargs -i date +%F -d-{}day ) |
787 sort | uniq -c | awk '$1--' | barcat --spark
789 Sparkline graphics of simple input given as inline parameters:
791 barcat -_ 3 1 4 1 5 0 9 2 4
793 Misusing the spark functionality to draw a lolcat line:
795 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
799 Mischa POSLAWSKY <perl@shiar.org>