Ask not what your user can do for you...

In many scripts, we need to prompt the end user for information - this could be a prompt for a file name, a selection from a list of options, or an answer to a yes/no question.

The traditional approach to this sort of question is to print your question to STDOUT, read a line from STDIN, and apply some sort of parsing to the answer...

   use 5.010;
   use strict;
   use warnings;
   
   my $answer;
   until (defined $answer) {
      print "Would you like fries with that?\n";
      $_ = <>;
      $answer = 1 if /^Y/i;
      $answer = 0 if /^N/i;
   }
   
   say "Adding fries!" if $answer;

One issue with this approach is: what happens when your script is not running in a terminal?

One attempt at solving this problem is IO::Prompt::Tiny and its ilk. This performs a simple test to determine if the script is running on an interactive terminal and only prompts the user if the terminal is interactive. When the script is being run non-interactively (or if the PERL_MM_USE_DEFAULT environment variable is set), then it returns a default answer instead.

   use 5.010;
   use strict;
   use warnings;
   use IO::Prompt::Tiny qw(prompt);
   
   my $answer;
   until (defined $answer) {
      # In non-interactive mode, assume they want no fries...
      $_ = prompt("Would you like fries with that?", "No");
      $answer = 1 if /^Y/i;
      $answer = 0 if /^N/i;
   }
   
   say "Adding fries!" if $answer;

The problem with this is that it makes the assumption that when the terminal is non-interactive, there is absolutely no other way to prompt the user, and you should be happy with the default answer. This is not always a good assumption.

Opening up a dialogue

On some operating systems, double-clicking a Perl file will launch it without a terminal. In these cases, you can probably interact with the user by launching a dialog box. But how to do that? Doesn't that require complex programming in Tk or Wx (modules which are not in core, and not always straightforward to build)?

Enter Ask. Ask abstracts away the details of interacting with your user. It will do the terminal interaction test; it will check PERL_MM_USE_DEFAULT; it will see if the Wx, Gtk2 or Tk modules are installed and usable; it will even use /usr/bin/zenity (a GNOME component for adding GUI dialog boxes to shell scripts) if it has to.

It will only resort to using the default answer if there's no other possibility of interacting with the user. Here's our fast food worker using Ask:

   use 5.010;
   use strict;
   use warnings;
   use Ask qw(question);
   
   my $answer = question("Would you like fries with that?", default => 0);
   say "Adding fries!" if $answer;

That is the question

In the previous example, we saw a yes-no question. How about something a bit harder?

   use Ask qw( multiple_choice );
   
   my @answers = multiple_choice(
      "Please choose some pizza toppings...",
      choices => [
         [ sauce        => 'Our famous pizza sauce' ],
         [ cheese       => 'Oozing Mozzarella cheese' ],
         [ ham          => 'Finest Bavarian ham' ],
         [ pepperoni    => 'Spicy pepperoni' ],
         [ onion        => 'Onion slices' ],
         [ tinned_fruit => 'Chunky cuts of fresh pineapple' ],
      ],
   );
   say "Adding $_" for @answers;

Or if you just wish them to choose a single option from a list:

   use Ask qw( single_choice );
   
   my $existance = single_choice(
      "To be, or not to be; that is the question.",
      choices => [
         [ be     => "Be" ],
         [ not_be => "Don't be" ],
      ],
   );

Ask also has functions for file selection, text entry (including hidden text - passwords) and displaying information, warnings and errors.

I object!

If you object to using the functional interface, you can get an object using the Ask->detect method and call question, single_choice and friends as object methods.

   use 5.010;
   use strict;
   use warnings;
   use Ask;
   
   my $interface = Ask->detect;
   
   my $answer = $interface->question(
      text      => "Would you like fries with that?",
      default   => 0,
   );
   say "Adding fries!" if $answer;

The functional interface is just a friendly wrapper around Ask's object-oriented core.

Boldly go

Let's say that you want to hook up your script to your drive-through restaurant's voice recognition system. Ask's backends are all Moo classes performing the Ask::API role. It's really easy to write your own:

   package Ask::VoiceRecognition {
      
      use MyApp::Voice::Generator ();
      use MyApp::Voice::Recognition ();
      
      use Moo;
      with 'Ask::API';
      
      has generator => (
         is      => 'lazy',
         default => sub { MyApp::Voice::Generator->new },
      );
      
      has recognition => (
         is      => 'lazy',
         default => sub { MyApp::Voice::Recognition->new },
      );
      
      sub info {
         my $self = shift;
         my %args = @_;
         $self->generator->say($args{text});
      }
      
      sub entry {
         my $self = shift;
         my %args = @_;
         $self->info($args{text}) if exists $args{text};
         return $self->recognition->listen(seconds => 30);
      }
   }

That's all there is to it!

The Ask::API provides default implementations of question, file_selection, multiple_choice, etc, which you can override if you choose to, but that is optional.

To force Ask to use your backend rather than the built-in ones, just set the PERL_ASK_BACKEND environment variable to the name of your module.

Ask the future

Ask is a young module and still needs some work. In particular:

  • Detection of the best module for interacting with the user is naive. It can end up selecting, say, Gtk2 on a headless Linux box.
  • A native Windows GUI backend is planned. The Gtk2, Wx and Tk backends should all theoretically work on Windows, but rely on various library files being present.

Ask is on GitHub and on Bitbucket so feel free to contribute improvements!

2 Comments

Great idea!

If I may add a suggestion: While perhaps its not as well known, Prima is a cross-platform GUI toolkit written especially for Perl, and it needs no other libraries (except x headers on Mac/Linux)! Perhaps you would think about adding support?

Leave a comment

About Toby Inkster

user-pic I'm tobyink on CPAN, IRC and PerlMonks.