Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I build a simple menu in Perl?

I'm working on a Perl script that requires some basic menu functionality. Ultimately I would like each menu to have a few options and then the option to either return to the previous menu or exit.

example:

This is a menu:

  1. Choice 1
  2. Choice 2
  3. Return to previous menu
  4. Exit

Select an option:

I currently have a menu subroutine making the menus, but there is no functionality allowing it to go back to the previous menu.

    sub menu
    {
        for (;;) {
            print "--------------------\n";
            print "$_[0]\n";
            print "--------------------\n";
            for (my $i = 0; $i < scalar(@{ $_[1]}); $i++) {
                print $i + 1, "\.\t ${ $_[1] }[$i]\n";
            }
            print "\n?: ";
            my $i = <STDIN>; chomp $i;
            if ($i && $i =~ m/[0-9]+/ && $i <= scalar(@{ $_[1]})) {
                return ${ $_[1] }[$i - 1];
            } else {
                print "\nInvalid input.\n\n";
            }
        }
    }

    # Using the menu
    my $choice1  = menu('Menu1 header', \@list_of_choices1);

    # I would like this menu to give the option to go back to
    # the first menu to change $choice1
    my $choice2 = menu('Menu2 header', \@list_of_choices2);

I don't want to hard code all of the menus and use if/elsif statements for all of the processing so I turned the menu into a function.

My menus currently look like this...

Menu Header:

  1. Choice1
  2. Choice2
  3. Choice3

?: (Enter input here)

This solution still doesn't allow the user to go back to the previous menu or exit though. I was considering making a menu class to handle the menus, but I am still not very good with object oriented Perl. This is a small program with only a few menus so using a complex menu building module may be overkill. I would like to keep my code as light as possible.

EDIT:

Thanks for the quick responses! However there is still an issue. When I select an option from "Menu1" and it progresses to "Menu2" I would like the save the choice from "Menu1" for later use:

Menu1:

  1. Choice1 <-- store value if selected and go to next menu
  2. Choice2 <-- ...
  3. Exit <-- quit

Menu2:

  1. Choice1 <-- store value if selected and go to next menu
  2. Choice2 <-- ...
  3. Back <-- go back to previous menu to reselect value
  4. Exit <-- quit

Selecting either Choice1 or Choice2 should store a value in a variable for later use and progress to the next menu. Then if you choose to go back to the first menu from Menu2, it will give you the option to reselect your choice and redefine the variable. I'm trying to avoid using global variables which makes this quite difficult.

After progressing through all of the menus and setting the values of all of these variables, I want to run a subroutine to process all of the choices and print a final output.

 sub main () {

   # DO MENU STUFF HERE

   # PROCESS RESULTS FROM MENU CHOICES
   my $output = process($menu1_choice, $menu2_choice, $menu3_choice, ... );
 }

Also if anyone has an object oriented approach to this using classes or some other data structure, although it may be overkill, I would still love to see it and try to wrap my head around the idea!

like image 409
tjwrona1992 Avatar asked Dec 20 '22 12:12

tjwrona1992


2 Answers

You could use a module such as Term::Choose:

use Term::Choose qw( choose );

my $submenus = {
    menu1 => [ qw( s_1 s_2 s_3 ) ],
    menu2 => [ qw( s_4 s_5 s_6 s_7) ],
    menu3 => [ qw( s_8 s_9 ) ],
};
my @menus = ( qw( menu1 menu2 menu3 ) );
my $mm = 0;
MAIN: while ( 1 ) {
    my $i = choose( 
        [ undef, @menus ],
        { layout => 3, undef => 'quit', index => 1, default => $mm }
    );
    last if ! $i;
    if ( $mm == $i ) {
        $mm = 0;
        next MAIN;
    }
    else {
        $mm = $i;
    }
    $i--;
    SUB: while ( 1 ) {
        my $choice = choose(
            [ undef, @{$submenus->{$menus[$i]}} ],
            { layout => 3, undef => 'back' }
        );
        last SUB if ! defined $choice;
        say "choice: $choice";
    }
}
like image 118
sid_com Avatar answered Dec 31 '22 03:12

sid_com


If you don't want to go full OO with this, a simple way that you can make this a lot more flexible is to allow each menu choice to control how it is executed. Let's say each menu has an array of hashes that contain the menu text and a coderef that implements what the menu does.

use strict;
use warnings;

sub menu {
    my @items = @_;

    my $count = 0;
    foreach my $item( @items ) {
        printf "%d: %s\n", ++$count, $item->{text};
    }

    print "\n?: ";

    while( my $line = <STDIN> ) {
        chomp $line;
        if ( $line =~ m/\d+/ && $line <= @items ) {
            return $items[ $line - 1 ]{code}->();
        }

        print "\nInvalid input\n\n?: ";
    }
}

my @menu_choices;
my @other_menu_choices;

@menu_choices = (
    { text  => 'do something',
      code  => sub { print "I did something!\n" } },
    { text  => 'do something else',
      code  => sub { print "foobar!\n" } },
    { text  => 'go to other menu',
      code  => sub { menu( @other_menu_choices ) } }
);

@other_menu_choices = (
    { text  => 'go back',
      code  => sub { menu( @menu_choices ) } }
);

menu( @menu_choices );

The menu subroutine takes an array of options, and each option "knows" how to perform its own action. If you want to switch between menus, the menu option just calls menu again with a different list of options, as in the "go back" example from @other_menu_choices. This make linking between menus very easy and it's also easy to add exit options and such.

To keep this code clean and readable, for anything other than trivial menu actions, use a named reference to a subroutine instead of an anonymous subroutine reference. For example:

@another_menu_options = (
    { text => 'complicated action'
      code => \&do_complicated_action
    }
);

sub do_complicated_action { 
    ...
}
like image 35
friedo Avatar answered Dec 31 '22 03:12

friedo