Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things

consolidating menu option processing

by temporal (Pilgrim)
on Jul 19, 2013 at 02:14 UTC ( #1045238=perlquestion: print w/replies, xml ) Need Help??

temporal has asked for the wisdom of the Perl Monks concerning the following question:

Hello fine monks! I have a question about coding menu options. I can never tell if I'm just refactoring my code into oblivion, so here goes.

I have a program which accepts user input - allowing for single character options to be specified instead of entering the requested string:

# simple menu/input collection enter word: foo you entered <foo> enter word: q quitting...

My code has many of these prompts for different types of menus with the same sort of idea. I found that I was constructing lots of if-elsif chains for the same options, which were long, unsightly, cluttering. And repetitive - for example 'q' exits from all prompts.

sub check_input { my $input = shift; return $input if length($input) != 1; if ($input eq 'q') { exit } elsif ($input eq 'b') { ... } else { print 'not recognized'; } }

I should have realized it beforehand - but what shall I do about prompt-unique options?

sub check_input { my ($type, $input) = @_; return $input if length($input) != 1; exit if $input eq 'q'; # etc for universal options # handle prompt types if ($type eq 'start') { if ($input eq 'n') { # do something useful with currently out-of-scope variable +s ... # (uh oh) } } # etc, etc }

Now I'm starting to feel like I've created a monster when the drive behind this refactoring was to clean up my code. Well, one of the primary uses of these single-character options is to break from a loop. So I guess I can use labels to force that sort of functionality...

while(1) { my $input = <>; chomp $input; check_input('ptype', $input); } BREAK: ... # sub check_input if ($input eq 'b') { goto BREAK: }

This feels dirty. I was raised to be extremely wary of goto statements from a very impressionable, young age. The term Spaghetti Code springs to mind.

But what about manipulating those out-of-scope variables in the check_input sub? Here's what I came up with:

my @working = (1 .. 10); # globally scoped anonymous coderef my $push = sub { push @working, shift }; my $pop = sub { pop @working }; while(1) { my $input = input(); check_input('type', $input); } ... # sub check_input if ($input eq 'p') { $pop->(); }

Probably been writing too much Ruby lately - I couldn't get plain bracketed code blocks ( $b = {print 'hi!'}; $b() ) to stop giving me syntax errors. Maybe that's a feature only available in Perl 6? But this isn't really ideal either, I'd rather not have all these globally scoped anonymous coderefs floating around. And what if my prompt isn't scoped above the check_input sub? Then I would have to pass these coderefs to the check_input routine, perhaps in a hash keyed to their corresponding characters. And maybe that's the way to go.

But maybe there is a better way? Any monkly advice welcome, even if it's just to say that just spinning my wheels on this =P

I've settled on something like this, for now:

sub check_input { my ($input, $opts_href) = @_; return $input if length($input) != 1; exit if $input =~ m/Q|c/; $opts_href->{$input}->() if defined $opts_href->{$input} && ref $opts_href->{$input} eq + 'CODE'; } ... # define non-global options hash in menu scope my %opts_hash = ( a => sub { goto SOMESPOT }, o => sub { print "continue? (y/N): "; $input = input(); pop @working if $input =~ m/^y$/i; + }, 'default' => sub { say "error: please select 'a', 'o', or 'c'" }, ); ... # cannot find this label! sub elsewhere { ... SOMESPOT: ... }

I've discovered that I cannot goto a label that is not the same or higher scope. Hm, I've always assumed that it would hop anywhere.

Replies are listed 'Best First'.
Re: consolidating menu option processing
by Anonymous Monk on Jul 19, 2013 at 04:04 UTC
    Class::StateMachine, Term::Interact example, dispatch table, IO::Prompter, Term::Interact, Term::Menu ...

    poorly thought out example

    use strict; use warnings; use Data::Dump qw/ dd pp /; my $stash = menu_one( "say hi or say bye:\n", [ qr/hi/i => sub { print "hi\n"; $_[1]->{hi}++; 1 }, ], [ qr/quit|bye|adios/i => sub { print "bye bye\n"; 0 }, ], ); dd( $stash ); dd( menu_one( "say why :", [ qr/.*/ => sub { die "why?!?!?\n"; 0 }, ], ), ); sub menu_one { my( $header, @actions ) = @_; my $ret = 1; my $stash = {}; while( $ret ){ my $line = getone($header); ACTION_LOOP: for my $action ( @actions ){ my( $match, $callback ) = @$action; if( $line =~ $match ){ $ret = $callback->( $line, $stash ); last ACTION_LOOP; } } } return $stash; } sub getone { print @_; scalar <>; }

      Well that's certainly a nifty approach, encapsulating the entire menu's functionality rather than just the unique options. Thanks for the example!

      I should've guessed there would already be something on CPAN, ha. Most of those modules seem to be good for validating input, but not so much for consolidating all of the options execution (which is in addition retrieving basic string input) at a single point of code.

      Since we both ended up implementing variations on them - it seems like dispatch tables are the way to go! =)

      Strange things are afoot at the Circle-K.

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://1045238]
Approved by kevbot
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others drinking their drinks and smoking their pipes about the Monastery: (3)
As of 2019-12-09 01:59 GMT
Find Nodes?
    Voting Booth?

    No recent polls found