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