# Disambiguate.pm # # Package used to validate a response against a list of choices # such that an unambiguous but incomplete string will be completed # but ambiguous or incorrect strings will fail. E.g. the choices # of "clear" and "close" would mean that a response of "c" or "cl" # is ambiguous but "cle", "clea" or "clear" all unambiguously mean # "clear". # # ============ package Disambiguate; # ============ # Turn on strictures and warnings. # use strict; use warnings; # Use Carp and Exporter. There is just the one subroutine to export. # use Carp; use Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(disambiguate); # Code reference scoped to the package that generates a regular # expression that will match any number of characters of a # choice from the minimum unambiguous string upwards. # # -------------- my $rcRegexForThis = sub # -------------- { # Get list reference of choice fragments, e.g. for "clear" # above they would be "cle", "a" and "r". Initialise regular # expression string. # my $rlFragments = shift; my $regexStr = q(); # While there is more than one fragment left, "pop" off the # rightmost and place it and any previously popped-off and # processed fragments in a non-capturing group with a quantifier # of "?" (0 or 1 of). Thus first time through we get "(?:r)?" and # the next we get "(?:a(?:r)?)?". # while (scalar @$rlFragments > 1) { my $fragment = pop @$rlFragments; $regexStr = '(?:' . $fragment . $regexStr . ')?' } # Prepend the last (leftmost) fragment, which is the minimum # unambiguous one, to the regular expression, giving in our # example "cle(?:a(?:r)?)?". Return this string. # $regexStr = $rlFragments->[0] . $regexStr; return $regexStr; }; # Exported subroutine that takes as arguments: a reference to the # scalar in the calling script that will receive the disambiguated # choice if the match is successful; a list of choices used to # generate and compile a regular expression against which a choice # will be validated. The subroutine returns the compiled regular # expression to do the validation match. # # ------------ sub disambiguate # ------------ { # Get scalar reference and list of choices. Validate. # my($rsChoice, @choices) = @_; croak "disambiguate(): arg. 1 not a SCALAR reference" unless ref $rsChoice eq "SCALAR"; croak "disambiguate(): no choices supplied" unless scalar @choices; my %seen = (); foreach my $choice (@choices) { croak "disambiguate(): duplicate choice \"$choice\" found" if $seen{$choice} ++; } # Initialise a hash for choice fragments, to be keyed by choice, # e.g. "cle", "a" and "r" for the choice "clear" as above. And # also one for unambiguous stubs, e.g. "cle", "clea" and "clear" # where there will be elements for each, all with a value of # "clear". Initialise a list that will hold a regular expression # string for each choice. # my %fragments = (); my %unambiguities = (); my @choiceRegexen = (); # Iterate over the choices, split'ing into characters. # foreach my $choice (@choices) { my @chars = split //, $choice; # Initialise scalar to hold shortest unambiguous string for # this choice. Add a character at a time in a loop, grep'ing # the choices that match the possibly unambiguous string; if # you get more than one match, it is ambiguous. # my $unambiguous = q(); while (@chars) { $unambiguous .= shift @chars; last unless 1 < grep {m/^$unambiguous/} @choices; } # We now have an unambiguous string (which could be the # entire choice) so push it and any remaining characters # onto the "fragments" hash for this choice. Update the # "unambiguities" hash for each unambiguous string up to # the full choice. # push @{$fragments{$choice}}, $unambiguous, @chars; $unambiguities{$unambiguous} = $choice; while (@chars) { $unambiguous .= shift @chars; $unambiguities{$unambiguous} = $choice; } # Call &$rcRegexForThis passing the fragments by reference # and push the resulting regular expression string onto # the list. # push @choiceRegexen, $rcRegexForThis->($fragments{$choice}); } # We have now processed all of the choices so we can begin to # construct the compiled regular expression that will be # returned to the calling script. First build up the text. # # The first part is a code block that takes a reference to # the %unambiguities hash. Next comes anchor to the start # of the string and open a memory group. Then joining all of # the individual regular expressions for each choice with the # "|" symbol creates an alternation between the choices. After # that we close the memory group and anchor to end of string. # Finally, if the match was against an unambiguous string and # was successful, execute the code block to assign the full # name of the choice to the dereferenced scalar supplied in # the calling script by looking up what we matched ($1) in # the hash reference ($^R) from the result of the last code # block, a reference to %unambiguities. # my $regexText = '(?{\%unambiguities})'; $regexText .= '^('; $regexText .= join '|', @choiceRegexen; $regexText .= ')$'; $regexText .= '(?{$$rsChoice = $^R->{$1}})'; # Declare and, use'ing re 'eval', compile the constructed # regular expression and return it to the calling script. # my $rxDisambiguate; { use re q(eval); $rxDisambiguate = qr{$regexText}; } return $rxDisambiguate; } 1; #### # Turn on strictures and warnings. Get the module to sort # out ambiguous answers. # use strict; use warnings; use Disambiguate; # Initialise a list of choices, declare a scalar that will # receive the disambiguated and completed choice. # my @choices = qw( alias allow appear apply begin clean clear clone close compare); my $youChose; # Call disambiguate() which is exported by Disambiguate.pm to # generate a compiled regular expression to validate choices. # Print it out just to see what is generated. # my $rxDisambiguate = disambiguate(\$youChose, @choices); print "\nCompiled regular expression is\n\n$rxDisambiguate\n"; # Test choices in an infinite loop, Ctrl-D to exit. Use Ctrl-Z # on Windows? # while(1) { # Prompt user, drop out of loop if Ctrl-D entered. Chomp # answer given. # print "\nChoose from following, partial unambiguous entry OK\n", " @choices\n", "Please choose (or Ctrl-D to exit) ? "; last if eof STDIN; chomp(my $ans = ); # Does answer match regular expression returned by disambiguate() # above. If it does, completed choice was placed in $youChose by # the regular expression. # if($ans =~ $rxDisambiguate) { print "You chose: $youChose\n"; } # It didn't match. Grep from @choices those that start with $ans. # If more than one grep'ed then $ans is ambiguous; if none then # $ans was not recognised. If only one was grep'ed then something # has gone badly wrong as the match above should have succeeded. # else { my @couldBe = grep {m/^$ans/} @choices; if(scalar @couldBe > 1) { print "\"$ans\" is ambiguous, ", "could be any one of - @couldBe\a\n"; } elsif(scalar @couldBe == 0) { print "\"$ans\" not recognised\a\n"; } else { die "It should not be possible to get here\n"; } } } print " \n"; #### Compiled regular expression is (?-xism:(?{\%unambiguities})^(ali(?:a(?:s)?)?|all(?:o(?:w)?)?|appe(?:a(?:r)?)?|appl(?:y)?|b(?:e(?:g(?:i(?:n)?)?)?)?|clean|clear|clon(?:e)?|clos(?:e)?|co(?:m(?:p(?:a(?:r(?:e)?)?)?)?)?)$(?{$$rsChoice = $^R->{$1}})) Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? a "a" is ambiguous, could be any one of - alias allow appear apply Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? al "al" is ambiguous, could be any one of - alias allow Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? ali You chose: alias Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? b You chose: begin Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? d "d" not recognised Choose from following, partial unambiguous entry OK alias allow appear apply begin clean clear clone close compare Please choose (or Ctrl-D to exit) ? ^D