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 th
I have found this old module without any perldoc in my perl modules... Please, give it a try...
#!/usr/bin/perl
BEGIN { $Curses::OldCurses = 1; }
use Curses;
use perlmenu;
&menu_init(0,"Select an Animal"); # Init menu
&menu_item("Collie","dog"); # Add item
&menu_item("Shetland","pony"); # Add item
&menu_item("Persian","cat"); # Add last item
$sel = &menu_display("Which animal?"); # Get user selection
if ($sel eq "dog") {print "Its Lassie!\n";}
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";
}
}
Thanks everyone for the responses! All three of the responses were helpful in finally coming up with my solution. I decided to go with the Term::Choose module, (Thanks sid_com for the idea). My menu structure was different than you had originally suggested and it took quite a while of scratching my head to figure out how to make it do exactly what I wanted. Hopefully this solution will help someone else out who encounters a similar problem.
I constructed the menu as shown below:
(I have replaced my variables with more general names so it is easier to follow)
#!/usr/bin/perl
use strict;
use warnings;
use Term::Choose qw(choose);
my @CHOICES1 = ('A','B','C');
my @CHOICES2 = ('1','2','3');
my @CHOICES3 = ('BLUE','YELLOW','GREEN');
# function to use the choices
sub some_function {
print "THIS IS SOME FUNCTION!\n";
print "Choice 1 is $_[0]\n";
print "Choice 2 is $_[1]\n";
print "Choice 3 is $_[2]\n";
print "Have a nice day! :)\n";
}
sub main() {
# clear the screen
# (for some reason the build in screen clear
# for the module was not working for me)
system ('cls');
# create menu object
my $menu = new Term::Choose();
# menu 1
for (;;) {
my $choice1 = $menu->choose(
[@CHOICES1, undef],
{
prompt => 'Select a choice1:',
undef => 'Exit',
layout => 3,
}
);
last if ! $choice1;
# submenu 1
for (;;) {
my $choice2 = $menu->choose(
[@CHOICES2, undef],
{
prompt => 'Select a choice2:',
undef => 'Back',
layout => 3,
}
);
last if ! $choice2;
# submenu2
for (;;) {
my $choice3 = $menu->choose(
[@CHOICES3, undef],
{
prompt => 'Select a choice3:',
undef => 'Back',
layout => 3,
}
);
last if ! $choice3;
# function operating on all choices
some_function($choice1, $choice2, $choice3);
return;
}
}
}
}
main();
I'm still very new to object oriented Perl so this took a very long time to figure out and it might not be perfect, but it gets the job done. Let me know if you have any ideas or improvements!
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 {
...
}
After a few more months of programming with Perl I learned much more about how to deal with objects and wrote a simple object oriented menu building module based off of friedo's answer.
# Menu.pm
#!/usr/bin/perl
package Menu;
use strict;
use warnings;
# Menu constructor
sub new {
# Unpack input arguments
my $class = shift;
my (%args) = @_;
my $title = $args{title};
my $choices_ref = $args{choices};
my $noexit = $args{noexit};
# Bless the menu object
my $self = bless {
title => $title,
choices => $choices_ref,
noexit => $noexit,
}, $class;
return $self;
}
# Print the menu
sub print {
# Unpack input arguments
my $self = shift;
my $title = $self->{title };
my @choices = @{$self->{choices}};
my $noexit = $self->{noexit };
# Print menu
for (;;) {
# Clear the screen
system 'cls';
# Print menu title
print "========================================\n";
print " $title\n";
print "========================================\n";
# Print menu options
my $counter = 0;
for my $choice(@choices) {
printf "%2d. %s\n", ++$counter, $choice->{text};
}
printf "%2d. %s\n", '0', 'Exit' unless $noexit;
print "\n?: ";
# Get user input
chomp (my $input = <STDIN>);
print "\n";
# Process input
if ($input =~ m/\d+/ && $input >= 1 && $input <= $counter) {
return $choices[$input - 1]{code}->();
} elsif ($input =~ m/\d+/ && !$input && !$noexit) {
print "Exiting . . .\n";
exit 0;
} else {
print "Invalid input.\n\n";
system 'pause';
}
}
}
1;
Using this module you can build menus and link them together relatively easy. See example of usage below:
# test.pl
#!/usr/bin/perl
use strict;
use warnings;
use Menu;
my $menu1;
my $menu2;
# define menu1 choices
my @menu1_choices = (
{ text => 'Choice1',
code => sub { print "I did something!\n"; }},
{ text => 'Choice2',
code => sub { print "I did something else!\n"; }},
{ text => 'Go to Menu2',
code => sub { $menu2->print(); }},
);
# define menu2 choices
my @menu2_choices = (
{ text => 'Choice1',
code => sub { print "I did something in menu 2!\n"; }},
{ text => 'Choice2',
code => sub { print "I did something else in menu 2!\n"; }},
{ text => 'Go to Menu1',
code => sub { $menu1->print(); }},
);
# Build menu1
$menu1 = Menu->new(
title => 'Menu1',
choices => \@menu1_choices,
);
# Build menu2
$menu2 = Menu->new(
title => 'Menu2',
choices => \@menu2_choices,
noexit => 1,
);
# Print menu1
$menu1->print();
This code will create a simple menu with a submenu. Once in the submenu you can easily go back to the previous menu.
Thanks for all of the great answers! They really helped me figure this out and I don't think i would have ended up with such a good solution without all the help!
A BETTER SOLUTION:
Say goodbye to those ugly arrays of hashes!
Some of the code internal to the Menu.pm and Item.pm modules may look slightly confusing, but this new design makes the interface of building the menus themselves much cleaner and more efficient.
After some careful code reworking and making the individual menu items into their own objects I was able to create a much cleaner interface for creating menus. Here is my new code:
This is a test script showing an example of how to use the modules to build menus.
# test.pl
#!/usr/bin/perl
# Always use these
use strict;
use warnings;
# Other use statements
use Menu;
# Create a menu object
my $menu = Menu->new();
# Add a menu item
$menu->add(
'Test' => sub { print "This is a test\n"; system 'pause'; },
'Test2' => sub { print "This is a test2\n"; system 'pause'; },
'Test3' => sub { print "This is a test3\n"; system 'pause'; },
);
# Allow the user to exit directly from the menu
$menu->exit(1);
# Disable a menu item
$menu->disable('Test2');
$menu->print();
# Do not allow the user to exit directly from the menu
$menu->exit(0);
# Enable a menu item
$menu->enable('Test2');
$menu->print();
The Menu.pm module is used to build menu objects. These menu objects can contain multiple Menu::Item objects. The objects are stored in an array so their order is preserved.
# Menu.pm
#!/usr/bin/perl
package Menu;
# Always use these
use strict;
use warnings;
# Other use statements
use Carp;
use Menu::Item;
# Menu constructor
sub new {
# Unpack input arguments
my ($class, $title) = @_;
# Define a default title
if (!defined $title) {
$title = 'MENU';
}
# Bless the Menu object
my $self = bless {
_title => $title,
_items => [],
_exit => 0,
}, $class;
return $self;
}
# Title accessor method
sub title {
my ($self, $title) = @_;
$self->{_title} = $title if defined $title;
return $self->{_title};
}
# Items accessor method
sub items {
my ($self, $items) = @_;
$self->{_items} = $items if defined $items;
return $self->{_items};
}
# Exit accessor method
sub exit {
my ($self, $exit) = @_;
$self->{_exit} = $exit if defined $exit;
return $self->{_exit};
}
# Add item(s) to the menu
sub add {
# Unpack input arguments
my ($self, @add) = @_;
croak 'add() requires name-action pairs' unless @add % 2 == 0;
# Add new items
while (@add) {
my ($name, $action) = splice @add, 0, 2;
# If the item already exists, remove it
for my $index(0 .. $#{$self->{_items}}) {
if ($name eq $self->{_items}->[$index]->name()) {
splice @{$self->{_items}}, $index, 1;
}
}
# Add the item to the end of the menu
my $item = Menu::Item->new($name, $action);
push @{$self->{_items}}, $item;
}
return 0;
}
# Remove item(s) from the menu
sub remove {
# Unpack input arguments
my ($self, @remove) = @_;
# Remove items
for my $name(@remove) {
# If the item exists, remove it
for my $index(0 .. $#{$self->{_items}}) {
if ($name eq $self->{_items}->[$index]->name()) {
splice @{$self->{_items}}, $index, 1;
}
}
}
return 0;
}
# Disable item(s)
sub disable {
# Unpack input arguments
my ($self, @disable) = @_;
# Disable items
for my $name(@disable) {
# If the item exists, disable it
for my $index(0 .. $#{$self->{_items}}) {
if ($name eq $self->{_items}->[$index]->name()) {
$self->{_items}->[$index]->active(0);
}
}
}
return 0;
}
# Enable item(s)
sub enable {
# Unpack input arguments
my ($self, @enable) = @_;
# Disable items
for my $name(@enable) {
# If the item exists, enable it
for my $index(0 .. $#{$self->{_items}}) {
if ($name eq $self->{_items}->[$index]->name()) {
$self->{_items}->[$index]->active(1);
}
}
}
}
# Print the menu
sub print {
# Unpack input arguments
my ($self) = @_;
# Print the menu
for (;;) {
system 'cls';
# Print the title
print "========================================\n";
print " $self->{_title}\n";
print "========================================\n";
# Print menu items
for my $index(0 .. $#{$self->{_items}}) {
my $name = $self->{_items}->[$index]->name();
my $active = $self->{_items}->[$index]->active();
if ($active) {
printf "%2d. %s\n", $index + 1, $name;
} else {
print "\n";
}
}
printf "%2d. %s\n", 0, 'Exit' if $self->{_exit};
# Get user input
print "\n?: ";
chomp (my $input = <STDIN>);
# Process user input
if ($input =~ m/^\d+$/ && $input > 0 && $input <= scalar @{$self->{_items}}) {
my $action = $self->{_items}->[$input - 1]->action();
my $active = $self->{_items}->[$input - 1]->active();
if ($active) {
print "\n";
return $action->();
}
} elsif ($input =~ m/^\d+$/ && $input == 0 && $self->{_exit}) {
exit 0;
}
# Deal with invalid input
print "\nInvalid input.\n\n";
system 'pause';
}
}
1;
The Item.pm Module must be stored in a subfolder called "Menu" In order for it to be referenced properly. This module lets you create Menu::Item objects that contain a name and a subroutine reference. These objects will be what the user selects from in the menu.
# Item.pm
#!/usr/bin/perl
package Menu::Item;
# Always use these
use strict;
use warnings;
# Menu::Item constructor
sub new {
# Unpack input arguments
my ($class, $name, $action) = @_;
# Bless the Menu::Item object
my $self = bless {
_name => $name,
_action => $action,
_active => 1,
}, $class;
return $self;
}
# Name accessor method
sub name {
my ($self, $name) = @_;
$self->{_name} = $name if defined $name;
return $self->{_name};
}
# Action accessor method
sub action {
my ($self, $action) = @_;
$self->{_action} = $action if defined $action;
return $self->{_action};
}
# Active accessor method
sub active {
my ($self, $active) = @_;
$self->{_active} = $active if defined $active;
return $self->{_active};
}
1;
This design is a vast improvement over my previous design and makes creating menus much easier and cleaner.
Let me know what you think.
Any comments, thoughts, or improvement ideas?
Following is one approach. Each choice has an associated subroutine. When the choice is made, the corresponding subroutine is called. Here I am using anonymous subroutines but you can also use references to named subroutines.
use warnings; use strict;
sub menu {
my $args = shift;
my $title = $args->{title};
my $choices = $args->{choices};
while (1) {
print "--------------------\n";
print "$title\n";
print "--------------------\n";
for (my $i = 1; $i <= scalar(@$choices); $i++) {
my $itemHeading = $choices->[$i-1][0];
print "$i.\t $itemHeading\n";
}
print "\n?: ";
my $i = <STDIN>; chomp $i;
if ($i && $i =~ m/[0-9]+/ && $i <= scalar(@$choices)) {
&{$choices->[$i-1][1]}();
} else {
print "\nInvalid input.\n\n";
}
}
}
my $menus = {};
$menus = {
"1" => {
"title" => "Menu 1 header",
"choices" => [
[ "Choice 1" , sub { print "Choice 1 selected"; }],
[ "Choice 2" , sub { print "Choice 2 selected"; }],
[ "Menu 2" , sub { menu($menus->{2}); }],
[ "Exit" , sub { exit; }],
],
},
"2" => {
"title" => "Menu 2 header",
"choices" => [
[ "Choice 3" , sub { print "Choice 3 selected"; }],
[ "Choice 4" , sub { print "Choice 4 selected"; }],
[ "Menu 1" , sub { menu($menus->{1}); }],
[ "Exit" , sub { exit; }],
],
},
};
menu($menus->{1});