NAME

IO::Prompt::Hooked - Simple prompting with validation hooks.

SYNOPSIS

use IO::Prompt::Hooked;

# Prompt exactly like IO::Prompt::Tiny
$input = prompt( 'Continue? (y/n)' );       # No default.
$input = prompt( 'Continue? (y/n)', 'y' );  # Defaults to 'y'.

# Prompt with validation.
$input = prompt(
  message  => 'Continue? (y/n)',
  default  => 'n',
  validate => qr/^[yn]$/i,
  error    => 'Input must be "y" or "n".',
);

# Limit number of attempts
$input = prompt(
  message  => 'Continue? (y/n)',
  validate => qr/^[yn]$/i,
  tries    => 5,
  error    => sub {
    my( $raw, $tries ) = @_;
    return "'y' or 'n' only. You have $tries attempts remaining.";
  },
);

# Validate with a callback.
$input = prompt(
  message  => 'Continue? (y/n)',
  validate => sub {
    my $raw = shift;
    return $raw =~ /^[yn]$/i;
  },
  error    => 'Input must be either "y" or "n".',
);

# Give user an escape sequence.
$input = prompt(
  message  => 'Continue? (y/n)',
  escape   => qr/^A$/,
  validate => qr/^[yn]$/i,
  error    => 'Input must be "y" or "n" ("A" to abort input.)',
);

# Break out of allotted attempts early.
$input = prompt(
  message  => 'Continue? (y/n)',
  validate => qr/^[yn]$/i,
  tries    => 5,
  error    => sub {
    my( $raw, $tries ) = @_;
    if( $raw !~ m/^[yn]/ && $tries > 3 ) {
      print "You're not reading the instructions!  Input terminated\n";
      IO::Prompt::Hooked::terminate_input(); # Think 'last', but dirtier.
    }
    return "Must enter a single character, 'y' or 'n'";
  },
);

DESCRIPTION

IO::Prompt::Tiny is a nice module to use for basic prompting. It properly detects interactive sessions, and since it's based on the prompt() routine from ExtUtils::MakeMaker, it's highly portable.

But IO::Prompt::Tiny is intentionally minimal. Often one begins wrapping it in logic to validate input, retry on invalid, limit number of attempts, and alert the user to invalid input. IO::Prompt::Hooked adds simple validation, attempt limiting, and error handling to IO::Prompt::Tiny's minimalism. It does this by allowing you to supply simple Regexp objects for validation, or subroutine callbacks if you need finer control.

"But we already have IO::Prompt for non-trivial needs.", you might be thinking. And you're right. But have you read its POD? It's far from being simple, and is not as portable as this module. IO::Prompt::Hooked aims to provide the portability of IO::Tiny, and easy to use hooks for input validation.

EXPORTS

IO::Prompt::Hooked exports prompt(), and optionally the terminate_input() helper.

SUBROUTINES

prompt

Just like IO::Prompt::Tiny

my $input = prompt( 'Prompt message' );
my $input = prompt( 'Prompt message', 'Default value' );

Or not... (named parameters)

my $input = prompt(
  message  => 'Please enter an integer between 0 and 255 ("A" to abort)',
  default  => '0',
  tries    => 5,
  validate => sub {
    my $raw = shift;
    return $raw =~ m/^[0-9]+$/ && $raw >= 0 && $raw <= 255;
  },
  escape   => qr/^A$/i,
  error    => sub {
    my( $raw, $tries ) = @_;
    return "Invalid input. You have $tries attempts remaining.";
  },
);

Description of named parameters

Named parameters may be passed as a list of key/value pairs, or as a hash-ref containing key/value pairs. prompt is smart enough to figure it out. Named parameters shouldn't be mixed with positional parameters though.

Unless otherwise mentioned, all named parameters are optional.

message

(Optional; empty string used if omitted.)

$input = prompt( message => 'Enter your first name' );

The message that will be displayed to the user ahead of the input cursor. If the session is not interactive, or if the PERL_MM_USE_DEFAULT environment variable is set, output will be suppressed, and the default will be used. If there is no default set, an empty string will be returned.

If message is omitted an empty string is used. This is different from IO::Prompt::Tiny's prompt, as that function throws an exception if no message is passed.

default

(Optional, but usually preferable.)

$input = prompt( message => 'Favorite color', default => 'green' );

An optional default value that will be displayed as [default] to the user, and that will be returned if the user hits enter without providing any input.

Be sure to provide a meaningful default for scripts that might run non-interactively.

validate

(Required only if error, or tries are used.)

$input = prompt( message  => 'Enter a word',
                 validate => qr/^\w+$/      );

$input = prompt( message  => 'Enter a word',
                 validate => sub {
                   my( $raw, $tries_remaining ) = @_;
                   return $raw =~ m/^\w+$/
} );

