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('${count: (%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;
400 say varfmt($opt{report}, \%vars);
405 my ($fmt, $vars) = @_;
406 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
407 my ($name, $op, $cmd) = split /\s*([;:])/, $1, 2;
408 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
409 local $_ = $vars->{$name};
411 $_ = $opt{'value-format'}->($_) if $format;
412 if ($cmd and $op eq ':') {
413 $_ = varfmt($cmd, $vars);
417 warn "Error in \$$name report: $@" if $@;
427 show_stat() if $opt{stat};
428 exit 130 if @_; # 0x80+signo
436 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
439 -a, --[no-]ascii Restrict user interface to ASCII characters
440 -C, --[no-]color Force colored output of values and bar markers
441 -f, --field=([+]N|REGEXP)
442 Compare values after a given number of whitespace
444 --header Prepend a chart axis with minimum and maximum
446 -H, --human-readable Format values using SI unit prefixes
447 --sexagesimal Convert seconds to HH:MM:SS time format
448 -t, --interval[=(N|-LINES)]
449 Output partial progress every given number of
450 seconds or input lines
451 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
452 -L, --limit=[N|[-]START(-[END]|+N)]
453 Select a range of lines to display
454 -e, --log Logarithmic (exponential) scale instead of linear
455 --graph-format=CHAR Glyph to repeat for the graph line
456 -m, --markers=FORMAT Statistical positions to indicate on bars
457 --min=N, --max=N Bars extend from 0 or the minimum value if lower
458 --palette=(PRESET|COLORS)
459 Override colors of parsed numbers
460 -_, --spark Replace lines by sparklines
461 --indicators[=CHARS] Prefix a unicode character corresponding to each
463 -s, --stat Total statistics after all data
464 -u, --unmodified Do not reformat values, keeping leading whitespace
465 --value-length=SIZE Reserved space for numbers
466 -w, --width=COLUMNS Override the maximum number of columns to use
467 -h, --usage Overview of available options
468 --help Full pod documentation
469 -V, --version Version information
475 barcat - concatenate texts with graph to visualize values
479 B<barcat> [<options>] [<file>... | <numbers>]
483 Visualizes relative sizes of values read from input
484 (parameters, file(s) or STDIN).
485 Contents are concatenated similar to I<cat>,
486 but numbers are reformatted and a bar graph is appended to each line.
488 Don't worry, barcat does not drink and divide.
489 It can has various options for input and output (re)formatting,
490 but remains limited to one-dimensional charts.
491 For more complex graphing needs
492 you'll need a larger animal like I<gnuplot>.
498 =item -a, --[no-]ascii
500 Restrict user interface to ASCII characters,
501 replacing default UTF-8 by their closest approximation.
502 Input is always interpreted as UTF-8 and shown as is.
504 =item -C, --[no-]color
506 Force colored output of values and bar markers.
507 Defaults on if output is a tty,
508 disabled otherwise such as when piped or redirected.
509 Can also be disabled by setting I<-M>
510 or the I<NO_COLOR> environment variable.
512 =item -f, --field=([+]<number> | <regexp>)
514 Compare values after a given number of whitespace separators,
515 or matching a regular expression.
517 Unspecified or I<-f0> means values are at the start of each line.
518 With I<-f1> the second word is taken instead.
519 A string can indicate the starting position of a value
520 (such as I<-f:> if preceded by colons),
521 or capture the numbers itself,
522 for example I<-f'(\d+)'> for the first digits anywhere.
523 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
527 Prepend a chart axis with minimum and maximum values labeled.
529 =item -H, --human-readable
531 Format values using SI unit prefixes,
532 turning long numbers like I<12356789> into I<12.4M>.
533 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
534 Short integers are aligned but kept without decimal point.
538 Convert seconds to HH:MM:SS time format.
540 =item -t, --interval[=(<seconds> | -<lines>)]
542 Output partial progress every given number of seconds or input lines.
543 An update can also be forced by sending a I<SIGALRM> alarm signal.
545 =item -l, --length=[-]<size>[%]
547 Trim line contents (between number and bars)
548 to a maximum number of characters.
549 The exceeding part is replaced by an abbreviation sign,
550 unless C<--length=0>.
552 Prepend a dash (i.e. make negative) to enforce padding
553 regardless of encountered contents.
555 =item -L, --limit=[<count> | [-]<start>(-[<end>] | +<count>)]
557 Select a range of lines to display.
558 A single integer indicates the last line number (like C<head>),
559 or first line counting from the bottom if negative (like C<tail>).
561 A range consists of a starting line number followed by either
562 a dash C<-> to an optional end, or plus sign C<+> with count.
564 All hidden input is still counted and analyzed for statistics,
565 but disregarded for padding and bar size.
569 Logarithmic (I<e>xponential) scale instead of linear
570 to compare orders of magnitude.
572 =item --graph-format=<character>
574 Glyph to repeat for the graph line.
575 Defaults to a dash C<->.
577 =item -m, --markers=<format>
579 Statistical positions to indicate on bars.
580 A single indicator glyph precedes each position:
586 Exact value to match on the axis.
587 A vertical bar at the zero crossing is displayed by I<|0>
589 For example I<π3.14> would locate pi.
593 Repeated at every multiple of a number.
594 For example I<:/1> for a grid at every integer.
596 =item <percentage>I<v>
598 Ranked value at the given percentile.
599 The default shows I<+> at I<50v> for the mean or median;
600 the middle value or average between middle values.
601 One standard deviation right of the mean is at about I<68.3v>.
602 The default includes I<< >31.73v <68.27v >>
603 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
608 the sum of all values divided by the number of counted lines.
609 Indicated by default as I<=>.
613 =item --min=<number>, --max=<number>
615 Bars extend from 0 or the minimum value if lower,
616 to the largest value encountered.
617 These options can be set to customize this range.
619 =item --palette=(<preset> | <color>...)
621 Override colors of parsed numbers.
622 Can be any CSI escape, such as I<90> for default dark grey,
623 or alternatively I<1;30> for bright black.
625 In case of additional colors,
626 the last is used for values equal to the maximum, the first for minima.
627 If unspecified, these are green and red respectively (I<31 90 32>).
628 Multiple intermediate colors will be distributed
629 relative to the size of values.
631 Predefined color schemes are named I<whites> and I<fire>,
632 or I<greys> and I<fire256> for 256-color variants.
636 Replace lines by I<sparklines>,
637 single characters (configured by C<--indicators>)
638 corresponding to input values.
640 =item --indicators[=<characters>]
642 Prefix a unicode character corresponding to each value.
643 The first specified character will be used for non-values,
644 the remaining sequence will be distributed over the range of values.
645 Unspecified, block fill glyphs U+2581-2588 will be used.
649 Total statistics after all data.
651 =item -u, --unmodified
653 Do not reformat values, keeping leading whitespace.
654 Keep original value alignment, which may be significant in some programs.
656 =item --value-length=<size>
658 Reserved space for numbers.
660 =item -w, --width=<columns>
662 Override the maximum number of columns to use.
663 Appended graphics will extend to fill up the entire screen,
664 otherwise determined by the environment variable I<COLUMNS>
665 or by running the C<tput> command.
669 Overview of available options.
673 Full pod documentation
674 as rendered by perldoc.
686 seq 30 | awk '{print sin($1/10)}' | barcat
688 Compare file sizes (with human-readable numbers):
690 du -d0 -b * | barcat -H
692 Same from formatted results, selecting the first numeric value:
694 tree -s --noreport | barcat -H -f+
696 Compare media metadata, like image size or play time:
698 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
700 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
702 find -type f -print0 | xargs -0 -L1 \
703 ffprobe -show_format -of json -v error |
704 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
706 Memory usage of user processes with long names truncated:
708 ps xo rss,pid,cmd | barcat -l40
710 Monitor network latency from prefixed results:
712 ping google.com | barcat -f'time=\K' -t
714 Commonly used after counting, for example users on the current server:
716 users | tr ' ' '\n' | sort | uniq -c | barcat
718 Letter frequencies in text files:
720 cat /usr/share/games/fortunes/*.u8 |
721 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
722 sort | uniq -c | barcat
724 Number of HTTP requests per day:
726 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
728 Any kind of database query results, preserving returned alignment:
730 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
733 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
735 > SELECT schemaname, relname, pg_total_relation_size(relid)
736 FROM pg_statio_user_tables ORDER BY idx_blks_hit
739 Same thing in SQLite (requires the sqlite3 client):
742 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
744 Earthquakes worldwide magnitude 1+ in the last 24 hours:
746 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
747 column -ts, -n | barcat -f4 -u -l80%
749 External datasets, like movies per year:
751 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
752 jq .[].year | uniq -c | barcat
754 Pokémon height comparison:
756 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
757 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
759 USD/EUR exchange rate from CSV provided by the ECB:
761 curl https://sdw.ecb.europa.eu/export.do \
762 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
763 barcat -f',\K' --value-length=7
765 Total population history in XML from the World Bank:
767 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
768 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
769 barcat -f1 -H --markers=+/1e9
771 Population and other information for all countries:
773 curl http://download.geonames.org/export/dump/countryInfo.txt |
774 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
776 And of course various Git statistics, such commit count by year:
778 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
780 Or the top 3 most frequent authors with statistics over all:
782 git shortlog -sn | barcat -L3 -s
784 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
786 ( git log --pretty=%ci --since=30day | cut -b-10
787 seq 0 30 | xargs -i date +%F -d-{}day ) |
788 sort | uniq -c | awk '$1--' | barcat --spark
790 Sparkline graphics of simple input given as inline parameters:
792 barcat -_ 3 1 4 1 5 0 9 2 4
794 Misusing the spark functionality to draw a lolcat line:
796 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
800 Mischa POSLAWSKY <perl@shiar.org>