简体   繁体   English

如何在Perl中构建一个简单的菜单?

[英]How can I build a simple menu in Perl?

I'm working on a Perl script that requires some basic menu functionality. 我正在研究一个需要一些基本菜单功能的Perl脚本。 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 选择1
  2. Choice 2 选择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. 我不想对所有菜单进行硬编码,并使用if / elsif语句进行所有处理,因此我将菜单转换为函数。

My menus currently look like this... 我的菜单目前看起来像这样......

Menu Header: 菜单标题:

  1. Choice1 选择1
  2. Choice2 选择2
  3. Choice3 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. 我正在考虑制作一个菜单类来处理菜单,但我仍然不太喜欢面向对象的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”中选择一个选项并进入“Menu2”时,我希望从“Menu1”中保存选项供以后使用:

Menu1: 菜单1:

  1. Choice1 <-- store value if selected and go to next menu Choice1 < - 如果选择存储值,则转到下一个菜单
  2. Choice2 <-- ... 选择2 < - ......
  3. Exit <-- quit 退出< - 退出

Menu2: 菜单2:

  1. Choice1 <-- store value if selected and go to next menu Choice1 < - 如果选择存储值,则转到下一个菜单
  2. Choice2 <-- ... 选择2 < - ......
  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. 选择Choice1或Choice2应该在变量中存储一个值供以后使用, 进入下一个菜单。 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. 然后,如果您选择从Menu2返回第一个菜单,它将为您提供重新选择并重新定义变量的选项。 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! 此外,如果任何人使用类或其他数据结构有一个面向对象的方法,虽然它可能是矫枉过正,我仍然希望看到它,并试图绕过这个想法!

You could use a module such as Term::Choose : 您可以使用诸如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";
    }
}

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. 如果你不想用这个完整的OO,一个简单的方法,你可以使这个更灵活是允许每个菜单选择来控制它的执行方式。 Let's say each menu has an array of hashes that contain the menu text and a coderef that implements what the menu does. 假设每个菜单都有一个包含菜单文本的哈希数组和一个实现菜单功能的coderef。

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. menu子例程采用一系列选项,每个选项“知道”如何执行自己的操作。 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 . 如果要在菜单之间切换,菜单选项只需再次使用不同的选项列表调用menu ,如@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. 在使用Perl进行了几个月的编程之后,我学到了更多关于如何处理对象的知识,并根据friedo的答案编写了一个简单的面向对象的菜单构建模块。

# 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. Menu.pm和Item.pm模块内部的一些代码可能看起来有点令人困惑,但这种新设计使得构建菜单的界面更加清晰和高效。

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. Menu.pm模块用于构建菜单对象。 These menu objects can contain multiple Menu::Item objects. 这些菜单对象可以包含多个Menu :: Item对象。 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. Item.pm模块必须存储在名为“Menu”的子文件夹中才能正确引用。 This module lets you create Menu::Item objects that contain a name and a subroutine reference. 此模块允许您创建包含名称和子例程引用的Menu :: Item对象。 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});

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). 我决定使用Term :: Choose模块,(感谢sid_com的想法)。 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. 我仍然是面向对象Perl的新手,所以这需要花费很长时间来弄清楚它可能并不完美,但它完成了工作。 Let me know if you have any ideas or improvements! 如果您有任何想法或改进,请告诉我们!

I have found this old module without any perldoc in my perl modules... Please, give it a try... 我在perl模块中找到了这个没有任何perldoc的旧模块...请试一试...

#!/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";}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM