The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Term::Prompt;
require 5.000;
require Exporter;
use Carp;

use Text::Abbrev;
use Text::Wrap;
use Term::ReadKey qw(GetTerminalSize);

BEGIN: {
  $VERSION = '0.05';
}

@ISA = qw(Exporter);
@EXPORT = qw(prompt);
@EXPORT_OK = qw(rangeit legalit typeit exprit yesit);

sub prompt ($$$$;@) {
  my($debug) = 0;		# debugging
  
  my($mopt, $prompt, $prompt_options, $help_prompt, $default, @things);
  my($repl, $match_options, $case, $low, $high, $before, $regexp,
     $prompt_full);
  
  # Figure out just what we are doing here
  $mopt = $_[0];
  print "mopt is: $mopt\n" if $debug;
  
  # check the size of the match option, it should just have one char.
  croak "Illegal call in im_prompt2 prompter."
    if ( length($mopt) > 1 );
  
  my($type) = 0;
  my($legal) = 0;
  my($range) = 0;
  my($expr) = 0;
  my($yn) = 0;
  my($uc) = 0;
  
  if ($mopt ne lc($mopt)) {
    $uc = 1;
    $mopt = lc($mopt);
  }

  if ( $mopt eq "x" || $mopt eq "a" || $mopt eq "n" || $mopt eq "f" ) {
    # More efficient this way - Allen
    ($mopt, $prompt, $prompt_options, $default) = @_;
    $type = 1;
  } elsif ( $mopt eq "c" || $mopt eq "i" ) {
    ($mopt, $prompt, $prompt_options, $default, @things) = @_;
    $legal = 1;
  } elsif ( $mopt eq "r" ) {
    ($mopt, $prompt, $prompt_options, $default, $low, $high) = @_;
    $range = 1;
  } elsif ( $mopt eq "e" ) {
    ($mopt, $prompt, $prompt_options, $default, $regexp) = @_;
    $expr = 1;
  } elsif ( $mopt eq "y" ) {
    ($mopt, $prompt, $prompt_options, $default) = @_;
    $yn = 1;
    if ((!defined($prompt_options)) || ($prompt_options eq '')) {
      if ($uc) {
	$prompt_options = "Enter y or n";
      } else {
	$prompt_options = "y or n";
      }
    }
    
    if (defined($default)) {
      unless ($default =~ m/^[ynYN]/) {
	if ($default) {
	  $default = "y";
	} else {
	  $default = "n";
	}
      }
    } else {
      $default = "n";
    }
  } else {
    croak "prompt type $mopt not recognized";
  }

  $ok = 0;
  $before = 0;
  
  while (1) {
    
    $prompt_full = "$prompt ";
    unless ($before || $uc || ($prompt_options eq '')) {
      $prompt_full .= "($prompt_options) ";
    }
    
    if ($default ne '') {
      $prompt_full .= "[default $default] ";
    }
    
    # print out the prompt string in all its gore
    print termwrap($prompt_full);

    my($old_divide) = undef;

    if (defined($/)) {
      $old_divide = $/;
    }

    $/ = "\n";

    $repl = scalar(readline(*STDIN));

    if (defined($old_divide)) {
      $/ = $old_divide;
    } else {
      undef($/);
    }

    chomp($repl);          # nuke the <CR>
    
    $repl =~ s/^\s*//;	# ignore leading white space
    $repl =~ s/\s*$//;	# ignore trailing white space
    
    $repl = $default if $repl eq '';
    
    if (($repl eq '') && (! $uc)) {
      # so that a simple return can be an end of a series of prompts - Allen
      print "Invalid option\n"; 
      next;
    }

    print termwrap("Reply: '$repl'\n") if $debug;
    
    # Now here is where things get real interesting

    if ($uc && ($repl eq '')) {
      $ok = 1;
    } elsif ( $type ) {
      $ok = &typeit(lc($mopt), $repl, $debug, $uc);
    } elsif ( $legal ) {
      $repl = $ok = &legalit(lc($mopt), $repl, $uc, @things);
    } elsif ( $range ) {
      $ok = &rangeit($repl, $low, $high, $uc);
    } elsif ( $expr ) { 
      $ok = &exprit($repl, $regexp, $prompt_options, $debug);
    } elsif ( $yn ) {
      ($ok,$repl) = &yesit($repl, $uc, $debug);
    } else {
      croak "No subroutine known for prompt option $mopt.";
    }
    
    if ( $ok ) {
      return $repl;
    } elsif ( $prompt_options ne '' ) {
      if ($uc) {
	print termwrap("$prompt_options\n");
      } else {
	print termwrap("Options are: $prompt_options\n");
	$before = 1;
      }
    }
  }
}

