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 '${partsum+; $_ .= " of "}',
151 '${sum+; color(1); $_ .= " total in "}',
153 '${lines#; $_ = $_ != @order && " over $_ lines"}',
154 sprintf(' (%s)', join ', ',
155 '${min; color(31)} min',
156 '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg',
157 '${max; color(32)} max',
160 $opt{palette} //= $opt{color} && [31, 90, 32];
161 $opt{indicators} = [split //, $opt{indicators} ||
162 ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█')
163 ] if defined $opt{indicators} or $opt{spark};
164 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
165 and undef $opt{interval};
167 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
168 $opt{'value-format'} = $opt{sexagesimal} ? sub {
169 my $s = abs($_[0]) + .5;
170 sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
171 } : $opt{units} && sub {
173 log(abs $_[0] || 1) / log(10)
174 - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1
175 + 1e-15 # float imprecision
177 my $decimal = ($unit % 3) == ($unit < 0);
178 $unit -= log($decimal ? .995 : .9995) / log(10); # rounded
179 $decimal = ($unit % 3) == ($unit < 0);
180 $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999
182 3 + ($_[0] < 0), # digits plus optional negative sign
184 $_[0] / 1000 ** int($unit/3), # number
185 $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
186 $opt{units}->[$unit/3] # suffix
188 } and $opt{reformat}++;
189 $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
192 my (@lines, @values, @order);
194 $SIG{$_} = \&show_stat for $opt{'signal-stat'} || ();
197 alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0;
199 $SIG{INT} = \&show_exit;
201 if (defined $opt{interval}) {
202 $opt{interval} ||= 1;
203 alarm $opt{interval} if $opt{interval} > 0;
206 require Tie::Array::Sorted;
207 tie @order, 'Tie::Array::Sorted', sub { $_[1] <=> $_[0] };
208 } or warn $@, "Expect slowdown with large datasets!\n";
211 my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish
212 my $valmatch = qr< $opt{anchor} ( \h* -? $float |) >x;
213 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
215 s/\A\h*// unless $opt{unmodified};
216 my $valnum = s/$valmatch/\n/ && $1;
217 push @values, $valnum;
218 push @order, $valnum if length $valnum;
219 if (defined $opt{trim} and defined $valnum) {
220 my $trimpos = abs $opt{trim};
221 $trimpos -= length $valnum if $opt{unmodified};
223 $_ = substr $_, 0, 2;
225 elsif (length > $trimpos) {
226 # cut and replace (intentional lvalue for speed, contrary to PBP)
227 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
231 show_lines() if defined $opt{interval} and $opt{interval} < 0
232 and $. % $opt{interval} == 0;
235 $SIG{INT} = 'DEFAULT';
238 $opt{color} and defined $_[0] or return '';
239 return "\e[$_[0]m" if defined wantarray;
240 $_ = color(@_) . $_ . color(0) if defined;
245 state $nr = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0;
246 @lines > $nr or return;
248 my $limit = $opt{hidemax} ? $opt{hidemax}->($#lines, $nr) : $#lines;
250 @order = sort { $b <=> $a } @order unless tied @order;
251 my $maxval = $opt{maxval} // (
252 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
255 my $minval = $opt{minval} // min $order[-1] // (), 0;
256 my $range = $maxval - $minval;
257 $range &&= log $range if $opt{log};
258 my $lenval = $opt{'value-length'} // max map { length } @order;
259 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
260 max(map { length $values[$_] && length $lines[$_] } $nr .. $limit)
262 my $size = defined $opt{width} && $range &&
263 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
266 if ($opt{markers} and $size > 0) {
267 for my $markspec (split /\h/, $opt{markers}) {
268 my ($char, $func) = split //, $markspec, 2;
270 if ($func eq 'avg') {
271 return sum(@order) / @order;
273 elsif ($func =~ /\A([0-9.]+)v\z/) {
275 "Invalid marker $char: percentile $1 out of bounds\n"
277 my $index = $#order * $1 / 100;
278 return ($order[$index] + $order[$index + .5]) / 2;
280 elsif ($func =~ /\A-?[0-9.]+\z/) {
283 elsif ($func =~ /\A\/($float)\z/) {
284 my @range = my $multiple = my $next = $1;
285 while ($next < $maxval) {
286 $multiple *= 10 if $opt{log};
287 push @range, $next += $multiple;
292 die "Unknown marker $char: $func\n";
301 $pos &&= log $pos if $opt{log};
303 color(36) for $barmark[$pos / $range * $size] = $char;
307 state $lastmax = $maxval;
308 if ($maxval > $lastmax) {
309 print ' ' x ($lenval + $len);
312 ($lastmax - $minval) * $size / $range + .5,
313 '-' x (($values[$nr - 1] - $minval) * $size / $range);
315 say '+' x (($range - $lastmax) * $size / $range + .5);
322 color(31), sprintf('%*s', $lenval, $minval),
323 color(90), '-', color(36), '+',
324 color(32), sprintf('%*s', $size - 3, $maxval),
325 color(90), '-', color(36), '+',
329 while ($nr <= $limit) {
330 my $val = $values[$nr];
333 $rel = $val - $minval;
334 $rel &&= log $rel if $opt{log};
335 $rel = min(1, $rel / $range) if $range; # 0..1
337 my $color = !length $val || !$opt{palette} ? undef :
338 $val == $order[0] ? $opt{palette}->[-1] : # max
339 $val == $order[-1] ? $opt{palette}->[0] : # min
340 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
341 my $indicator = $opt{indicators} && $opt{indicators}->[
342 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
343 $#{$opt{indicators}} < 2 ? 1 :
344 $val >= $order[0] ? -1 :
345 $rel * ($#{$opt{indicators}} - 1e-14) + 1
349 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
350 print color($color), $_ for $indicator;
353 print $indicator if defined $indicator;
356 $val = sprintf("%*s", $lenval,
357 $opt{reformat} ? $opt{'value-format'}->($val) : $val
359 color($color) for $val;
361 my $line = $lines[$nr] =~ s/\n/$val/r;
362 if (not length $val) {
366 printf '%-*s', $len + length($val), $line;
367 if ($rel and $size) {
368 print $barmark[$_] // $opt{'graph-format'}
369 for 1 .. $rel * $size + .5;
376 say $opt{palette} ? color(0) : '' if $opt{spark};
386 my $linemin = !$opt{hidemin} ? 0 :
387 ($vars{start} = $opt{hidemin}->($#lines));
388 my $linemax = !$opt{hidemax} ? $#lines :
389 ($vars{end} = $opt{hidemax}->($#lines, $vars{start}));
391 $vars{partsum} = sum(0, grep {length} @values[$linemin .. $linemax])
392 if $linemin <= $linemax and ($opt{hidemin} or $opt{hidemax});
398 $vars{avg} = $vars{sum} / @order;
399 print varfmt($opt{report}, \%vars);
406 my ($fmt, $vars) = @_;
407 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
408 my ($name, $cmd) = split /\s*;/, $1, 2;
409 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
410 local $_ = $vars->{$name};
412 $_ = $opt{'value-format'}->($_) if $format;
415 warn "Error in \$$name report: $@" if $@;
425 show_stat() if $opt{stat};
426 exit 130 if @_; # 0x80+signo
434 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
437 -a, --[no-]ascii Restrict user interface to ASCII characters
438 -C, --[no-]color Force colored output of values and bar markers
439 -f, --field=([+]N|REGEXP)
440 Compare values after a given number of whitespace
442 --header Prepend a chart axis with minimum and maximum
444 -H, --human-readable Format values using SI unit prefixes
445 --sexagesimal Convert seconds to HH:MM:SS time format
446 -t, --interval[=(N|-LINES)]
447 Output partial progress every given number of
448 seconds or input lines
449 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
450 -L, --limit=[N|[-]START(-[END]|+N)]
451 Select a range of lines to display
452 -e, --log Logarithmic (exponential) scale instead of linear
453 --graph-format=CHAR Glyph to repeat for the graph line
454 -m, --markers=FORMAT Statistical positions to indicate on bars
455 --min=N, --max=N Bars extend from 0 or the minimum value if lower
456 --palette=(PRESET|COLORS)
457 Override colors of parsed numbers
458 -_, --spark Replace lines by sparklines
459 --indicators[=CHARS] Prefix a unicode character corresponding to each
461 -s, --stat Total statistics after all data
462 -u, --unmodified Do not reformat values, keeping leading whitespace
463 --value-length=SIZE Reserved space for numbers
464 -w, --width=COLUMNS Override the maximum number of columns to use
465 -h, --usage Overview of available options
466 --help Full pod documentation
467 -V, --version Version information
473 barcat - concatenate texts with graph to visualize values
477 B<barcat> [<options>] [<file>... | <numbers>]
481 Visualizes relative sizes of values read from input
482 (parameters, file(s) or STDIN).
483 Contents are concatenated similar to I<cat>,
484 but numbers are reformatted and a bar graph is appended to each line.
486 Don't worry, barcat does not drink and divide.
487 It can has various options for input and output (re)formatting,
488 but remains limited to one-dimensional charts.
489 For more complex graphing needs
490 you'll need a larger animal like I<gnuplot>.
496 =item -a, --[no-]ascii
498 Restrict user interface to ASCII characters,
499 replacing default UTF-8 by their closest approximation.
500 Input is always interpreted as UTF-8 and shown as is.
502 =item -C, --[no-]color
504 Force colored output of values and bar markers.
505 Defaults on if output is a tty,
506 disabled otherwise such as when piped or redirected.
507 Can also be disabled by setting I<-M>
508 or the I<NO_COLOR> environment variable.
510 =item -f, --field=([+]<number> | <regexp>)
512 Compare values after a given number of whitespace separators,
513 or matching a regular expression.
515 Unspecified or I<-f0> means values are at the start of each line.
516 With I<-f1> the second word is taken instead.
517 A string can indicate the starting position of a value
518 (such as I<-f:> if preceded by colons),
519 or capture the numbers itself,
520 for example I<-f'(\d+)'> for the first digits anywhere.
521 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
525 Prepend a chart axis with minimum and maximum values labeled.
527 =item -H, --human-readable
529 Format values using SI unit prefixes,
530 turning long numbers like I<12356789> into I<12.4M>.
531 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
532 Short integers are aligned but kept without decimal point.
536 Convert seconds to HH:MM:SS time format.
538 =item -t, --interval[=(<seconds> | -<lines>)]
540 Output partial progress every given number of seconds or input lines.
541 An update can also be forced by sending a I<SIGALRM> alarm signal.
543 =item -l, --length=[-]<size>[%]
545 Trim line contents (between number and bars)
546 to a maximum number of characters.
547 The exceeding part is replaced by an abbreviation sign,
548 unless C<--length=0>.
550 Prepend a dash (i.e. make negative) to enforce padding
551 regardless of encountered contents.
553 =item -L, --limit=[<count> | [-]<start>(-[<end>] | +<count>)]
555 Select a range of lines to display.
556 A single integer indicates the last line number (like C<head>),
557 or first line counting from the bottom if negative (like C<tail>).
559 A range consists of a starting line number followed by either
560 a dash C<-> to an optional end, or plus sign C<+> with count.
562 All hidden input is still counted and analyzed for statistics,
563 but disregarded for padding and bar size.
567 Logarithmic (I<e>xponential) scale instead of linear
568 to compare orders of magnitude.
570 =item --graph-format=<character>
572 Glyph to repeat for the graph line.
573 Defaults to a dash C<->.
575 =item -m, --markers=<format>
577 Statistical positions to indicate on bars.
578 A single indicator glyph precedes each position:
584 Exact value to match on the axis.
585 A vertical bar at the zero crossing is displayed by I<|0>
587 For example I<π3.14> would locate pi.
591 Repeated at every multiple of a number.
592 For example I<:/1> for a grid at every integer.
594 =item <percentage>I<v>
596 Ranked value at the given percentile.
597 The default shows I<+> at I<50v> for the mean or median;
598 the middle value or average between middle values.
599 One standard deviation right of the mean is at about I<68.3v>.
600 The default includes I<< >31.73v <68.27v >>
601 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
606 the sum of all values divided by the number of counted lines.
607 Indicated by default as I<=>.
611 =item --min=<number>, --max=<number>
613 Bars extend from 0 or the minimum value if lower,
614 to the largest value encountered.
615 These options can be set to customize this range.
617 =item --palette=(<preset> | <color>...)
619 Override colors of parsed numbers.
620 Can be any CSI escape, such as I<90> for default dark grey,
621 or alternatively I<1;30> for bright black.
623 In case of additional colors,
624 the last is used for values equal to the maximum, the first for minima.
625 If unspecified, these are green and red respectively (I<31 90 32>).
626 Multiple intermediate colors will be distributed
627 relative to the size of values.
629 Predefined color schemes are named I<whites> and I<fire>,
630 or I<greys> and I<fire256> for 256-color variants.
634 Replace lines by I<sparklines>,
635 single characters (configured by C<--indicators>)
636 corresponding to input values.
638 =item --indicators[=<characters>]
640 Prefix a unicode character corresponding to each value.
641 The first specified character will be used for non-values,
642 the remaining sequence will be distributed over the range of values.
643 Unspecified, block fill glyphs U+2581-2588 will be used.
647 Total statistics after all data.
649 =item -u, --unmodified
651 Do not reformat values, keeping leading whitespace.
652 Keep original value alignment, which may be significant in some programs.
654 =item --value-length=<size>
656 Reserved space for numbers.
658 =item -w, --width=<columns>
660 Override the maximum number of columns to use.
661 Appended graphics will extend to fill up the entire screen,
662 otherwise determined by the environment variable I<COLUMNS>
663 or by running the C<tput> command.
667 Overview of available options.
671 Full pod documentation
672 as rendered by perldoc.
684 seq 30 | awk '{print sin($1/10)}' | barcat
686 Compare file sizes (with human-readable numbers):
688 du -d0 -b * | barcat -H
690 Same from formatted results, selecting the first numeric value:
692 tree -s --noreport | barcat -H -f+
694 Compare media metadata, like image size or play time:
696 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
698 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
700 find -type f -print0 | xargs -0 -L1 \
701 ffprobe -show_format -of json -v error |
702 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
704 Memory usage of user processes with long names truncated:
706 ps xo rss,pid,cmd | barcat -l40
708 Monitor network latency from prefixed results:
710 ping google.com | barcat -f'time=\K' -t
712 Commonly used after counting, for example users on the current server:
714 users | tr ' ' '\n' | sort | uniq -c | barcat
716 Letter frequencies in text files:
718 cat /usr/share/games/fortunes/*.u8 |
719 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
720 sort | uniq -c | barcat
722 Number of HTTP requests per day:
724 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
726 Any kind of database query results, preserving returned alignment:
728 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
731 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
733 > SELECT schemaname, relname, pg_total_relation_size(relid)
734 FROM pg_statio_user_tables ORDER BY idx_blks_hit
737 Same thing in SQLite (requires the sqlite3 client):
740 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
742 Earthquakes worldwide magnitude 1+ in the last 24 hours:
744 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
745 column -ts, -n | barcat -f4 -u -l80%
747 External datasets, like movies per year:
749 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
750 jq .[].year | uniq -c | barcat
752 Pokémon height comparison:
754 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
755 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
757 USD/EUR exchange rate from CSV provided by the ECB:
759 curl https://sdw.ecb.europa.eu/export.do \
760 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
761 barcat -f',\K' --value-length=7
763 Total population history in XML from the World Bank:
765 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
766 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
767 barcat -f1 -H --markers=+/1e9
769 Population and other information for all countries:
771 curl http://download.geonames.org/export/dump/countryInfo.txt |
772 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
774 And of course various Git statistics, such commit count by year:
776 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
778 Or the top 3 most frequent authors with statistics over all:
780 git shortlog -sn | barcat -L3 -s
782 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
784 ( git log --pretty=%ci --since=30day | cut -b-10
785 seq 0 30 | xargs -i date +%F -d-{}day ) |
786 sort | uniq -c | awk '$1--' | barcat --spark
788 Sparkline graphics of simple input given as inline parameters:
790 barcat -_ 3 1 4 1 5 0 9 2 4
792 Misusing the spark functionality to draw a lolcat line:
794 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
798 Mischa POSLAWSKY <perl@shiar.org>