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 print varfmt('${sum+} of ', {
363 lines => $linemax - $linemin + 1,
364 sum => sum(0, grep {length} @values[$linemin .. $linemax]),
368 my $total = sum @order;
369 my $fmt = '${sum+;color(1)} total in ${count#} values';
370 $fmt .= ' over ${lines#} lines' if @order != @lines;
371 $fmt .= " ($_)" for $opt{report} || ();
378 avg => $total / @order,
386 my ($fmt, $vars) = @_;
387 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
388 my ($name, $cmd) = split /\s*;/, $1, 2;
389 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
390 local $_ = $vars->{$name};
392 $_ = $opt{'value-format'}->($_) if $format;
395 warn "Error in \$$name report: $@" if $@;
400 warn "Unknown variable \$$name in report\n";
409 show_stat() if $opt{stat};
410 exit 130 if @_; # 0x80+signo
418 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
421 -a, --[no-]ascii Restrict user interface to ASCII characters
422 -C, --[no-]color Force colored output of values and bar markers
423 -f, --field=([+]N|REGEXP)
424 Compare values after a given number of whitespace
426 --header Prepend a chart axis with minimum and maximum
428 -H, --human-readable Format values using SI unit prefixes
429 --sexagesimal Convert seconds to HH:MM:SS time format
430 -t, --interval[=(N|-LINES)]
431 Output partial progress every given number of
432 seconds or input lines
433 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
434 -L, --limit[=(N|-LAST|START-[END])]
435 Stop output after a number of lines
436 -e, --log Logarithmic (exponential) scale instead of linear
437 --graph-format=CHAR Glyph to repeat for the graph line
438 -m, --markers=FORMAT Statistical positions to indicate on bars
439 --min=N, --max=N Bars extend from 0 or the minimum value if lower
440 --palette=(PRESET|COLORS)
441 Override colors of parsed numbers
442 -_, --spark Replace lines by sparklines
443 --indicators[=CHARS] Prefix a unicode character corresponding to each
445 -s, --stat Total statistics after all data
446 -u, --unmodified Do not reformat values, keeping leading whitespace
447 --value-length=SIZE Reserved space for numbers
448 -w, --width=COLUMNS Override the maximum number of columns to use
449 -h, --usage Overview of available options
450 --help Full pod documentation
451 -V, --version Version information
457 barcat - concatenate texts with graph to visualize values
461 B<barcat> [<options>] [<file>... | <numbers>]
465 Visualizes relative sizes of values read from input
466 (parameters, file(s) or STDIN).
467 Contents are concatenated similar to I<cat>,
468 but numbers are reformatted and a bar graph is appended to each line.
470 Don't worry, barcat does not drink and divide.
471 It can has various options for input and output (re)formatting,
472 but remains limited to one-dimensional charts.
473 For more complex graphing needs
474 you'll need a larger animal like I<gnuplot>.
480 =item -a, --[no-]ascii
482 Restrict user interface to ASCII characters,
483 replacing default UTF-8 by their closest approximation.
484 Input is always interpreted as UTF-8 and shown as is.
486 =item -C, --[no-]color
488 Force colored output of values and bar markers.
489 Defaults on if output is a tty,
490 disabled otherwise such as when piped or redirected.
491 Can also be disabled by setting I<-M>
492 or the I<NO_COLOR> environment variable.
494 =item -f, --field=([+]<number> | <regexp>)
496 Compare values after a given number of whitespace separators,
497 or matching a regular expression.
499 Unspecified or I<-f0> means values are at the start of each line.
500 With I<-f1> the second word is taken instead.
501 A string can indicate the starting position of a value
502 (such as I<-f:> if preceded by colons),
503 or capture the numbers itself,
504 for example I<-f'(\d+)'> for the first digits anywhere.
505 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
509 Prepend a chart axis with minimum and maximum values labeled.
511 =item -H, --human-readable
513 Format values using SI unit prefixes,
514 turning long numbers like I<12356789> into I<12.4M>.
515 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
516 Short integers are aligned but kept without decimal point.
520 Convert seconds to HH:MM:SS time format.
522 =item -t, --interval[=(<seconds> | -<lines>)]
524 Output partial progress every given number of seconds or input lines.
525 An update can also be forced by sending a I<SIGALRM> alarm signal.
527 =item -l, --length=[-]<size>[%]
529 Trim line contents (between number and bars)
530 to a maximum number of characters.
531 The exceeding part is replaced by an abbreviation sign,
532 unless C<--length=0>.
534 Prepend a dash (i.e. make negative) to enforce padding
535 regardless of encountered contents.
537 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
539 Stop output after a number of lines.
540 A single value indicates the last line number (like C<head>),
541 or first line counting from the bottom if negative (like C<tail>).
542 A specific range can be given by two values.
544 All input is still counted and analyzed for statistics,
545 but disregarded for padding and bar size.
549 Logarithmic (I<e>xponential) scale instead of linear
550 to compare orders of magnitude.
552 =item --graph-format=<character>
554 Glyph to repeat for the graph line.
555 Defaults to a dash C<->.
557 =item -m, --markers=<format>
559 Statistical positions to indicate on bars.
560 A single indicator glyph precedes each position:
566 Exact value to match on the axis.
567 A vertical bar at the zero crossing is displayed by I<|0>
569 For example I<:3.14> would show a colon at pi.
571 =item <percentage>I<v>
573 Ranked value at the given percentile.
574 The default shows I<+> at I<50v> for the mean or median;
575 the middle value or average between middle values.
576 One standard deviation right of the mean is at about I<68.3v>.
577 The default includes I<< >31.73v <68.27v >>
578 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
583 the sum of all values divided by the number of counted lines.
584 Indicated by default as I<=>.
588 =item --min=<number>, --max=<number>
590 Bars extend from 0 or the minimum value if lower,
591 to the largest value encountered.
592 These options can be set to customize this range.
594 =item --palette=(<preset> | <color>...)
596 Override colors of parsed numbers.
597 Can be any CSI escape, such as I<90> for default dark grey,
598 or alternatively I<1;30> for bright black.
600 In case of additional colors,
601 the last is used for values equal to the maximum, the first for minima.
602 If unspecified, these are green and red respectively (I<31 90 32>).
603 Multiple intermediate colors will be distributed
604 relative to the size of values.
606 Predefined color schemes are named I<whites> and I<fire>,
607 or I<greys> and I<fire256> for 256-color variants.
611 Replace lines by I<sparklines>,
612 single characters (configured by C<--indicators>)
613 corresponding to input values.
615 =item --indicators[=<characters>]
617 Prefix a unicode character corresponding to each value.
618 The first specified character will be used for non-values,
619 the remaining sequence will be distributed over the range of values.
620 Unspecified, block fill glyphs U+2581-2588 will be used.
624 Total statistics after all data.
626 =item -u, --unmodified
628 Do not reformat values, keeping leading whitespace.
629 Keep original value alignment, which may be significant in some programs.
631 =item --value-length=<size>
633 Reserved space for numbers.
635 =item -w, --width=<columns>
637 Override the maximum number of columns to use.
638 Appended graphics will extend to fill up the entire screen,
639 otherwise determined by the environment variable I<COLUMNS>
640 or by running the C<tput> command.
644 Overview of available options.
648 Full pod documentation
649 as rendered by perldoc.
661 seq 30 | awk '{print sin($1/10)}' | barcat
663 Compare file sizes (with human-readable numbers):
665 du -d0 -b * | barcat -H
667 Same from formatted results, selecting the first numeric value:
669 tree -s --noreport | barcat -H -f+
671 Compare media metadata, like image size or play time:
673 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
675 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
677 find -type f -print0 | xargs -0 -L1 \
678 ffprobe -show_format -of json -v error |
679 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
681 Memory usage of user processes with long names truncated:
683 ps xo rss,pid,cmd | barcat -l40
685 Monitor network latency from prefixed results:
687 ping google.com | barcat -f'time=\K' -t
689 Commonly used after counting, for example users on the current server:
691 users | tr ' ' '\n' | sort | uniq -c | barcat
693 Letter frequencies in text files:
695 cat /usr/share/games/fortunes/*.u8 |
696 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
697 sort | uniq -c | barcat
699 Number of HTTP requests per day:
701 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
703 Any kind of database query results, preserving returned alignment:
705 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
708 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
710 > SELECT schemaname, relname, pg_total_relation_size(relid)
711 FROM pg_statio_user_tables ORDER BY idx_blks_hit
714 Same thing in SQLite (requires the sqlite3 client):
717 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
719 Earthquakes worldwide magnitude 1+ in the last 24 hours:
721 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
722 column -ts, -n | barcat -f4 -u -l80%
724 External datasets, like movies per year:
726 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
727 jq .[].year | uniq -c | barcat
729 Pokémon height comparison:
731 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
732 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
734 USD/EUR exchange rate from CSV provided by the ECB:
736 curl https://sdw.ecb.europa.eu/export.do \
737 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
738 barcat -f',\K' --value-length=7
740 Total population history in XML from the World Bank:
742 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
743 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
746 Population and other information for all countries:
748 curl http://download.geonames.org/export/dump/countryInfo.txt |
749 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
751 And of course various Git statistics, such commit count by year:
753 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
755 Or the top 3 most frequent authors with statistics over all:
757 git shortlog -sn | barcat -L3 -s
759 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
761 ( git log --pretty=%ci --since=30day | cut -b-10
762 seq 0 30 | xargs -i date +%F -d-{}day ) |
763 sort | uniq -c | awk '$1--' | barcat --spark
765 Sparkline graphics of simple input given as inline parameters:
767 barcat -_ 3 1 4 1 5 0 9 2 4
769 Misusing the spark functionality to draw a lolcat line:
771 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
775 Mischa POSLAWSKY <perl@shiar.org>