Hack 14. Simplify Your Terminal Interactions


Read data from users correctly, effectively, and without thinking about it.

Even when you know the right way to handle interactive I/O [Hack #13], the resulting code can still be frustratingly messy:

my $offset; print "Enter an offset: " if is_interactive; GET_OFFSET: while (<>) {     chomp;     if (m/\\A [+-] \\d+ \\z/x)     {         $offset = $_;         last GET_OFFSET;     }     print "Enter an offset (please enter an integer): "         if is_interactive; }

You can achieve exactly the same effect (and much more) with the prompt( ) subroutine provided by the IO::Prompt CPAN module. Instead of all the above infrastructure code, just write:

use IO::Prompt; my $offset = prompt( "Enter an offset: ", -integer );

prompt( ) prints the string you give it, reads a line from standard input, chomps it, and then tests the input value against any constraint you specify (for example, -integer). If the constraint is not satisfied, the prompt repeats, along with a clarification of what was wrong. When the user finally enters an acceptable value, prompt( ) returns it.

Most importantly, prompt( ) is smart enough not to bother writing out any prompts if the application isn't running interactively, so you don't have to code explicitly for that case.

Infrastructure code is code that doesn't actually contribute to solving your problem, but merely exists to hold your program together. Typically this kind of code implements standard low-level tasks that probably ought to have built-ins dedicated to them. Many modules in the standard library and on CPAN exist solely to provide cleaner alternatives to continually rewriting your own infrastructure code. Discovering and using them can significantly decrease both the size and cruftiness of your code.


Train -req

prompt( ) has a general mechanism for telling it what kind of input you need and how to ask for that input. For example:

my $hex_num = prompt( "Enter a hex number> ",           -req => { "A *hex* number please!> " => qr/^[0-9A-F]+$/i }           ); print "That's ", hex($hex_num), " in base 10\\n";

When this code executes, you will see something like:

Enter a hex number> 2B|!2B A *hex* number please!> C3P0 A *hex* number please!> 124C1 That's 74945 in base 10

The -req argument takes a hash reference, in which each value is something to test the input against, and each key is a secondary prompt to print when the test fails. The tests can be regexes (which the input must match) or subroutines (which receive the input as $_ and should return true if that input satisfies the constraint). For example:

my $factor = prompt( "Enter a prime: ",                      -req => { "Try again: " => sub { is_prime($_) } }                    );

Yea or Nay

One particularly useful constraint that prompt( ) supports is a mode that accepts only the letters y or n as input:

if (prompt -YESNO, "Quit? ") {     save_changes($changes)         if $changes && prompt -yes, "Save changes? ";     print "Changes: $changes\\n";     exit; }

The first call to prompt( ) requires the user to type a word beginning with Y or N. It will ignore anything else and return the prompt with an explanation. If the input is Y, the call will return true; if N, it will return false. On the other hand, the second call (with the -yes argument) actually accepts any input. If that string starts with a y or Y, prompt( ) returns true; for any other input, it returns false. For example:

Quit? q Quit? (Please enter 'Y' or 'N') Y Save changes? n Changes: not saved

These different combinations of -YES/-yes/-no/-NO allow for varying degrees of punctiliousness in obtaining the user's consent. In particular, using -YESNO forces users to hit Shift and one of only two possible keys, which often provides enough of a pause to prevent unthinking responses that they'll deeply regret about 0.1 seconds after hitting Enter.

At the Touch of a Button

On the other hand, sometimes it's immensely annoying to have to press Enter at all. Sometimes you want to hit a single key and just let the application get on with things. Thus prompt( ) provides a single character mode:

for my $file (@matching_files) {     next unless prompt -one_char, -yes, "Copy $file? ";     copy($file, "$backup_dir/$file"); }

With -one_char in effect, the first typed character completes the entire input operation. In this case, prompt( ) returns true only if that character was y or Y.

Of course, single character mode can accept more than just y and n. For example, the following call allows the user to select a drive instantly, simply by typing its single character name (in upper- or lowercase):

my $drive = uc prompt "Select a drive: ",                       -one_char,                       -req => { "Please select A-F: " => qr/[A-F]/i };

Engage Cloaking Device

You can tell prompt( ) not to echo input (good for passwords):

my $passwd = prompt( "First password: ", -echo=>"" );

or to echo something different in place of what you actually type (also good for passwords):

my $passwd = prompt( "Second password: ", -echo=>"*" );

This allows you to produce interfaces like:

First password: Second password: ********             

What's On the Menu?

Often you can't rely on users to type in the right responses; it's easier to list them and ask the user to choose. This is menu-driven interaction, and prompt( ) supports various forms of it. The simplest is just to give the subroutine a list of possible responses in an array:

my $device = prompt 'Activate which device?',                     -menu =>                     [                         'Sharks with "laser" beams',                         'Disinhibiter gas grenades',                         'Death ray',                         'Mirror ball',                     ]; print "Activating $device in 10:00 and counting...\\n";

This produces the request:

Activate which device?   a. Sharks with "laser" beams   b. Disinhibiter gas grenades   c. Death ray   d. Mirror ball > q (Please enter a-d) > d Activating Mirror ball in 10:00 and counting...

The menu call to prompt only accepts characters in the range displayed, and returns the value corresponding to the character entered.

You can also pass the -menu option a hash reference:

my $device = prompt 'Initiate which master plan?',                     -menu =>                     {                         Cousteau => 'Sharks with "laser" beams',                         Libido   => 'Disinhibiter gas grenades',                         Friar    => 'Death ray',                         Shiny    => 'Mirror ball',                     }; print "Activating $device in 10:00 and counting...\\n";

in which case it will show the list of keys and return the value corresponding to the key selected:

Initiate which master plan?  a. Cousteau  b. Friar  c. Libido  d. Shiny > d Activating Mirror ball in 10:00 and counting...

You can even nest hashes and arrays:

my $device = prompt 'Select your platform:',                     -menu =>                     {                         Windows => [ 'WinCE', 'WinME', 'WinNT' ],                         MacOS   => {                                      'MacOS 9' => 'Mac (Classic)',                                      'MacOS X' => 'Mac (New Age)',                                    },                         Linux   => 'Linux',                     };

to create hierarchical menus:

Select your platform:  a. Linux  b. MacOS  c. Windows > b MacOS:  a. MacOS 9  b. Mac OS X > b Compiling for Mac (New Age)...



Perl Hacks
Perl Hacks: Tips & Tools for Programming, Debugging, and Surviving
ISBN: 0596526741
EAN: 2147483647
Year: 2004
Pages: 141

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net