#!perl -w # ask.pl - Will Mengarini - Tu 22 Oct 96 package main; # Change history follows __END__. # This is a Perl4-compatible package for line-oriented user interaction. # For example, to ask a yes/no question or prompt for a number, you could # use the functions defined here. # The engine of the package is &askValue, which is a fully-parameterized # sub that can prompt for any kind of answer, with or without a default # response, check that the response conforms to any criteria, & return it # if it does or rebuke the user & try again if it doesn't. # &askValue takes these args: # $_[0]: the prompt to be displayed to the user # $_[1]: a regexp that the user's response must match # $_[2]: a human-language description of what that regexp must match # $_[3]: an optional "validator" expression to further constrain responses # $_[4]: an optional input file handle replacing STDIN # $_[5]: an optional output file handle replacing STDOUT # Note that there's no arg for the default value; &askValue parses $_[0] # for a string inside brackets, & if that matches $_[1], uses it as # the default. So a prompt like "What's the frequency, Kenneth [666Hz]? " # would have a default of '666Hz'. Note also that $_[1] will automatically # be supplied with a leading "^" & a trailing "$", so $_[1] must match the # user's entire response, not just a substring of it. # Although &askValue does all the work, it's actually the least useful # sub in the package, because all those args are a nuisance to code. # Instead, it's better to use subs that call &askValue, plugging in some # of the parameters. The most useful sub in the package is &askYN, which # only takes these args: # $_[0]: the prompt to be displayed to the user # $_[1]: an optional input file handle replacing STDIN # $_[2]: an optional output file handle replacing STDOUT # In practice, only $_[0] is normally used. Example: # &explode if( &askYN( "Destroy computer (Y/N) [N]? " ) ); # Note that the default here is "N" (recommended for such questions). # The user may respond in either upper or lower case. Another feature # of &askValue is that it parses its $_[1], the pattern, to see whether # it matches exactly one character; if so, it does console input in a # manner that doesn't require the user to hit after the character. # (However, this only happens if input is coming from a terminal-like # device; if you're reading from a file used in some interactive script, # you do need a newline after the "y" or "n".) Since &askYN just calls # &askValue, it shares this feature. &askYN returns a boolean rather than # the user's response, so you can use it directly in a conditional. # The code for &askYN is extremely simple. It's worth understanding, so you # can write your own &askFoo functions as needed. Here it is: sub askYN { local( $prompt, $IN, $OUT ) = @_; &askValue( $prompt, '[yYnN]', "either \"y\" or \"n\"", undef, $IN, $OUT ) =~ /[yY]/; } # Refer to &askValue's arg list above to make sure you understand what's # happening in &askYN. Note how &askValue's return value is tested # with =~ to produce &askYN's boolean value. # Coding a sub that prompts for an integer is almost as trivial. # &askInteger takes these args: # $_[0]: the prompt to be displayed to the user # $_[1]: an optional "validator" expression to further constrain responses # $_[2]: an optional input file handle replacing STDIN # $_[3]: an optional output file handle replacing STDOUT # The only new arg is the "validator" expression, which is used for things # like requiring the answer to be in a particular range. It'll be explained # later. Defaults work the same way as for &askYN. Here's the code: sub askInteger { local( $prompt, $validator, $IN, $OUT ) = @_; &askValue( $prompt, '-?\d+([Ee]-?\d+)?', "an integer", $validator, $IN, $OUT ) + 0; } # All that really changed from &askYN was that $validator is passed on, # and the pattern is more complicated. To accept a floating-point number, # the only thing that changes from &askInteger is the pattern: sub askNumber { local( $prompt, $validator, $IN, $OUT ) = @_; &askValue( $prompt, '-?(\d+(\.\d*)?|\.\d+)([Ee]-?\d+)?', "a number", $validator, $IN, $OUT ) + 0; } # Note that both of those subs numericize &askValue's return by adding 0. # &askValue & some of its derivatives take a $validator argument for the # purpose of further constraining the values to be accepted; e.g., &askNumber # can be given a $validator constraining it to accept only probabilities. # $validator is a string to be evalled. For the example of probabilities, # it could be '$value >= 0 && $value <= 1'. When $validator is evalled, # $value will contain the user's most recent response. Example: # $p = &askNumber( "Enter a probability: ", '$value >= 0 && $value <= 1' ) # Note that when the string is denoted, it must be SINGLE-quoted to prevent # "$value" from being interpolated (presumably to '') at that point; you # want interpolation to wait until &askValue does it. # If the user's response doesn't satisfy $validator, &askValue will say # something like 'Condition "50 >= 0 && 50 <= 1" is false'. Since this may # be obscure to some users, it's possible to define a function that returns # a clearer error message as the second element of a list whose first element # is whether the response was valid. Here are some built-in examples. # You can use them as a model for your own validator functions, or actually # use them directly in an invocation like # $n = &askNumber( "Enter a # in 1 to 10: ", '&inRange(1,10)' ) # in which $validator is still a string to be evalled, but now is just # an invocation rather than a boolean expression. Be careful to keep # $validator quoted so it doesn't get executed at the point of # invocation of &askValue. It'll need to be single-quoted if its invocation # contains references to $value. sub inRange { local( $min, $max ) = @_; if( $value < $min || $value > $max ){ (0, "$value isn't in $min..$max.\n"); }else{ (1) } } sub isAProbability { if( $value < 0 || $value > 1 ){ (0, "$value isn't a probability (which must be in 0..1).\n"); }else{ (1) } } # Note that &isAProbability could have been defined as # sub isAProbability { # &inRange(0,1) # } # but the result would have been a less clear error message. # Note that like validator expressions, validator subs don't take an arg # that contains $value; they inherit it thru dynamic localization. The only # args a validator sub takes are those it needs to parameterize the condition # it will enforce, like $min & $max for &inRange. # OK, now we're ready to look at the &askValue engine. Reading this code # is more difficult than reading the code above because this code really # does something, so if you just want to use this package rather than hack # it, you could stop reading now. How to call &askValue is adequately # documented at the top of this file, & the other &askFoo subs are examples # of how to code your own specialized &askFoo subs. # To read &askValue, we need to discuss some gory details. # This is a self-testing package, which means that it contains the code # for its own regression testing. When ask.pl is used in anger, it's # required into the script using it; but it can also be invoked from the # command line with a single arg of '--selftest', in which case it runs its # test. The mechanism for this is documented in selftest.pl, q.v. You'll # need to have read that first to understand everything after this point. # All selftest.pl does is implement the mechanism for calling the self-test # code when '--selftest' is given; each self-tester is responsible for # implementing its own test strategy. Interactive code is thorny because the # the interaction needs to be redirected during the test. That's the main # reason $IN & $OUT were put into the arg lists, altho they may have other # uses. However, they turn out not to be enough, because coding all those # args explicitly in the test suite is a pain in the ass. So what I first # tried to do was wrap the test code in stuff like # open( SAVEIN, '<&STDIN' ) || die "Can't open SAVEIN: $!"; # open( STDIN, '<&TESTDATA' ) || die "Can't redirect STDIN: $!"; # open( SAVEOUT, '>&STDOUT' ) || die "Can't open SAVEOUT: $!"; # open( STDOUT, '>Nul' ) || die "Can't redirect STDOUT: $!"; # # [...run the tests...] # open( STDIN, '<&SAVEIN' ) || die "Can't restore STDIN: $!"; # open( STDOUT, '>&SAVEOUT' ) || die "Can't restore STDOUT: $!"; # but altho this worked for STDOUT, it was a NOP for STDIN, using # Darryl Okahata's Perl 4.036 running under Windows 3.1 over DOS 6.20. This # $kluge = fileno(TESTDATA); # open( STDIN, "<&=$kluge" ) || die "Can't redirect STDIN: $!"; # said "<&=$kluge" was an invalid argument, which is better than a NOP but # is still wrong. To get around this, I created $ask'default{IN,OUT}, & # &askValue uses those instead of 'STDIN' & 'STDOUT' if $IN & $OUT are undef. # These package-globals can be ignored by users, in which case &askValue will # just behave as if the defaults are always 'STDIN' & 'STDOUT'. # Another self-testing problem is that if &askValue receives a user response # that fails to either match $pattern ($_[1]) or satisfy $validator ($_[3]), # it explains the problem & loops back to prompt again. If this failure # results not from a real-life user's error but from a code failure during # self-testing, that loop will keep slurping values from until one # matches or eof(TESTDATA), & this bollixes up all subsequent test cases. To # prevent that, $ask'iterationLimit can define a maximum # of attempts, # after which &askValue will warn & return undef. If !$ask'iterationLimit # there is no limit (appropriate for interactive use). # When other packages self-test, they may want all interactive output # suppressed, which they can achieve by redirecting $ask'defaultOUT to # /dev/null, or they may want it displayed so the progress of the test can be # observed. In that case, however, they'll want the input echoed to the # screen too, & in such cases the input has typically been redirected from a # file. Therefore, &askValue needs to be able to be told that whatever input # has been received needs to be echoed; $ask'echoInput is a boolean for this. # It has no effect when single-char input is being used, but single-char # input is only used when input is from a terminal, so it wouldn't apply # to self-testing anyway. # Here are all package-globals, placed as selftest.pl documents is needed: require 'selftest.pl'; $ask'defaultIN = 'STDIN'; $ask'defaultOUT = 'STDOUT'; $ask'iterationLimit = 0; $ask'echoInput = 0; &maybeSelfTest; # Another Perl failure complicated things, but this one occurs not only in # Okahata's 4.036 but in kosher 5.002 under SunOS 4.1.4: getc() doesn't work # correctly when used from a terminal-like device, blocking until the user # hits , then returning just the first char, & leaving the rest for # the next call on that filehandle. This obviously misses the point. (It # seems to me read(,,1) could do that, leaving getc() for stuff like this.) # I worked around this with the appalling construct # $value = `getche` # in which getche is required to be some external program that does the # console I/O as desired. Under DOS, I have a .Com file for that; under # Unix, a shell script can do it using stty & dd. The name "getche" stands # for "get char with echo" & is standard in DOS-heritage C libraries. # See after __END__ for getche implementation(s). # That concludes the horror stories; now we're ready for &askValue's code. # First it extracts its args & allows all but the first 2 to be defaulted. # Then it defines $default, the default for the user's response, if one was # contained in brackets in $prompt. Then $singleChar becomes whether we want # to use the `getche` abomination or just <$IN>. Then it's ready to loop. # Each iteration displays the prompt, gets a response, validates it first # against $pattern then if necessary against $validator, aborting if # $ask'iterationLimit is exceeded, redoing if $value is invalid, or # returning $value if it's acceptable. All that fits on a single 50-line # screen of code, but just barely, which is why the comments are up here # instead of down there. # Here again are &askValue's args, this time with their local names: # $prompt: the prompt to be displayed to the user # $pattern: a regexp that the user's response must match # $description: a human-language description of what that regexp must match # $validator: an optional expression to further constrain responses # $IN: an optional input file handle replacing STDIN # $OUT: an optional output file handle replacing STDOUT sub askValue { local( $prompt, $pattern, $description, $validator, $IN, $OUT ) = @_; local( $default, $singleChar, $iterations ); # $validator requires $value to be dynamically scoped (i.e. local, not my) local( $value ); # Perl 4 requires () for even a single value $description = "/$pattern/" unless defined $description; $IN = $ask'defaultIN unless defined $IN; $OUT = $ask'defaultOUT unless defined $OUT; $default = $prompt =~ /\[($pattern)\]/ ? $1 : ''; $singleChar = -t $IN && $pattern =~ /^\[[^\[\]]*\]$/; $iterations = 0; { #loop if( $ask'iterationLimit && ++$iterations > $ask'iterationLimit ){ warn 'ITERATION LIMIT EXCEEDED'; ; return undef; } print $OUT $prompt; if( $singleChar ){ $value = `getche`; # $value = getc($IN) fails }else{ chop( $value = <$IN> ); print $value,$/ if $ask'echoInput; } $value = $default if $value eq ''; unless( $value =~ /^($pattern)$/ ){ print $OUT "\"$value\" isn't $description.\n"; ; redo; } if( defined $validator ){ ($valid,$whyNot) = eval $validator; $@ && die __FILE__ . ": &askValue: eval \"$validator\" failed:\n$@"; unless( $valid ){ if( $whyNot ){ print $OUT "$whyNot\n"; }else{ $_ = $validator; s#"#\\"#; $_ = eval qq/"$_"/; #interpolate $value print $OUT "Condition \"$_\" is false.\n"; } ; redo; } } } #end loop $value; } # Because ask.pl is self-testing, it must be command-line executable as # well as requireable, & this causes problems with -w under Perl4 when subs # are declared here but referenced only in the self-test code, which comes # after __END__. This declaration avoids the "possible typo" warning: &askNumber, &askInteger, &askYN, &inRange, &isAProbability if 0; 1; __END__ ask.pl doesn't use , & scripts that require ask.pl get separate DATA filehandles, so this is a free area for storing random stuff. Note that there's a __TESTCODE__ delimiter farther down (see selftest.pl). ================================CHANGE HISTORY=============================== Fr 10 Jan 97 Implemented $ask'echoInput for use by ttyForm.pl. Tu 21 Jan 97 Reformatted eval error message in &askValue. Su 26 Jan 97 Tweaked validator documentation. Mo 11 Aug 97 Replaced 1e30 with 1e18 in test cases because 5.004_01 ported to Windows by Gurusamy Sarathy gives "illegal instruction" for 1e30. Tweaked some documentation. Put explicit "package main" at top. ==============================END CHANGE HISTORY============================= Here is a file that can be piped into the DOS Debug program to produce the DOS program getche.com: ----------------------------------getche.dbg--------------------------------- E 00 32 E4 CD 16 84 C0 74 21 B4 0E 32 FF CD 10 B4 02 E 10 8A D0 CD 21 B4 0E 32 FF B0 0D CD 10 B4 0E 32 FF E 20 B0 0A CD 10 B8 00 4C CD 21 32 E4 CD 16 B8 01 4C E 30 CD 21 rcx 32 Ngetche.Com W 0 Q ----------------------------------------------------------------------------- If you're prudent & an assembly programmer you can use Debug's U command to eyeball the resulting code before executing it. A truly self-testing program runs a completely automated regression test, & that's what ask.pl normally does when invoked with --selftest. However, sometimes there's a reason to want to experiment interactively with the code. Here are some test cases that can be run by moving them below __TESTCODE__. print "Got ",&askYN( "Yes or no (y/n) [y]? " ),$/; print "Got ",&askYN( "Yes or no (y/n) [n]? " ),$/; print "Got ",&askYN( "Yes or no (y/n) [y]? " ),$/; print "Got ",&askYN( "Yes or no (y/n) [n]? " ),$/; print "Got ",&askYN( "Yes or no (y/n)? " ),$/; print "Got ",&askYN( "Yes or no (y/n)? " ),$/; exit 12 if &askYN( "Stop now (y/n) [y]? " ); for $default ( 1..3 ){ print "Got ", &askNumber( "Enter a # in 1..2 [$default]: ", '&inRange(1,2)' ),$/; } exit 12 if &askYN( "Stop now (y/n) [y]? " ); print "Got ",&askNumber( "Enter a value in 5..7: ",'$value >= 5 && $value <= 7' ),$/; print "Got ",&askNumber( "Enter a probability: ",'&isAProbability' ),$/; for( 1..3 ){ print "Got ",&askValue( "Enter a probability: ", '-?(\d+(\.\d*)?|\.\d+)(e-?\d+)?', "a number", '&isAProbability', ),$/; } print "Executed ask.pl interactive self-test.\n"; Everything between __TESTCODE__ & __TESTDATA__ gets evalled if --selftest, so comments must be prefixed with "#". __TESTCODE__ # Note that interactive test code is available above before "__TESTCODE__". # Insert any that's wanted above this comment. # SET UP AUTOMATED REGRESSION TESTING # The concept is that the test cases themselves all go in __TESTDATA__; # this is just code to extract & eval them, & check the results. # We'll define a &try here that takes 2 args, an expression to eval & the # expected result; &try evals the expression & copes with failures. Both # those args will occur on a line of __TESTDATA__, ready to be interpolated # into a &try call. Since the &askFoo subs to be tested are interactive, # they need to get input from someplace, so all input will go in __TESTDATA__ # right after the &try arglist that needs it. Here are 2 sample lines: # '&askYN( "Y/N? " )', 1 # y # The first line gives &try an expression to eval & the result to check for; # the second line is the input to &askYN that should produce the result of 1. # Note that the canonical Perl TRUE is 1 but FALSE is ''. $verboseTesting = 1; $tryCount = $errorCount = 0; $maxErrorCount = 12; sub try { ($expression,$want) = @_; ++$tryCount; warn "Trying: $expression\n" if $verboseTesting; $got = eval $expression; die $@ if $@; ; return if $got eq $want; ++$errorCount; warn "$expression: '$got' instead of '$want'\n"; die "$maxErrorCount errors exceeded\n" if $errorCount > $maxErrorCount; } # RUN THE TESTS # This requires input to be redirected from STDIN to TESTDATA. Output is # also a problem. It'd've been possible to define code that saved the output # from each test case, read what it was supposed to be, & verified it; but # that just didn't seem worth it, so here &askFoo output just goes to # /dev/null. Anything the human tester is intended to see is therefore # output with warn, so it'll go to STDERR. (The strategy of always using # a $prompt of '' wouldn't allow testing defaults (bracketed in $prompt).) # For Darryl Okahata's Perl 4.036 running under Windows 3.1 + DOS 6.20, # redirecting STDIN with open() seems to be a NOP altho STDOUT works. # That's why the $ask'defaultFOO variables were created. $ask'defaultIN = 'TESTDATA'; $ask'defaultOUT = 'DEV_NULL'; $DEV_NULL = $ENV{'PATH'} =~ ';' ? '>Nul' : '>/dev/null'; #DOS or Unix open( DEV_NULL ) || die "Can't open DEV_NULL"; # For reasons documented in the substantive script, $ask'iterationLimit # will need to be nonzero during testing. Actually, to prevent &try arglists # from being slurped by runaway failures, it always needs to equal exactly # the number of input lines provided for each &try, & this will change # during testing. To allow this, __TESTDATA__ may contain lines prefixed # with 'EVAL: '; their remainders will be evalled at that point in the run. # We might as well also allow comments & blank lines in __TESTDATA__. while( ){ ; next if /^[ \t]*(#|$)/; #comments or blank lines if( /^EVAL: (.*)$/ ){ #modifications of the test environment eval $1; die $@ if $@; ; next; } eval '&try( '. $_ . ')'; die $@ if $@; #Note that &try also reads from TESTDATA when it does eval $expression, #because of the way $ask'defaultFOO were set above. } # The $ask'defaultFOO variables will probably not be referenced again, # but too weird things could happen if they were but weren't now reset. $ask'defaultIN = 'STDIN'; $ask'defaultOUT = 'STDOUT'; # REPORT RESULTS AND EXIT # During an early run I found that I didn't notice a failure because the # output formats for failures & successes were too similar. # The failure format needs to be an eyecatcher. if( $errorCount ){ print "ATTEMPTS: $tryCount\n"; print "FAILURES: $errorCount <----------------- LOOK\n"; exit 1; }else{ print "Tried $tryCount expression", $tryCount == 1 ? '' : 's',"; OK\n"; exit 0; } __TESTDATA__ # ------------------------------------------- # Note that blank & comment lines are OK here EXCEPT immediately # after a test expression; at least one line after a test expression # is the input to that expression's &ask*(). # Don't forget to keep track of $ask'iterationLimit. # ----------------Test &askYN---------------- EVAL: $ask'iterationLimit = 1; '&askYN( "Y/N [y]? " )', 1 '&askYN( "Y/N [n]? " )', '' '&askYN( "Y/N? " )', 1 y '&askYN( "Y/N? " )', '' n EVAL: $ask'iterationLimit = 3; '&askYN( "Y/N? " )', 1 ? y # -------Test &askNumber without defaults------- # Under 4.036, I had a bunch of test values of 1e30, & they worked fine # on DOS 6.20 & Windows 3.1. Under 5.004, "print 1E30" generates 3 # separate illegal operations & halts the program. (Anything over 1e19 # fails this way.) I've switched them below to 1e18. EVAL: $ask'iterationLimit = 1; '&askNumber( "Number? " )', 0 0 '&askNumber( "Number? " )', 1 1 '&askNumber( "Number? " )', 2 2 '&askNumber( "Number? " )', 2000000 2000000 '&askNumber( "Number? " )', .1 .1 '&askNumber( "Number? " )', .1 0.1 '&askNumber( "Number? " )', .1 000000000000000.1 '&askNumber( "Number? " )', .1 0.10000000000000 '&askNumber( "Number? " )', -1 -1 '&askNumber( "Number? " )', -.1 -.1 '&askNumber( "Number? " )', -.1 -0.1 '&askNumber( "Number? " )', -.1 -000000000000000.1 '&askNumber( "Number? " )', -.1 -0.10000000000000 '&askNumber( "Number? " )', 1E18 1E18 '&askNumber( "Number? " )', 1e18 1e18 '&askNumber( "Number? " )', -1e18 -1e18 EVAL: $ask'iterationLimit = 8; '&askNumber( "Number (testing pattern mismatches)? " )', 666 « #That 8-bit char in the IBM PC char set is the glyph for one half. #In an earlier life I'd've hacked in code to make that work. uh, like, 668 668: the Neighbor of the Beast 668 more or less 666 # ------------------------------------------- # That should take care of various types of #s. The next question is # whether defaults work, & I don't think it's necessary to retest every # one of the above cases, just a few. It turned out to be easiest to # generate the tests automatically from the previous batch, so there # are still more than necessary. This implies that when enhancing, you # shouldn't consider it necessary to have this quantity of tests; just # test what's likely to fail. # -------Test &askNumber with defaults------- '&askNumber( "Number [0]? " )', 0 '&askNumber( "Number [0]? " )', 0 0 '&askNumber( "Number [1]? " )', 1 '&askNumber( "Number [1]? " )', 1 1 '&askNumber( "Number [2]? " )', 2 '&askNumber( "Number [2]? " )', 2 2 '&askNumber( "Number [2000000]? " )', 2000000 '&askNumber( "Number [2000000]? " )', 2000000 2000000 '&askNumber( "Number [.1]? " )', .1 '&askNumber( "Number [.1]? " )', .1 .1 '&askNumber( "Number [0.1]? " )', .1 '&askNumber( "Number [0.1]? " )', .1 0.1 '&askNumber( "Number [-1]? " )', -1 '&askNumber( "Number [-1]? " )', -1 -1 '&askNumber( "Number [-.1]? " )', -.1 '&askNumber( "Number [-.1]? " )', -.1 -.1 '&askNumber( "Number [-0.1]? " )', -.1 '&askNumber( "Number [-0.1]? " )', -.1 -0.1 '&askNumber( "Number [1E18]? " )', 1E18 '&askNumber( "Number [1E18]? " )', 1E18 1E18 '&askNumber( "Number [1e18]? " )', 1e18 '&askNumber( "Number [1e18]? " )', 1e18 1e18 '&askNumber( "Number [-1e18]? " )', -1e18 EVAL: $ask'iterationLimit = 4; '&askNumber( "Number (testing pattern mismatches) [666]? " )', 666 uh, like, 668 668: the Neighbor of the Beast 668 more or less EVAL: $ask'iterationLimit = 1; '&askNumber( "Number [-1e18]? " )', -1e18 -1e18 # ------------------------------------------- # Next question is whether the $validator arg works. # -------Test &askNumber with $validator------- EVAL: $ask'iterationLimit = 3; q/&askNumber( "Number [0]? ", '$value > 2' )/, 3 1 2 3 q/&askNumber( "Number? ", '$value > 2' )/, 3 3 q/&askNumber( "Number? ", '&inRange(-5,5)' )/, 0 -88888888888888888 888888e88 0 q/&askNumber( "Probability (0..1)? ", '&isAProbability' )/, .5 20e0 -20e-0 .5 # ------------------------------------------- # &askInteger just does a subset of what &askNumber does, so a few tests # will suffice. # -------Test &askInteger assuming &askNumber works------- EVAL: $ask'iterationLimit = 1; '&askInteger( "Integer [0]? " )', 0 '&askInteger( "Integer [0]? " )', 0 0 '&askInteger( "Integer [1]? " )', 1 '&askInteger( "Integer [1]? " )', 1 1 '&askInteger( "Integer [2]? " )', 2 '&askInteger( "Integer [2]? " )', 2 2 '&askInteger( "Integer [2000000]? " )', 2000000 2000000 '&askInteger( "Integer [1E18]? " )', 1E18 1E18 '&askInteger( "Integer [-1e18]? " )', -1e18 -1e18 EVAL: $ask'iterationLimit = 3; q/&askInteger( "Integer? ", '&inRange(-5,5)' )/, 0 -88888888888888888 888888e88 0