#!/usr/bin/perl -CA use 5.014; use warnings; use utf8; use List::Util qw( min max sum ); use open qw( :std :utf8 ); use re '/msx'; our $VERSION = '1.10'; my %opt; if (@ARGV) { require Getopt::Long; Getopt::Long->import('2.33', qw( :config gnu_getopt )); GetOptions(\%opt, 'ascii|a!', 'color|C!', 'M' => sub { $opt{color} = 0 }, 'field|f=s' => sub { eval { local $_ = $_[1]; s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/; s{\A[+]([0-9]*)\z}{ (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)' }e; $opt{anchor} = qr/$_/; } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r; }, 'count|c!', 'human-readable|H!', 'sexagesimal!', 'reformat!', 'interval|t:i', 'trim|length|l=s' => sub { my ($optname, $optval) = @_; $optval =~ s/%$// and $opt{trimpct}++; $optval =~ m/\A-?[0-9]+\z/ or die( "Value \"$optval\" invalid for option $optname", " (number or percentage expected)\n" ); $opt{trim} = $optval; }, 'value-length=i', 'minval=f', 'maxval=f', 'limit|L:s' => sub { my ($optname, $optval) = @_; $optval ||= 0; $optval =~ /\A-[0-9]+\z/ and $optval .= '-'; # tail shorthand $optval =~ s/[+]/--/; my ($start, $end) = $optval =~ m/\A (?: (-? [0-9]+)? - )? (-? [0-9]+)? \z/ or die( "Value \"$optval\" invalid for option limit", " (range expected)\n" ); $start ||= 1; $start--; s/\A-0*\z// and $_ ||= undef for $end // (); $opt{hidemin} = sub { my ($lines) = @_; if ($start < 0) { return max(0, $lines + $start + 2); } return $start; } if $start; $opt{hidemax} = sub { my ($limit, $offset) = @_; if ($end < 0) { return $offset - $end - 1; # count } elsif ($start < 0) { return $limit - $end + 1; # bottom } elsif ($end <= $limit) { return $end - 1; # less } return $limit; } if defined $end; }, 'log|e!', 'header!', 'markers|m=s', 'graph-format=s' => sub { $opt{'graph-format'} = substr $_[1], 0, 1; }, 'spark|_!', 'indicators:s', 'palette=s' => sub { $opt{palette} = { '' => [], fire => [qw( 90 31 91 33 93 97 96 )], fire256=> [map {"38;5;$_"} qw( 235 52 88 124 160 196 202 208 214 220 226 227 228 229 230 231 159 )], whites => [qw( 1;30 0;37 1;37 )], grays => [map {"38;5;$_"} 0, 232..255, 15], random => [map {"38;5;$_"} List::Util::shuffle(17..231)], rainbow=> [map {"38;5;$_"} 196, # r (map { 196 + $_*6 } 0..4), # +g (map { 226 - $_*6*6 } 0..4), # -r (map { 46 + $_ } 0..4), # +b (map { 51 - $_*6 } 0..4), # -g (map { 21 + $_*6*6 } 0..4), # +r (map { 201 - $_ } 0..4), # -b 196, ], }->{$_[1]} // do { my @vals = split /[^0-9;]/, $_[1] or die "Empty palette resulting from \"$_[1]\"\n"; \@vals; }; }, 'stat|s!', 'report=s', 'signal-stat=s', 'unmodified|u!', 'width|w=i', 'version|V' => sub { my $mascot = $opt{ascii} ? '=^,^=' : 'ฅ^•ﻌ•^ฅ'; say "barcat $mascot version $VERSION"; exit; }, 'usage|h' => sub { /^=/ ? last : print for readline *DATA; # text between __END__ and pod exit; }, 'help|?' => sub { require Pod::Usage; Pod::Usage::pod2usage( -exitval => 0, -perldocopt => '-oman', -verbose => 2, ); }, ) or exit 64; # EX_USAGE } $opt{width} ||= $ENV{COLUMNS} || qx(tput cols) || 80 unless $opt{spark}; $opt{color} //= $ENV{NO_COLOR} ? 0 : -t *STDOUT; # enable on tty $opt{'graph-format'} //= '-'; $opt{trim} *= $opt{width} / 100 if $opt{trimpct}; $opt{units} = [split //, ' kMGTPEZYRQqryzafpn'.($opt{ascii} ? 'u' : 'μ').'m'] if $opt{'human-readable'}; $opt{'value-length'} = 4 if $opt{units}; $opt{'value-length'} = 1 if $opt{unmodified}; $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT'; $opt{markers} //= '=avg <31.73v >68.27v +50v |0'; $opt{report} //= join('', '${partsum+; $_ .= " of "}', '${sum+; color(1); $_ .= " total in "}', '${count#} values', '${lines#; $_ = $_ != @order && " over $_ lines"}', sprintf('${count: (%s)}', join ', ', '${0v; color(31)} min', '${avg; $opt{reformat} or $_ = sprintf "%0.2f", $_; color(36)} avg', '${100v; color(32)} max', ), ); $opt{palette} //= $opt{color} && [31, 90, 32]; $opt{indicators} = [split //, $opt{indicators} || ($opt{ascii} ? ' .oO' : $opt{spark} ? ' ▁▂▃▄▅▆▇█' : ' ▏▎▍▌▋▊▉█') ] if defined $opt{indicators} or $opt{spark}; $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef and undef $opt{interval}; $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] }; $opt{'value-format'} = $opt{sexagesimal} ? sub { my $s = abs($_[0]) + .5; sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60); } : $opt{units} && sub { my $unit = ( log(abs $_[0] || 1) / log(10) - 3 * (abs($_[0]) < .9995) # shift to smaller unit if below 1 + 1e-15 # float imprecision ); my $decimal = ($unit % 3) == ($unit < 0); $unit -= log($decimal ? .995 : .9995) / log(10); # rounded $decimal = ($unit % 3) == ($unit < 0); $decimal &&= $_[0] !~ /^-?0*[0-9]{1,3}$/; # integer 0..999 sprintf('%*.*f%1s', 3 + ($_[0] < 0), # digits plus optional negative sign $decimal, # tenths $_[0] / 1000 ** int($unit/3), # number $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) : $opt{units}->[$unit/3] # suffix ); } and $opt{reformat}++; $opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] }; my (@lines, @values, @order, %uniq); $SIG{$_} = \&show_stat for $opt{'signal-stat'} || (); $SIG{ALRM} = sub { show_lines(); alarm $opt{interval} if defined $opt{interval} and $opt{interval} > 0; }; $SIG{INT} = \&show_exit; if (defined $opt{interval}) { $opt{interval} ||= 1; alarm $opt{interval} if $opt{interval} > 0; eval { require Tie::Array::Sorted; tie @order, 'Tie::Array::Sorted', sub { $_[0] <=> $_[1] }; } or warn $@, "Expect slowdown with large datasets!\n" unless $opt{count}; } my $float = qr<[0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )?>; # positive numberish my $valmatch = $opt{anchor} // qr/\A/; $valmatch .= !$opt{count} ? qr/( \h* -? $float |)/ : $opt{anchor} ? qr/(\S*)/ : qr/(.*)/; while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) { s/\r?\n\z//; my $valnum; if ($opt{count}) { $valnum = m/$valmatch/ && $1; $uniq{$valnum}++ and next; push @values, $valnum; s/\A/\n /; } else { s/\A\h*// unless $opt{unmodified}; $valnum = s/$valmatch/\n/ && $1; push @values, $valnum; push @order, $valnum if length $valnum; } if (defined $opt{trim} and defined $valnum) { my $trimpos = abs $opt{trim}; $trimpos -= length $valnum if $opt{unmodified}; if ($trimpos <= 1) { $_ = substr $_, 0, 2; } elsif (length > $trimpos) { # cut and replace (intentional lvalue for speed, contrary to PBP) substr($_, $trimpos - 1) = $opt{ascii} ? '>' : '…'; } } push @lines, $_; } continue { show_lines() if defined $opt{interval} and $opt{interval} < 0 and $. % $opt{interval} == 0; } $SIG{INT} = 'DEFAULT'; sub color { $opt{color} and defined $_[0] or return ''; return "\e[$_[0]m" if defined wantarray; $_ = color(@_) . $_ . color(0) if defined; } sub show_lines { state $nr = $opt{hidemin} ? $opt{hidemin}->($#lines) : 0; @lines > $nr or return; my $limit = $opt{hidemax} ? $opt{hidemax}->($#lines, $nr) : $#lines; if ($opt{count}) { $_ = $uniq{$_} for @values[$nr .. $limit]; @order = @values; } @order = sort { $a <=> $b } @order unless tied @order; my $maxval = $opt{maxval} // ( $opt{hidemax} ? max grep { length } @values[$nr .. $limit] : $order[-1] ) // 0; my $minval = $opt{minval} // min $order[0] // (), 0; my $range = $maxval - $minval; $range &&= log $range if $opt{log}; my $lenval = $opt{'value-length'} // max map { length } @order; my $len = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 : max(map { length $values[$_] && length $lines[$_] } $nr .. $limit) // 0; # left padding my $size = defined $opt{width} && $range && ($opt{width} - $lenval - $len - !!$opt{indicators}); # bar multiplication my @barmark; if ($opt{markers} and $size > 0) { for my $markspec (split /\h/, $opt{markers}) { my ($char, $func) = split //, $markspec, 2; my $increment = $func =~ s/[+]\z//; my @pos = eval { if ($func =~ /\A\/($float)\z/) { my @range = my $multiple = my $next = $1; while ($next < $maxval) { $multiple *= 10 if $opt{log}; push @range, $next += $multiple; } return @range; } return calc($func); } or do { warn "Invalid marker $char: $@" if $@; next; }; for my $pos (@pos) { $pos -= $minval; $pos &&= log $pos if $opt{log}; $pos >= 0 or next; $increment ||= $minval && !$pos; color(36) for $barmark[$pos / $range * $size + $increment + .5] = $char; } } state $lastmax = $maxval; if ($maxval > $lastmax) { print ' ' x ($lenval + $len); print color(90); printf '%-*s', ($lastmax - $minval) * $size / $range + .5, '-' x (($values[$nr - 1] - $minval) * $size / $range); print color(92); say '+' x (($range - $lastmax) * $size / $range + .5); print color(0); $lastmax = $maxval; } } say( color(31), sprintf('%*s', $lenval, $minval), color(90), '-', color(36), '+', color(32), sprintf('%*s', $size - 3, $maxval), color(90), '-', color(36), '+', color(0), ) if $opt{header}; while ($nr <= $limit) { my $val = $values[$nr]; my $rel; if (length $val) { $rel = $val - $minval; $rel &&= log $rel if $opt{log}; $rel = min(1, $rel / $range) if $range; # 0..1 } my $color = !length $val || !$opt{palette} ? undef : $val == $order[-1] ? $opt{palette}->[-1] : # max $val == $order[0] ? $opt{palette}->[0] : # min $opt{palette}->[ $rel * ($#{$opt{palette}} - 1) + 1 ]; my $indicator = $opt{indicators} && $opt{indicators}->[ !length($val) || !$#{$opt{indicators}} ? 0 : # blank $#{$opt{indicators}} < 2 ? 1 : $val >= $order[-1] ? -1 : $rel * ($#{$opt{indicators}} - 1e-14) + 1 ]; if ($opt{spark}) { say '' if $opt{width} and $nr and $nr % $opt{width} == 0; print color($color), $_ for $indicator; next; } print $indicator if defined $indicator; if (length $val) { $val = sprintf("%*s", $lenval, $opt{reformat} ? $opt{'value-format'}->($val) : $val ); color($color) for $val; } my $line = $lines[$nr] =~ s/\n/$val/r; if (not length $val) { say $line; next; } printf '%-*s', $len + length($val), $line; if ($rel and $size) { print $barmark[$_] // $opt{'graph-format'} for 1 .. $rel * $size + .5; } say ''; } continue { $nr++; } say $opt{palette} ? color(0) : '' if $opt{spark}; %uniq = () if $opt{interval} and $opt{count}; return $nr; } sub show_stat { my %vars = ( partsum => undef, count => int @order, lines => int @lines, ); my $linemin = !$opt{hidemin} ? 0 : ($vars{start} = $opt{hidemin}->($#lines)); my $linemax = !$opt{hidemax} ? $#lines : ($vars{end} = $opt{hidemax}->($#lines, $vars{start})); if (@order) { $vars{partsum} = sum(0, grep {length} @values[$linemin .. $linemax]) if $linemin <= $linemax and ($opt{hidemin} or $opt{hidemax}); } say varfmt($opt{report}, \%vars); return 1; } sub calc { my ($func) = @_; if ($func eq 'avg') { return calc('sum') / @order; } elsif ($func eq 'sum') { state $cache; # avoid recount state $cachednr = 0; # if unchanged unless (@order == $cachednr) { $cache = sum(@order); $cachednr = @order; } return $cache; } elsif ($func =~ /\A([0-9.]+)v\z/) { $1 <= 100 or die( "percentile $1 out of bounds\n" ); my $index = $#order * $1 / 100; my $f = $index - int $index; my $val = $order[$index]; if ($f) { my $next = $order[$index + 1]; $val -= $f * ($val - $next); } return $val; } elsif ($func =~ /\A-?[0-9.]+\z/) { return $func; } else { die "$func unknown\n"; } } sub varfmt { my ($fmt, $vars) = @_; $fmt =~ s[\$\{ \h*+ ((?: [^{}]++ | \{(?1)\} )+) \}]{ my ($name, $op, $cmd) = split /\s*([;:])/, $1, 2; my $format = $name =~ s/\+// || $name !~ s/\#// && $opt{reformat}; local $_ = exists $vars->{$name} ? $vars->{$name} : calc($name); defined && do { $_ = $opt{'value-format'}->($_) if $format; if ($cmd and $op eq ':') { $_ = !!$_ && varfmt($cmd, $vars); } elsif ($cmd) { eval $cmd; warn "Error in \$$name report: $@" if $@; } $_; } }eg; return $fmt; } sub show_exit { show_lines(); show_stat() if $opt{stat}; exit 130 if @_; # 0x80+signo exit; } show_exit(); __END__ Usage: /\_/\ barcat [OPTIONS] [FILES|NUMBERS] (=•.•=) (u u) Options: -a, --[no-]ascii Restrict user interface to ASCII characters -C, --[no-]color Force colored output of values and bar markers -c, --count Omit repetitions and count the number of occurrences -f, --field=([+]N|REGEXP) Compare values after a given number of whitespace separators --header Prepend a chart axis with minimum and maximum values labeled -H, --human-readable Format values using SI unit prefixes --sexagesimal Convert seconds to HH:MM:SS time format -t, --interval[=(N|-LINES)] Output partial progress every given number of seconds or input lines -l, --length=[-]SIZE[%] Trim line contents (between number and bars) -L, --limit=[N|[-]START(-[END]|+N)] Select a range of lines to display -e, --log Logarithmic (exponential) scale instead of linear --graph-format=CHAR Glyph to repeat for the graph line -m, --markers=FORMAT Statistical positions to indicate on bars --min=N, --max=N Bars extend from 0 or the minimum value if lower --palette=(PRESET|COLORS) Override colors of parsed numbers -_, --spark Replace lines by sparklines --indicators[=CHARS] Prefix a unicode character corresponding to each value -s, --stat Total statistics after all data -u, --unmodified Do not reformat values, keeping leading whitespace --value-length=SIZE Reserved space for numbers -w, --width=COLUMNS Override the maximum number of columns to use -h, --usage Overview of available options --help Full pod documentation -V, --version Version information =encoding utf8 =head1 NAME barcat - concatenate texts with graph to visualize values =head1 SYNOPSIS B [I] [I... | I] =head1 DESCRIPTION Visualizes relative sizes of values read from input (parameters, file(s) or STDIN). Contents are concatenated similar to I, but numbers are reformatted and a bar graph is appended to each line. It can has various options for input and output (re)formatting, but remains limited to one-dimensional charts. For more complex graphing needs you'll need a larger animal like I. =head1 OPTIONS =over =item B<-a>, B<-->[B]B Restrict user interface to ASCII characters, replacing default UTF-8 by their closest approximation. Input is always interpreted as UTF-8 and shown as is. =item B<-C>, B<-->[B]B Force colored output of values and bar markers. Defaults on if output is a tty, disabled otherwise such as when piped or redirected. Can also be disabled by setting B<-M> or the I environment variable. =item B<-c>, B<--count> Omit repetitions and count the number of occurrences. Similar to piping input through C but keeping the order of first appearances. Lines are omitted if they (or a specified field) are identical, and the amount of matches is prepended and used as values for bars and subsequent statistics. =item B<-f>, B<--field>=([B<+>]I | I) Compare values after a given number of whitespace separators, or matching a regular expression. Unspecified or B<-f0> means values are at the start of each line. With B<-f1> the second word is taken instead. A string can indicate the starting position of a value (such as B<-f:> if preceded by colons), or capture the numbers itself, for example B<-f'(\d+)'> for the first digits anywhere. A shorthand for this is C<+0>, or C<+N> to find the Nth number. =item B<--header> Prepend a chart axis with minimum and maximum values labeled. =item B<-H>, B<--human-readable> Format values using SI unit prefixes, turning long numbers like C<12356789> into C<12.4M>. Also changes an exponent C<1.602176634e-19> to C<160.2z>. Short integers are aligned but kept without decimal point. =item B<--sexagesimal> Convert seconds to HH:MM:SS time format. =item B<-t>, B<--interval>[=(I | B<->I)] Output partial progress every given number of seconds or input lines. An update can also be forced by sending a I alarm signal. =item B<-l>, B<--length>=[B<->]I[B<%>] Trim line contents (between number and bars) to a maximum number of characters. The exceeding part is replaced by an abbreviation sign, unless B<--length=0>. Prepend a dash (i.e. make negative) to enforce padding regardless of encountered contents. =item B<-L>, B<--limit>=[I | [B<->]I(B<->[I] | B<+>I)] Select a range of lines to display. A single integer indicates the last line number (like I), or first line counting from the bottom if negative (like I). A range consists of a starting line number followed by either a dash C<-> to an optional end, or plus sign C<+> with count. All hidden input is still counted and analyzed for statistics, but disregarded for padding and bar size. =item B<-e>, B<--log> Logarithmic (Bxponential) scale instead of linear to compare orders of magnitude. =item B<--graph-format>=I Glyph to repeat for the graph line. Defaults to a dash C<->. =item B<-m>, B<--markers>=I Statistical positions to indicate on bars. A single indicator glyph precedes each position: =over 2 =item I Exact value to match on the axis. A vertical bar at the zero crossing is displayed by C<|0> for negative values. For example C<π3.14> would locate pi. =item BI Repeated at every multiple of a number. For example C<:/1> for a grid at every integer. =item IB Ranked value at the given percentile, or score at or below which a percentage falls in its frequency distribution (inclusive). The default shows C<+> at C<50v> for the mean or median: the middle value or interpolation between two values. One standard deviation above the median is at about C<68v>. The default includes C<< <31.73v >68.27v >> to encompass all I results, or 68% of all entries, by I<< <--> >>. =item B Matches the average (arithmetic mean); the sum of all values divided by the number of counted lines. Indicated by default as C<=>. =back =item B<--min>=I, B<--max>=I Bars extend from 0 or the minimum value if lower, to the largest value encountered. These options can be set to customize this range. =item B<--palette>=(I | I...) Override colors of parsed numbers. Can be any CSI escape, such as C<90> for default dark gray, or alternatively C<1;30> for bright black. In case of additional colors, the last is used for values equal to the maximum, the first for minima. If unspecified, these are green and red respectively (C<31 90 32>). Multiple intermediate colors will be distributed relative to the size of values. A non-numeric name can refer to a predefined color scheme: =over 8 =item B Minimal set of monochrome brightnesses. =item B Utilize the 24 grayscale ramp in 256-color terminals. =item B Gradient red to white in 7 out of 16 colors. =item B Extended to 17 colors out of 256. =item B Saturated red to green to blue to red. =item B All 215 extended colors in unrelated orders. =back =item B<-_>, B<--spark> Replace lines by I, single characters (configured by B<--indicators>) corresponding to input values. =item B<--indicators>[=I] Prefix a unicode character corresponding to each value. The first specified character will be used for non-values, the remaining sequence will be distributed over the range of values. Unspecified, block fill glyphs U+2581-2588 will be used. =item B<-s>, B<--stat> Total statistics after all data. While processing (possibly a neverending pipe), intermediate results are also shown on signal I if available (control+t on BSDs) or I otherwise (ctrl+\ on linux). =item B<-u>, B<--unmodified> Do not reformat values, keeping leading whitespace. Keep original value alignment, which may be significant in some programs. =item B<--value-length>=I Reserved space for numbers. =item B<-w>, B<--width>=I Override the maximum number of columns to use. Appended graphics will extend to fill up the entire screen, otherwise determined by the environment variable I or by running the I command. =item B<-h>, B<--usage> Overview of available options. =item B<--help> Full pod documentation as rendered by perldoc. =item B<-V>, B<--version> Version information. =back =head1 EXAMPLES Draw a sine wave: seq 30 | awk '{print sin($1/10)}' | barcat Compare file sizes (with human-readable numbers): du -d0 -b * | barcat -H Same from formatted results, selecting the first numeric value: tree -s --noreport | barcat -H -f+ Compare media metadata, like image size or play time: exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal find -type f -print0 | xargs -0 -L1 \ ffprobe -show_format -of json -v error | jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex Memory usage of user processes with long names truncated: ps xo rss,pid,cmd | barcat -l40 Monitor network latency from prefixed results: ping google.com | barcat -f'time=\K' -t Commonly used after counting, eg letter frequencies in text files: cat /usr/share/games/fortunes/*.u8 | perl -CS -nE 'say for grep length, split /\PL*/, uc' | sort | uniq -c | barcat Users on the current server while preserving order: users | tr ' ' '\n' | barcat -c Number of HTTP requests per day: barcat -cf'\[([^:]+)' httpd/access.log Any kind of database query results, preserving returned alignment: echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' | psql -t | barcat -u In PostgreSQL from within the client; a fancy C<\dt+> perhaps: > SELECT schemaname, relname, pg_total_relation_size(relid) FROM pg_statio_user_tables ORDER BY idx_blks_hit \g |barcat -uHf+ Same thing in SQLite (requires the sqlite3 client): > .once |barcat -Hf+ > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1; Earthquakes worldwide magnitude 1+ in the last 24 hours: curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv | column -ts, -n | barcat -f4 -u -l80% External datasets, like movies per year: curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L | jq .[].year | uniq -c | barcat Pokémon height comparison: curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json -L | jq -r '.pokemon[] | [.height,.num,.name] | join(" ")' | barcat USD/EUR exchange rate from CSV provided by the ECB: curl https://sdw.ecb.europa.eu/export.do \ -Gd 'node=SEARCHRESULTS&q=EXR.D.USD.EUR.SP00.A&exportType=csv' | barcat -f',\K' --value-length=7 Total population history in XML from the World Bank: curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL | xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n | barcat -f1 -H --markers=+/1e9 Population and other information for all countries: curl http://download.geonames.org/export/dump/countryInfo.txt | grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s And of course various Git statistics, such commit count by year: git log --pretty=%ci | cut -b-4 | uniq -c | barcat Or the top 3 most frequent authors with statistics over all: git shortlog -sn | barcat -L3 -s Activity graph of the last days (substitute date C<-v-{}d> on BSD): ( git log --pretty=%ci --since=30day | cut -b-10 seq 0 30 | xargs -i date +%F -d-{}day ) | sort | uniq -c | awk '$1--' | barcat --spark Sparkline graphics of simple input given as inline parameters: barcat -_ 3 1 4 1 5 0 9 2 4 Misusing the spark functionality to draw a lolcat line: seq $(tput cols) | barcat --spark --indicator=- --palette=rainbow =head1 AUTHOR Mischa POSLAWSKY =head1 LICENSE GPL3+.