sub rangeit ($$$$) {
  # this routine makes sure that the reply is within a given range 

  my($repl, $low, $high, $uc) = @_;

  if ( $low <= $repl && $repl <= $high ) { 
    return 1;
  } elsif (! $uc) {
    print "Invalid range value.  ";
  }
  return 0;
}

sub legalit ($$$@) {
  # this routine checks to see if a repl is one of a set of "things"
  # it checks case based on c = case check, i = ignore case
  
  my($mopt, $repl, $uc, @things) = @_;
  my(@match,$tmp) = ();
  
  local(%abbrevhash) = ();
  abbrev(*abbrevhash,@things);
  
  if ( $mopt eq "i" ) {
    my(%abbrevhash2) = %abbrevhash;
    foreach $key (keys %abbrevhash2) {
      $abbrevhash{lc($key)} = $abbrevhash{$key};
    }
    $repl = lc($repl);
  }
  
  if (exists ($abbrevhash{$repl})) {
    return $abbrevhash{$repl};
  } elsif (! $uc) {
    print "Invalid.  ";
  }
  return 0;
}

sub typeit ($$$$) {
  # this routine does checks based on the following:
  # x = no checks, a = alpha only, n = numeric only
  
  my ($mopt, $repl, $debug, $uc) = @_;
  
  print "inside of typeit\n" if $debug;
  
  if ( $mopt eq "x" ) { return 1; }
  
  if ( $mopt eq "a" ) {
    if ( $repl =~ /^[a-zA-Z]*$/ ) { 
      return 1;
    } elsif (! $uc) {		
      print "Invalid type value.  ";
    }
    return 0;
  }

  if ( $mopt eq "n" ) {
    if ( $repl =~/^[0-9]*$/ ) { 
      return 1; 
    } elsif (! $uc) {
      print "Invalid numeric value.  ";
    }
    return 0;
  }

  if ( $mopt eq "f" ) {
    if (( $repl =~ m/^-?[0-9]+\.?[0-9]*$/ ) ||
	( $repl =~ m/^-?[0-9]*\.[0-9]+$/ )) {
      return 1;
    } elsif (! $uc) {
      print "Invalid floating point value.  ";
    }
  }
}

sub exprit ($$$$) {
  # This routine does checks based on whether something
  # matches a supplied regexp - Allen
  my ($repl, $regexp, $prompt_options, $debug) = @_;
  print "inside of exprit\n" if $debug;
  
  if ( $repl =~ /^$regexp$/ ) {
    return 1;
  } elsif ( $prompt_options eq '') {
    print termwrap("Reply needs to match regular expression /^$regexp$/.\n");
    return 0;
  }
}

sub yesit ($$$) {
  # basic yes or no - Allen
  my ($repl, $uc, $debug) = @_;
  print "inside of yesit\n" if $debug;
  
  if ($repl =~ m/^[0nN]/) {
    return 1,0;
  } elsif ($repl =~ m/^[1yY]/) {
    return 1,1;
  } elsif (! $uc) {
    print "Invalid yes or no response. ";
  }
  return 0,0;
}

