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";
183 my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish
184 my $valmatch = qr< $opt{anchor} ( \h* -? $float |) >x;
185 while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
187 s/\A\h*// unless $opt{unmodified};
188 my $valnum = s/$valmatch/\n/ && $1;
189 push @values, $valnum;
190 push @order, $valnum if length $valnum;
191 if (defined $opt{trim} and defined $valnum) {
192 my $trimpos = abs $opt{trim};
193 $trimpos -= length $valnum if $opt{unmodified};
195 $_ = substr $_, 0, 2;
197 elsif (length > $trimpos) {
198 # cut and replace (intentional lvalue for speed, contrary to PBP)
199 substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…';
203 show_lines() if defined $opt{interval} and $opt{interval} < 0
204 and $. % $opt{interval} == 0;
207 $SIG{INT} = 'DEFAULT';
210 $opt{color} and defined $_[0] or return '';
211 return "\e[$_[0]m" if defined wantarray;
212 $_ = color(@_) . $_ . color(0) if defined;
218 $opt{hidemin} < 0 ? max(0, @lines + $opt{hidemin} + 1) :
220 @lines > $nr or return;
223 if (defined $opt{hidemax}) {
224 if ($opt{hidemin} and $opt{hidemin} < 0) {
225 $limit -= $opt{hidemax} - 1;
227 elsif ($opt{hidemax} <= $limit) {
228 $limit = $opt{hidemax} - 1;
232 @order = sort { $b <=> $a } @order unless tied @order;
233 my $maxval = $opt{maxval} // (
234 $opt{hidemax} ? max grep { length } @values[$nr .. $limit] :
237 my $minval = $opt{minval} // min $order[-1] // (), 0;
238 my $range = $maxval - $minval;
239 $range &&= log $range if $opt{log};
240 my $lenval = $opt{'value-length'} // max map { length } @order;
241 my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
242 max map { length $values[$_] && length $lines[$_] }
243 0 .. min $#lines, $opt{hidemax} || (); # left padding
244 my $size = defined $opt{width} && $range &&
245 ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication
248 if ($opt{markers} and $size > 0) {
249 for my $markspec (split /\h/, $opt{markers}) {
250 my ($char, $func) = split //, $markspec, 2;
252 if ($func eq 'avg') {
253 return sum(@order) / @order;
255 elsif ($func =~ /\A([0-9.]+)v\z/) {
257 "Invalid marker $char: percentile $1 out of bounds\n"
259 my $index = $#order * $1 / 100;
260 return ($order[$index] + $order[$index + .5]) / 2;
262 elsif ($func =~ /\A-?[0-9.]+\z/) {
265 elsif ($func =~ /\A\/($float)\z/) {
266 my @range = my $multiple = my $next = $1;
267 while ($next < $maxval) {
268 $multiple *= 10 if $opt{log};
269 push @range, $next += $multiple;
274 die "Unknown marker $char: $func\n";
283 $pos &&= log $pos if $opt{log};
285 color(36) for $barmark[$pos / $range * $size] = $char;
289 state $lastmax = $maxval;
290 if ($maxval > $lastmax) {
291 print ' ' x ($lenval + $len);
294 ($lastmax - $minval) * $size / $range + .5,
295 '-' x (($values[$nr - 1] - $minval) * $size / $range);
297 say '+' x (($range - $lastmax) * $size / $range + .5);
304 color(31), sprintf('%*s', $lenval, $minval),
305 color(90), '-', color(36), '+',
306 color(32), sprintf('%*s', $size - 3, $maxval),
307 color(90), '-', color(36), '+',
311 while ($nr <= $limit) {
312 my $val = $values[$nr];
315 $rel = $val - $minval;
316 $rel &&= log $rel if $opt{log};
317 $rel = min(1, $rel / $range) if $range; # 0..1
319 my $color = !length $val || !$opt{palette} ? undef :
320 $val == $order[0] ? $opt{palette}->[-1] : # max
321 $val == $order[-1] ? $opt{palette}->[0] : # min
322 $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ];
323 my $indicator = $opt{indicators} && $opt{indicators}->[
324 !length($val) || !$#{$opt{indicators}} ? 0 : # blank
325 $#{$opt{indicators}} < 2 ? 1 :
326 $val >= $order[0] ? -1 :
327 $rel * ($#{$opt{indicators}} - 1e-14) + 1
331 say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
332 print color($color), $_ for $indicator;
335 print $indicator if defined $indicator;
338 $val = sprintf("%*s", $lenval,
339 $opt{reformat} ? $opt{'value-format'}->($val) : $val
341 color($color) for $val;
343 my $line = $lines[$nr] =~ s/\n/$val/r;
344 if (not length $val) {
348 printf '%-*s', $len + length($val), $line;
349 if ($rel and $size) {
350 print $barmark[$_] // $opt{'graph-format'}
351 for 1 .. $rel * $size + .5;
358 say $opt{palette} ? color(0) : '' if $opt{spark};
364 if ($opt{hidemin} or $opt{hidemax}) {
365 my $linemin = $opt{hidemin};
366 my $linemax = ($opt{hidemax} || @lines) - 1;
369 $linemax = @lines - $linemax;
371 print varfmt('${sum+} of ', {
372 lines => $linemax - $linemin + 1,
373 sum => sum(0, grep {length} @values[$linemin .. $linemax]),
377 my $total = sum @order;
378 my $fmt = '${sum+;color(1)} total in ${count#} values';
379 $fmt .= ' over ${lines#} lines' if @order != @lines;
380 $fmt .= " ($_)" for $opt{report} || ();
387 avg => $total / @order,
395 my ($fmt, $vars) = @_;
396 $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{
397 my ($name, $cmd) = split /\s*;/, $1, 2;
398 my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat};
399 local $_ = $vars->{$name};
401 $_ = $opt{'value-format'}->($_) if $format;
404 warn "Error in \$$name report: $@" if $@;
409 warn "Unknown variable \$$name in report\n";
418 show_stat() if $opt{stat};
419 exit 130 if @_; # 0x80+signo
427 barcat [OPTIONS] [FILES|NUMBERS] (=•.•=)
430 -a, --[no-]ascii Restrict user interface to ASCII characters
431 -C, --[no-]color Force colored output of values and bar markers
432 -f, --field=([+]N|REGEXP)
433 Compare values after a given number of whitespace
435 --header Prepend a chart axis with minimum and maximum
437 -H, --human-readable Format values using SI unit prefixes
438 --sexagesimal Convert seconds to HH:MM:SS time format
439 -t, --interval[=(N|-LINES)]
440 Output partial progress every given number of
441 seconds or input lines
442 -l, --length=[-]SIZE[%] Trim line contents (between number and bars)
443 -L, --limit[=(N|-LAST|START-[END])]
444 Stop output after a number of lines
445 -e, --log Logarithmic (exponential) scale instead of linear
446 --graph-format=CHAR Glyph to repeat for the graph line
447 -m, --markers=FORMAT Statistical positions to indicate on bars
448 --min=N, --max=N Bars extend from 0 or the minimum value if lower
449 --palette=(PRESET|COLORS)
450 Override colors of parsed numbers
451 -_, --spark Replace lines by sparklines
452 --indicators[=CHARS] Prefix a unicode character corresponding to each
454 -s, --stat Total statistics after all data
455 -u, --unmodified Do not reformat values, keeping leading whitespace
456 --value-length=SIZE Reserved space for numbers
457 -w, --width=COLUMNS Override the maximum number of columns to use
458 -h, --usage Overview of available options
459 --help Full pod documentation
460 -V, --version Version information
466 barcat - concatenate texts with graph to visualize values
470 B<barcat> [<options>] [<file>... | <numbers>]
474 Visualizes relative sizes of values read from input
475 (parameters, file(s) or STDIN).
476 Contents are concatenated similar to I<cat>,
477 but numbers are reformatted and a bar graph is appended to each line.
479 Don't worry, barcat does not drink and divide.
480 It can has various options for input and output (re)formatting,
481 but remains limited to one-dimensional charts.
482 For more complex graphing needs
483 you'll need a larger animal like I<gnuplot>.
489 =item -a, --[no-]ascii
491 Restrict user interface to ASCII characters,
492 replacing default UTF-8 by their closest approximation.
493 Input is always interpreted as UTF-8 and shown as is.
495 =item -C, --[no-]color
497 Force colored output of values and bar markers.
498 Defaults on if output is a tty,
499 disabled otherwise such as when piped or redirected.
500 Can also be disabled by setting I<-M>
501 or the I<NO_COLOR> environment variable.
503 =item -f, --field=([+]<number> | <regexp>)
505 Compare values after a given number of whitespace separators,
506 or matching a regular expression.
508 Unspecified or I<-f0> means values are at the start of each line.
509 With I<-f1> the second word is taken instead.
510 A string can indicate the starting position of a value
511 (such as I<-f:> if preceded by colons),
512 or capture the numbers itself,
513 for example I<-f'(\d+)'> for the first digits anywhere.
514 A shorthand for this is I<+0>, or I<+N> to find the Nth number.
518 Prepend a chart axis with minimum and maximum values labeled.
520 =item -H, --human-readable
522 Format values using SI unit prefixes,
523 turning long numbers like I<12356789> into I<12.4M>.
524 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
525 Short integers are aligned but kept without decimal point.
529 Convert seconds to HH:MM:SS time format.
531 =item -t, --interval[=(<seconds> | -<lines>)]
533 Output partial progress every given number of seconds or input lines.
534 An update can also be forced by sending a I<SIGALRM> alarm signal.
536 =item -l, --length=[-]<size>[%]
538 Trim line contents (between number and bars)
539 to a maximum number of characters.
540 The exceeding part is replaced by an abbreviation sign,
541 unless C<--length=0>.
543 Prepend a dash (i.e. make negative) to enforce padding
544 regardless of encountered contents.
546 =item -L, --limit[=(<count> | -<last> | <start>-[<end>])]
548 Stop output after a number of lines.
549 A single value indicates the last line number (like C<head>),
550 or first line counting from the bottom if negative (like C<tail>).
551 A specific range can be given by two values.
553 All input is still counted and analyzed for statistics,
554 but disregarded for padding and bar size.
558 Logarithmic (I<e>xponential) scale instead of linear
559 to compare orders of magnitude.
561 =item --graph-format=<character>
563 Glyph to repeat for the graph line.
564 Defaults to a dash C<->.
566 =item -m, --markers=<format>
568 Statistical positions to indicate on bars.
569 A single indicator glyph precedes each position:
575 Exact value to match on the axis.
576 A vertical bar at the zero crossing is displayed by I<|0>
578 For example I<π3.14> would locate pi.
582 Repeated at every multiple of a number.
583 For example I<:/1> for a grid at every integer.
585 =item <percentage>I<v>
587 Ranked value at the given percentile.
588 The default shows I<+> at I<50v> for the mean or median;
589 the middle value or average between middle values.
590 One standard deviation right of the mean is at about I<68.3v>.
591 The default includes I<< >31.73v <68.27v >>
592 to encompass all I<normal> results, or 68% of all entries, by B<< <--> >>.
597 the sum of all values divided by the number of counted lines.
598 Indicated by default as I<=>.
602 =item --min=<number>, --max=<number>
604 Bars extend from 0 or the minimum value if lower,
605 to the largest value encountered.
606 These options can be set to customize this range.
608 =item --palette=(<preset> | <color>...)
610 Override colors of parsed numbers.
611 Can be any CSI escape, such as I<90> for default dark grey,
612 or alternatively I<1;30> for bright black.
614 In case of additional colors,
615 the last is used for values equal to the maximum, the first for minima.
616 If unspecified, these are green and red respectively (I<31 90 32>).
617 Multiple intermediate colors will be distributed
618 relative to the size of values.
620 Predefined color schemes are named I<whites> and I<fire>,
621 or I<greys> and I<fire256> for 256-color variants.
625 Replace lines by I<sparklines>,
626 single characters (configured by C<--indicators>)
627 corresponding to input values.
629 =item --indicators[=<characters>]
631 Prefix a unicode character corresponding to each value.
632 The first specified character will be used for non-values,
633 the remaining sequence will be distributed over the range of values.
634 Unspecified, block fill glyphs U+2581-2588 will be used.
638 Total statistics after all data.
640 =item -u, --unmodified
642 Do not reformat values, keeping leading whitespace.
643 Keep original value alignment, which may be significant in some programs.
645 =item --value-length=<size>
647 Reserved space for numbers.
649 =item -w, --width=<columns>
651 Override the maximum number of columns to use.
652 Appended graphics will extend to fill up the entire screen,
653 otherwise determined by the environment variable I<COLUMNS>
654 or by running the C<tput> command.
658 Overview of available options.
662 Full pod documentation
663 as rendered by perldoc.
675 seq 30 | awk '{print sin($1/10)}' | barcat
677 Compare file sizes (with human-readable numbers):
679 du -d0 -b * | barcat -H
681 Same from formatted results, selecting the first numeric value:
683 tree -s --noreport | barcat -H -f+
685 Compare media metadata, like image size or play time:
687 exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
689 exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
691 find -type f -print0 | xargs -0 -L1 \
692 ffprobe -show_format -of json -v error |
693 jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
695 Memory usage of user processes with long names truncated:
697 ps xo rss,pid,cmd | barcat -l40
699 Monitor network latency from prefixed results:
701 ping google.com | barcat -f'time=\K' -t
703 Commonly used after counting, for example users on the current server:
705 users | tr ' ' '\n' | sort | uniq -c | barcat
707 Letter frequencies in text files:
709 cat /usr/share/games/fortunes/*.u8 |
710 perl -CS -nE 'say for grep length, split /\PL*/, uc' |
711 sort | uniq -c | barcat
713 Number of HTTP requests per day:
715 cat httpd/access.log | cut -d\ -f4 | cut -d: -f1 | uniq -c | barcat
717 Any kind of database query results, preserving returned alignment:
719 echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
722 In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
724 > SELECT schemaname, relname, pg_total_relation_size(relid)
725 FROM pg_statio_user_tables ORDER BY idx_blks_hit
728 Same thing in SQLite (requires the sqlite3 client):
731 > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
733 Earthquakes worldwide magnitude 1+ in the last 24 hours:
735 curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
736 column -ts, -n | barcat -f4 -u -l80%
738 External datasets, like movies per year:
740 curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
741 jq .[].year | uniq -c | barcat
743 Pokémon height comparison:
745 curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L |
746 jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat
748 USD/EUR exchange rate from CSV provided by the ECB:
750 curl https://sdw.ecb.europa.eu/export.do \
751 -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' |
752 barcat -f',\K' --value-length=7
754 Total population history in XML from the World Bank:
756 curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
757 xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
758 barcat -f1 -H --markers=+/1e9
760 Population and other information for all countries:
762 curl http://download.geonames.org/export/dump/countryInfo.txt |
763 grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
765 And of course various Git statistics, such commit count by year:
767 git log --pretty=%ci | cut -b-4 | uniq -c | barcat
769 Or the top 3 most frequent authors with statistics over all:
771 git shortlog -sn | barcat -L3 -s
773 Activity graph of the last days (substitute date C<-v-{}d> on BSD):
775 ( git log --pretty=%ci --since=30day | cut -b-10
776 seq 0 30 | xargs -i date +%F -d-{}day ) |
777 sort | uniq -c | awk '$1--' | barcat --spark
779 Sparkline graphics of simple input given as inline parameters:
781 barcat -_ 3 1 4 1 5 0 9 2 4
783 Misusing the spark functionality to draw a lolcat line:
785 seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow
789 Mischa POSLAWSKY <perl@shiar.org>