validate accepts either a Regexp object (created via qr//), or a subroutine reference. The regexp must match, or the sub must return true for the input to be accepted. Any false value will cause input to be rejected, and the user will be prompted again unless tries has been set, and has run out (see tries, below).

The sub callback will be invoked as $valiate_cb->( $raw_input, $tries_remaining ). Thus, the sub you supply has access to the raw (chomped) user input, as well as how many tries are remaining.

tries

(Optional. Only useful if validate is used.)

$input = prompt( message  => 'Proceed?',
                 default  => 'y',
                 validate => qr/^[yn]$/i,
                 tries    => 5,
                 error    => 'Invalid input, please try again.' );

Useful only if input is being validated. By setting a positive number of attempts, the prompt will continue trying to get valid input either until valid input is provided, or the counter reaches zero.. If tries is set to zero, prompt won't prompt, and will return the default if one exists, or undef otherwise.

If tries hasn't been explicitly set, it implicitly starts out at -1 for the first attempt, and counts down, -2, -3, etc. This may be useful to calbacks that need to monitor how many attempts have been made even when no specific limit is imposed.

error

(Optional. Only useful if validate is used.)

$input = prompt( message  => 'Proceed?',
                 validate => qr/^[yn]$/i,
                 error    => "Invalid input.\n" );

$input = prompt( message  => 'Your age?',
                 validate => qr/^[01]?[0-9]{1,2}$/,
                 error    => sub {
                   my( $raw, $tries ) = @_;
                   return 'Roman numerals not allowed'
                     if $raw =~ m/^[IVXLCDM]+$/i;
                   return 'Age must be specified in base-10.'
                     if $raw =~ m/[A-Fa-f]/;
                   return 'Invalid input.'
                 } );

The error field accepts a string that will be printed to notify the user of invalid input, or a subroutine reference that should return a string. The sub-ref callback has access to the raw input and number of tries remaining just like the validate callback. The purpose of the error field is to generate a warning message. But by supplying a subref, it can be abused as you see fit. The callback will only be invoked if the user input fails to validate. Output will be suppressed if the session is interactive, or if the environment variable PERL_MM_USE_DEFAULT is set.

If error is omitted, and a validation fails, there will be no error message.

escape

$input = prompt( message  => 'True or false? (T, F, or S to skip.)',
                 validate => qr/^[tf]$/i,
                 error    => "Invalid input.\n",
                 escape   => qr/^s$/i );

The escape field accepts a regular expression object, or a subroutine reference to be used as a callback. If the regex matches, or the callback returns true, prompt() returns undef immediately. default will be ignored.

As with the other callbacks, the escape callback is invoked as $escape_cb->( $raw, $tries ). The primary use is to give the user an escape sequence. But again, the sub callback opens the doors to much flexibility.

terminate_input

Insert a call to IO::Prompt::Hooked::terminate_input() inside of any callback to force prompt() to return undef immediately. This is essentially a means of placing "last" into your callback without generating a warning about returning from a subroutine via last. It's a dirty trick, but might be useful.

CAVEATS & WARNINGS

Keep in mind that prompting behaves differently in a non-interactive environment. In a non-interactive environment, the default will be used. If no default is set, undef will be returned. If the default matches the escape, undef will be returned. Next, if default fails to validate, then tries will count down until zero is reached, at which time undef will be returned.

If non-interactive mode is detected, and "tries" isn't set to a positive limit, a tries limit of one is automatically set to prevent endless looping in cases where validation doesn't match the default.

CONFIGURATION AND ENVIRONMENT

This module should be highly portable. The environment variable PERL_MM_USE_DEFAULT may be set to prevent IO::Prompt::Hooked from prompting interactively.

This module is expected to work exactly like IO::Prompt::Tiny when invoked in positional parameter (non-named-parameter) mode except that it uses an empty string for the prompt message if no prompt message is supplied as a parameter, rather than throwing an exception. For regression testing the test suite validates behavior against the IO::Prompt::Tiny tests. Overall test coverage for IO::Prompt::Hooked is 100%.

DEPENDENCIES

This module has two non-core dependencies: Params::Smart, and IO::Prompt::Tiny. The test suite requires Capture::Tiny.

INCOMPATIBILITIES

No known incompatibilities.

SEE ALSO

IO::Prompt::Tiny, IO::Prompt

AUTHOR

David Oswald <davido at cpan dot org>

DIAGNOSTICS

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc IO::Prompt::Hooked

This module is maintained in a public repo at Github. You may look for information at:

ACKNOWLEDGEMENTS

LICENSE AND COPYRIGHT

Copyright 2012 David Oswald.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.