sub termwrap ($;@) {
  my($message) = "";
  if ($#_ > 0) {
    if (defined($,)) {
      $message = join($,,@_);
    } else {
      $message = join(" ",@_);
    }
  } else {
    $message = $_[0];
  }

  my($width) = eval {
    local($SIG{__DIE__});
    (GetTerminalSize(select))[0];
  } || eval {
    local($SIG{__DIE__});
    (GetTerminalSize(STDOUT))[0];
  } || eval {
    local($SIG{__DIE__});
    (GetTerminalSize(STDERR))[0];
  };

  if ($width) {
    $Text::Wrap::Columns = $width;
  }

  if ($message =~ m/\n\Z/) {
    $message = wrap("", "\t", $message);
    $message =~ s/\n*\Z/\n/;
    return $message;
  } else {
    $message = wrap("", "\t", $message);
    $message =~ s/\n*\Z//;
    return $message;
  }
}

1;


__END__

=head1 NAME

Term::Prompt - Perl extension for prompting a user for information

=head1 SYNOPSIS

    use Term::Prompt;

=head1 DESCRIPTION

 This perl routine will take a prompt, a default response and a list of
 possible responses and deal with the user interface, (and the user!),
 by displaying the prompt, showing the default, and checking to be sure
 that the response is one of the legal choices.
 --Mark Henderson


 Derived from im_prompt2.pl, from anlpasswd (see
 ftp://info.mcs.anl.gov/pub/systems/), with permission. Revisions for Perl 5,
 addition of alternative help text presentation, addition of floating point
 type, addition of regular expression type, addition of yes/no type, and line
 wrapping by E. Allen Smith.

 Additional "types" that could be added would be a phone type,
 a social security type, a generic numeric pattern type...

 The usage is the following:
 x = don't care, a = alpha-only, n = numeric-only, i = ignore case
 c = case sensitive, r = ranged by the low and high values
 f = floating-point, y = yes/no, e = regular expression - Added by Allen

 $result = &prompt("x", "text prompt", "help prompt", "default" );

 $result = &prompt("a", "text prompt", "help prompt", "default" );

 $result = &prompt("n", "text prompt", "help prompt", "default" );

 The result will be a positive integer or 0.

 $result = &prompt("i", "text prompt", "help prompt", "default",
	                 "legal_options-ignore-case-list");

 $result = &prompt("c", "text prompt", "help prompt", "default",
	                 "legal_options-case-sensitive-list");

 $result = &prompt("r", "text prompt", "help prompt", "default",
                       "low", "high");

 $result = &prompt("f", "text prompt", "help prompt", "default" );

 The result will be a floating-point number.

 $result = &prompt("y", "text prompt", "help prompt", "default")

 The result will be 1 for y, 0 for n. A default not starting with y or n
 (or the uc versions of these) will be treated as y for positive, n for
 negative.

 $result = &prompt("e", "text prompt", "help prompt", "default",
                       "regular expression");

 The regular expression for the last has ^ and $ surrounding it automatically;
 just put in .* before or after if you need to free it up before or
 after. - Allen

 What, you might ask, is the difference between a "text prompt" and a
 "help prompt"?  Think about the case where the "legal_options" look 
 something like: "1-1000".  Now consider what happens when you tell someone
 that "0" is not between 1-1000 and that the possible choices are:  :)
 1 2 3 4 5 .....
 This is what the "help prompt" is for.

 It will work off of unique parts of "legal_options".

 Changed by Allen - it will be treated as a true "help prompt" if you
 capitalize the type of prompt, and otherwise will be treated as a list of
 options. Capitalizing the type of prompt will also mean that a return may be
 accepted as a response, even if there is no default; whether it actually is
 will depend on the type of prompt.

=head1 AUTHOR

Mark Henderson (henderson@mcs.anl.gov or systems@mcs.anl.gov)
Primary contact author: Allen Smith (easmith@beatrice.rutgers.edu)

=head1 SEE ALSO

perl(1).

=cut