#!/usr/bin/env perl
use strict;
use warnings;
use autodie;
use IO::Handle;
our $VERSION = '0.01';
# It's not very useful to have the user type in lines to then select...
exit if -t *STDIN;
# We need to talk to the user directly. stdin and stdout are what we're
# filtering.
open my $in, '<', '/dev/tty';
open my $out, '>', '/dev/tty';
$out->autoflush(1);
my @lines;
my @is_selected;
my $cursor = 0;
my $done = 0;
my $counted = 0;
my %command_table;
%command_table = (
j => {
command => sub { ++$cursor },
documentation => "skip to the next line",
},
k => {
command => sub { --$cursor },
available_when => sub { $cursor > 0 },
documentation => "back up to the previous line",
},
y => {
command => sub { $is_selected[$cursor] = 1; ++$cursor },
documentation => "pass this line through",
},
n => {
command => sub { $is_selected[$cursor] = 0; ++$cursor },
documentation => "don't pass this line through",
},
d => {
command => sub { $done = 1 },
documentation => "print selected lines, skipping all remaining lines",
},
q => {
command => sub { exit },
documentation => "cancel ask, passing no lines through",
},
c => {
command => sub {
# Force the rest of stdin, so we can get a count of how many lines
# need to be decided upon
push @lines, <>;
$counted = 1;
},
available_when => sub { !$counted },
documentation => "read the rest of input to count its size",
},
'?' => {
command => sub {
for my $key (sort keys %command_table) {
printf "%s: %s\n", $key, $command_table{$key}{documentation};
}
print "<Space>: accept the current default (which is capitalized)\n";
},
documentation => "show this help",
},
);
while (!$done) {
my $c = get_input();
last if !defined($c);
run_command($c);
}
# Print selected lines!
print map { $lines[$_] }
grep { $is_selected[$_] }
0 .. $#lines;
sub get_input {
# If the cursor is beyond the already-read lines, lazily read the next
# line.
push @lines, scalar <> until @lines > $cursor;
# We hit EOF, so we're done getting input from the user.
return if !defined($lines[-1]);
prompt_user();
my $c = read_key();
# We always want to add a blank line before any more output, since our
# prompt didn't add a newline.
tell_user("\n");
return $c;
}
sub run_command {
my $c = shift;
# Space uses the default.
$c = default_command() if $c eq ' ';
return tell_user("Invalid response, try again!\n")
if !exists($command_table{$c});
return tell_user("That command is currently unavailable.\n")
if exists($command_table{$c}{available_when})
&& !$command_table{$c}{available_when}->();
$command_table{$c}{command}->();
}
sub read_key {
# Term::ReadKey operates on STDIN.
local *STDIN = $in;
use Term::ReadKey;
ReadMode(3); # Single-character input, without echo
my $c = ReadKey;
ReadMode(0); # Restore regular readline
return $c;
}
sub tell_user {
print { $out } @_;
}
sub prompt_user {
# current line
tell_user($lines[$cursor]);
tell_user("Shall I pass this line through? ");
my $current = $cursor + 1; # start from 1 not 0
my $max = $counted ? @lines : '?';
tell_user("($current/$max) ");
my $default = default_command();
# Construct the list of available commands.
my $keys = join '',
# Capitalize default command
map { $_ eq $default ? uc($default) : $_ }
sort
# Don't display help command
grep { $_ ne '?' }
# Hide unavailable commands
grep {
exists($command_table{$_}{available_when})
? $command_table{$_}{available_when}->()
: 1
}
# All commands
keys %command_table;
tell_user("[$keys]> ");
}
sub default_command { $is_selected[$cursor] ? 'y' : 'n' }
__END__
=head1 NAME
ask - filters stdin by asking the user for each line
=head1 DESCRIPTION
C<ask> is a utility that filters stdin to stdout not by any automatic process,
but by prompting the user about each line.
=head1 AUTHOR
Shawn M Moore, C<sartak at bestpractical dot com>
=head1 PREREQUISITES
autodie
Term::ReadKey
=pod SCRIPT CATEGORIES
UNIX/System_administration
=cut