use utf8;
use List::Util qw( min max sum );
use open qw( :std :utf8 );
+use re '/msx';
-our $VERSION = '1.06';
+our $VERSION = '1.07';
use Getopt::Long '2.33', qw( :config gnu_getopt );
my %opt;
'field|f=s' => sub {
eval {
local $_ = $_[1];
- $opt{anchor} = /^[0-9]+$/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
- } or die $@ =~ s/(?: at .+)?$/ for option $_[0]/r;
+ $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
+ } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
},
'human-readable|H!',
'interval|t:i',
'trim|length|l=s' => sub {
my ($optname, $optval) = @_;
$optval =~ s/%$// and $opt{trimpct}++;
- $optval =~ m/^-?[0-9]+$/ or die(
+ $optval =~ m/\A-?[0-9]+\z/ or die(
"Value \"$optval\" invalid for option $optname",
" (number or percentage expected)\n"
);
my ($optname, $optval) = @_;
$optval ||= 0;
($opt{hidemin}, $opt{hidemax}) =
- $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/x or die(
+ $optval =~ m/\A (?: ([0-9]+)? - )? ([0-9]+)? \z/ or die(
"Value \"$optval\" invalid for option limit",
" (range expected)\n"
);
exit;
},
'usage|h' => sub {
- local $/;
+ local $/ = undef; # slurp
my $pod = readline *DATA;
- $pod =~ s/^=over\K/ 22/m; # indent options list
- $pod =~ s/^=item \N*\n\n\N*\n\K(?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/msg;
+ $pod =~ s/^=over\K/ 25/; # indent options list
+ $pod =~ s/^=item\ \N*\n\n\N*\n\K (?:(?:^=over.*?^=back\n)?(?!=)\N*\n)*/\n/g;
+ $pod =~ s/[.,](?=\n)//g; # trailing punctuation
+ $pod =~ s/^=item\ \K(?=--)/____/g; # align long options
+ # abbreviate <variable> indicators
+ $pod =~ s/\Q>.../s>/g;
+ $pod =~ s/<(?:number|count|seconds)>/N/g;
+ $pod =~ s/<character(s?)>/\Uchar$1/g;
+ $pod =~ s/\Q | /|/g;
+ $pod =~ s/(?<!\w)<([a-z]+)>/\U$1/g; # uppercase
require Pod::Usage;
- my $parser = Pod::Usage->new;
+ my $parser = Pod::Usage->new(USAGE_OPTIONS => {
+ -indent => 2, -width => 78,
+ });
$parser->select('SYNOPSIS', 'OPTIONS');
$parser->output_string(\my $contents);
$parser->parse_string_document($pod);
$contents =~ s/\n(?=\n\h)//msg; # strip space between items
+ $contents =~ s/^\ \ \K____/ /g; # nbsp substitute
print $contents;
exit;
},
$opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
$opt{markers} //= '=avg >31.73v <68.27v +50v |0';
$opt{palette} //= $opt{color} && [31, 90, 32];
-$opt{input} = @ARGV && $ARGV[0] =~ m/\A[-0-9]/ ? \@ARGV : undef
+$opt{hidemin} = ($opt{hidemin} || 1) - 1;
+$opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
and undef $opt{interval};
my (@lines, @values, @order);
}
my $valmatch = qr<
- $opt{anchor} ( \h* -? [0-9]* \.? [0-9]+ (?: e[+-]?[0-9]+ )? |)
+ $opt{anchor} ( \h* -? [0-9]* [.]? [0-9]+ (?: e[+-]?[0-9]+ )? |)
>x;
while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
s/\r?\n\z//;
- s/^\h*// unless $opt{unmodified};
- push @values, s/$valmatch/\n/ && $1;
- push @order, $1 if length $1;
- if (defined $opt{trim} and defined $1) {
+ s/\A\h*// unless $opt{unmodified};
+ my $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 $1 if $opt{unmodified};
+ $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) = '…';
}
}
$_ = color(@_) . $_ . color(0) if defined;
}
+sub sival {
+ my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
+ my $float = $_[0] !~ /\A0*[-0-9]{1,3}\z/;
+ return sprintf('%3.*f%1s',
+ $float && ($unit % 3) == ($unit < 0), # tenths
+ $_[0] / 1000 ** int($unit/3), # number
+ $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
+ );
+}
+
sub show_lines {
-state $nr = $opt{hidemin} ? $opt{hidemin} - 1 : 0;
-@lines and @lines > $nr or return;
+state $nr = $opt{hidemin};
@lines or return;
-@lines > $nr or return unless $opt{hidemin};
+@lines > $nr or return;
@order = sort { $b <=> $a } @order unless tied @order;
my $maxval = $opt{maxval} // (
$order[0]
) // 0;
my $minval = $opt{minval} // min $order[-1] // (), 0;
+my $range = $maxval - $minval;
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[$_] }
0 .. min $#lines, $opt{hidemax} || (); # left padding
-my $size = ($maxval - $minval) &&
- ($opt{width} - $lenval - $len) / ($maxval - $minval); # bar multiplication
+my $size = defined $opt{width} && $range &&
+ ($opt{width} - $lenval - $len) / $range; # bar multiplication
my @barmark;
if ($opt{markers} and $size > 0) {
return sum(@order) / @order;
}
elsif ($func =~ /\A([0-9.]+)v\z/) {
+ die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
my $index = $#order * $1 / 100;
return ($order[$index] + $order[$index + .5]) / 2;
}
- else {
+ elsif ($func =~ /\A-?[0-9.]+\z/) {
return $func;
}
- } - $minval;
+ else {
+ die "Unknown marker $char: $func\n";
+ }
+ };
+ defined $pos or do {
+ warn $@ if $@;
+ next;
+ };
+ $pos -= $minval;
$pos >= 0 or next;
color(36) for $barmark[$pos * $size] = $char;
}
($lastmax - $minval) * $size + .5,
'-' x (($values[$nr - 1] - $minval) * $size);
print color(92);
- say '+' x (($maxval - $lastmax - $minval) * $size + .5);
+ say '+' x (($range - $lastmax) * $size + .5);
print color(0);
$lastmax = $maxval;
}
}
-@lines > $nr or return if $opt{hidemin};
-
-sub sival {
- my $unit = int(log(abs $_[0] || 1) / log(10) - 3*($_[0] < 1) + 1e-15);
- my $float = $_[0] !~ /^0*[-0-9]{1,3}$/;
- sprintf('%3.*f%1s',
- $float && ($unit % 3) == ($unit < 0), # tenths
- $_[0] / 1000 ** int($unit/3), # number
- $#{$opt{units}} * 1.5 < abs $unit ? "e$unit" : $opt{units}->[$unit/3]
- );
-}
-
say(
color(31), sprintf('%*s', $lenval, $minval),
color(90), '-', color(36), '+',
- color(32), sprintf('%*s', $size * ($maxval - $minval) - 3, $maxval),
+ color(32), sprintf('%*s', $size * $range - 3, $maxval),
color(90), '-', color(36), '+',
color(0),
) if $opt{header};
while ($nr <= $#lines) {
$nr >= $opt{hidemax} and last if defined $opt{hidemax};
my $val = $values[$nr];
- my $rel = length $val && ($val - $minval) / ($maxval - $minval);
+ my $rel = length $val && $range && ($val - $minval) / $range;
my $color = !length $val || !$opt{palette} ? undef :
$val == $order[0] ? $opt{palette}->[-1] : # max
$val == $order[-1] ? $opt{palette}->[0] : # min
if ($opt{spark}) {
say '' if $opt{width} and $nr and $nr % $opt{width} == 0;
print color($color), $opt{spark}->[
- !$val ? 0 : # blank
+ !$val || !$#{$opt{spark}} ? 0 : # blank
$val == $order[0] ? -1 : # max
$val == $order[-1] ? 1 : # min
$#{$opt{spark}} < 3 ? 1 :
}
say $opt{palette} ? color(0) : '' if $opt{spark};
+ return $nr;
}
sub show_stat {
if ($opt{hidemin} or $opt{hidemax}) {
- $opt{hidemin} ||= 1;
- $opt{hidemax} ||= @lines;
- printf '%s of ', sum(grep {length} @values[$opt{hidemin} - 1 .. $opt{hidemax} - 1]) // 0;
+ printf '%s of ', sum(grep { length }
+ @values[$opt{hidemin} .. ($opt{hidemax} || @lines) - 1]
+ ) // 0;
}
if (@order) {
my $total = sum @order;
);
}
say '';
+ return 1;
}
sub show_exit {
Defaults on if output is a tty,
disabled otherwise such as when piped or redirected.
-=item -f, --field=(<number>|<regexp>)
+=item -f, --field=(<number> | <regexp>)
Compare values after a given number of whitespace separators,
or matching a regular expression.
Also changes an exponent I<1.602176634e-19> to I<160.2z>.
Short integers are aligned but kept without decimal point.
-=item -t, --interval[=(<seconds>|-<lines>)]
+=item -t, --interval[=(<seconds> | -<lines>)]
Output partial progress every given number of seconds or input lines.
An update can also be forced by sending a I<SIGALRM> alarm signal.
Override colors of parsed numbers.
Can be any CSI escape, such as I<90> for default dark grey,
-or alternatively I<1;30> for bold black.
+or alternatively I<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 (I<31 90 32>).
+Multiple intermediate colors will be distributed
+relative to the size of values.
+
+Predefined color schemes are named I<whites> and I<fire>,
+or I<greys> and I<fire256> for 256-color variants.
-=item --spark[=<glyphs>]
+=item --spark[=<characters>]
Replace lines by I<sparklines>,
single characters corresponding to input values.
echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
psql -t | barcat -u
+In PostgreSQL from within the client:
+
+ postgres=> SELECT sin(generate_series(0, 3, .1)) \g |barcat
+
Earthquakes worldwide magnitude 1+ in the last 24 hours:
- https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
- column -tns, | graph -f4 -u -l80%
+ curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
+ column -tns, | barcat -f4 -u -l80%
External datasets, like movies per year:
- curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json |
+ curl https://github.com/prust/wikipedia-movie-data/raw/master/movies.json -L |
perl -054 -nlE 'say if s/^"year"://' | uniq -c | barcat
But please get I<jq> to process JSON
Pokémon height comparison:
- curl https://github.com/Biuni/PokemonGO-Pokedex/raw/master/pokedex.json |
+ 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:
Total population history in XML from the World Bank:
- curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL |
+ curl http://api.worldbank.org/v2/country/1W/indicator/SP.POP.TOTL -L |
xmllint --xpath '//*[local-name()="date" or local-name()="value"]' - |
sed -r 's,</wb:value>,\n,g; s,(<[^>]+>)+, ,g' | barcat -f1 -H
git shortlog -sn | barcat -L3 -s
-Activity of the last days (substitute date C<-v-{}d> on BSD):
+Sparkline graphics of simple input given as inline parameters:
+
+ barcat --spark= 3 1 4 1 5 0 9 2 4
+
+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--' | graph --spark
+ sort | uniq -c | awk '$1--' | barcat --spark
=head1 AUTHOR