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 grays => [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> [I<options>] [I<file>... | I<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 B<-a>, B<-->[B<no->]B<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 B<-C>, B<-->[B<no->]B<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 B<-M>
510 or the I<NO_COLOR> environment variable.
512 =item B<-f>, B<--field>=([B<+>]I<number> | I<regexp>)
514 Compare values after a given number of whitespace separators,
515 or matching a regular expression.
517 Unspecified or B<-f0> means values are at the start of each line.
518 With B<-f1> the second word is taken instead.
519 A string can indicate the starting position of a value
520 (such as B<-f:> if preceded by colons),
521 or capture the numbers itself,
522 for example B<-f'(\d+)'> for the first digits anywhere.
523 A shorthand for this is C<+0>, or C<+N> to find the Nth number.
527 Prepend a chart axis with minimum and maximum values labeled.
529 =item B<-H>, B<--human-readable>
531 Format values using SI unit prefixes,
532 turning long numbers like C<12356789> into C<12.4M>.
533 Also changes an exponent C<1.602176634e-19> to C<160.2z>.
534 Short integers are aligned but kept without decimal point.
536 =item B<--sexagesimal>
538 Convert seconds to HH:MM:SS time format.
540 =item B<-t>, B<--interval>[=(I<seconds> | B<->I<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 B<-l>, B<--length>=[B<->]I<size>[B<%>]
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 B<--length=0>.
552 Prepend a dash (i.e. make negative) to enforce padding
553 regardless of encountered contents.
555 =item B<-L>, B<--limit>=[I<count> | [B<->]I<start>(B<->[I<end>] | B<+>I<count>)]
557 Select a range of lines to display.
558 A single integer indicates the last line number (like I<head>),
559 or first line counting from the bottom if negative (like I<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.
567 =item B<-e>, B<--log>
569 Logarithmic (B<e>xponential) scale instead of linear
570 to compare orders of magnitude.
572 =item B<--graph-format>=I<character>
574 Glyph to repeat for the graph line.
575 Defaults to a dash C<->.
577 =item B<-m>, B<--markers>=I<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 C<|0>
589 For example C<π3.14> would locate pi.
591 =item B</>I<interval>
593 Repeated at every multiple of a number.
594 For example C<:/1> for a grid at every integer.
596 =item I<percentage>B<v>
598 Ranked value at the given percentile.
599 The default shows C<+> at C<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 C<68.3v>.
602 The default includes C<< >31.73v <68.27v >>
603 to encompass all I<normal> results, or 68% of all entries, by I<< <--> >>.
608 the sum of all values divided by the number of counted lines.
609 Indicated by default as C<=>.
613 =item B<--min>=I<number>, B<--max>=I<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 B<--palette>=(I<preset> | I<color>...)
621 Override colors of parsed numbers.
622 Can be any CSI escape, such as C<90> for default dark gray,
623 or alternatively C<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 (C<31 90 32>).
628 Multiple intermediate colors will be distributed
629 relative to the size of values.
631 A non-numeric name can refer to a predefined color scheme:
637 Minimal set of monochrome brightnesses.
641 Utilize the 24 grayscale ramp in 256-color terminals.
645 Gradient red to white in 7 out of 16 colors.
649 Extended to 17 colors out of 256.
653 Saturated red to green to blue to red.
657 All 215 extended colors in unrelated orders.
661 =item B<-_>, B<--spark>
663 Replace lines by I<sparklines>,
664 single characters (configured by B<--indicators>)
665 corresponding to input values.
667 =item B<--indicators>[=I<characters>]
669 Prefix a unicode character corresponding to each value.
670 The first specified character will be used for non-values,
671 the remaining sequence will be distributed over the range of values.
672 Unspecified, block fill glyphs U+2581-2588 will be used.
674 =item B<-s>, B<--stat>
676 Total statistics after all data.
678 While processing (possibly a neverending pipe),
679 intermediate results are also shown on signal I<SIGINFO> if available (control+t on BSDs)
680 or I<SIGQUIT> otherwise (ctrl+\ on linux).
682 =item B<-u>, B<--unmodified>
684 Do not reformat values, keeping leading whitespace.
685 Keep original value alignment, which may be significant in some programs.
687 =item B<--value-length>=I<size>
689 Reserved space for numbers.
691 =item B<-w>, B<--width>=I<columns>
693 Override the maximum number of columns to use.
694 Appended graphics will extend to fill up the entire screen,
695 otherwise determined by the environment variable I<COLUMNS>
696 or by running the I<tput> command.
698 =item B<-h>, B<--usage>
700 Overview of available options.
704 Full pod documentation
705 as rendered by perldoc.
707 =item B<-V>, B<--version>
717 seq 30 | awk '{print sin($1/10)}' | barcat
719 Compare file sizes (with human-readable numbers):
721 du -d0 -b * | barcat -H
723 Same from formatted results, selecting the first numeric value:
725 tree -s --noreport | barcat -H -f+
727 Compare media metadata, like image size or play time:
729 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
731 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
733 find -type f -print0 | xargs -0 -L1 \
734 ffprobe -show_format -of json -v error |
735 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
737 Memory usage of user processes with long names truncated:
739 ps xo rss,pid,cmd | barcat -l40
741 Monitor network latency from prefixed results:
743 ping google.com | barcat -f'time=\K' -t
745 Commonly used after counting, for example users on the current server:
747 users | tr ' ' '\n' | sort | uniq -c | barcat
749 Letter frequencies in text files:
751 cat /usr/share/games/fortunes/*.u8 |
752 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
753 sort | uniq -c | barcat
755 Number of HTTP requests per day:
757 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
759 Any kind of database query results, preserving returned alignment:
761 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
764 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
766 > SELECT schemaname, relname, pg_total_relation_size(relid)
767 FROM pg_statio_user_tables ORDER BY idx_blks_hit
770 Same thing in SQLite (requires the sqlite3 client):
773 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
775 Earthquakes worldwide magnitude 1+ in the last 24 hours:
777 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
778 column -ts, -n | barcat -f4 -u -l80%
780 External datasets, like movies per year:
782 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
783 jq .[].year | uniq -c | barcat
785 Pokémon height comparison:
787 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
788 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
790 USD/EUR exchange rate from CSV provided by the ECB:
792 curl https://sdw.ecb.europa.eu/export.do \
793 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
794 barcat -f',\K' --value-length=7
796 Total population history in XML from the World Bank:
798 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
799 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
800 barcat -f1 -H --markers=+/1e9
802 Population and other information for all countries:
804 curl http://download.geonames.org/export/dump/countryInfo.txt |
805 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
807 And of course various Git statistics, such commit count by year:
809 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
811 Or the top 3 most frequent authors with statistics over all:
813 git shortlog -sn | barcat -L3 -s
815 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
817 ( git log --pretty=%ci --since=30day | cut -b-10
818 seq 0 30 | xargs -i date +%F -d-{}day ) |
819 sort | uniq -c | awk '$1--' | barcat --spark
821 Sparkline graphics of simple input given as inline parameters:
823 barcat -_ 3 1 4 1 5 0 9 2 4
825 Misusing the spark functionality to draw a lolcat line:
827 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
831 Mischa POSLAWSKY <perl@shiar.org>