Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Creating Powerful Menu Systems with Term::Menus


Brian M. Kelly is a professional consultant specializing in software configuration management, and is the author of Term::Menus. His website is http://fullautosoftware.net/, and he can be contacted via e-mail at [email protected].


GUIs are the right approach for many—if not most—menuing requirements. However, there are scores of IT problems where a menu is needed or desirable, and where writing a GUI is inappropriate or simply overkill. Some examples:

  • The menuing needs are very simple.
  • The menuing needs are especially complex. Oddly enough, sometimes textual menus can be better for this task.
  • The need for using the menu is infrequent.
  • The targeted user community is highly skilled, and prefers the speed and range of options available in a text-based environment.

What drove me to write Term::Menus was the desire for a menuing module that could cope with great complexity, yet be easy to install, implement and configure, and—most importantly—dispense with the unnecessary overhead of writing and (as many power users will agree) of having to use a GUI.

Term::Menus allows you to create powerful Terminal, Console, and CMD environment menus. Any Perl script used in a Terminal, Console, or CMD environment can employ Term::Menus to include a menu facility that includes submenus, forward and backward navigation, single or multiple selection capabilities, dynamic item creation, and customized banners. All this power is simple to implement with a straightforward and very intuitive configuration hash structure that mirrors the actual menu architechture needed by the application. A separate configuration file is optional. Term::Menus is cross-platform compatible. Term::Menus also has complete pod documentation embedded, so I won't discuss usage details in this article. Rather, I'll focus on describing and suggesting scenarios where using Term::Menus can really make an impact.

In one of my previous consulting engagements, I wrote a Perl-based file-transfer architecture that managed the transfer of encrypted files over the Internet between my client and over a hundred of its vendor partners. In these exchanges, there were essentially four ways to pass these files: client to vendor, client picks up from vendor, vendor to client, and vendor picks up from client. Politics, not technology, often determined which method was employed. When the client first gave me my marching orders, the "nonnegotiable" policy was that everyone would pick up files from the client host. That policy, of course, lasted less than 24 hours: Vendor A insisted the client pick up files from its host. The client caved and, panic stricken, looked to me for a solution. Luckily, I anticipated this development and had already built flexibility into the architechture. Vendor A made it as tough as possible. They refused anything but read access to their ftp host, which meant we couldn't move, delete, or rename files we already picked up. Furthermore, they failed to clean this host on any kind of normal schedule, so after a couple of months, there were hundreds of files—new and old—cluttering the pick-up directory. To keep track of which files were already transfered and which weren't, I had to utilize a Berkeley Database that was automatically updated and cleaned (with Perl code, of course) whenever Vendor A did manage to do some unscheduled maintenance. Occasionally, however, there would be a need to "re-pick up" a file or force a manual transfer or check if a file was already transferred, or awaiting transfer.

This posed a problem: How do I search the remote file system and the Berkeley DB, compare the results, and present the findings in an easy to use and navigable menu format so that a user can tell—at one glance—what status and actions are available?

Term::Menus was my solution. I wanted to see all the files on the remote host with those already transferred, and untransferred, with all the untransferred files preselected by default. Here's the code that does that, and the output that the user will see:

use Term::Menus;

sub DB_all_files_transferred {

  return 'grep'
  # normally would be a list of files returned from the DB

}

my $filter=DB_all_files_transferred();

%Menu_1 = (

  Label   => 'Menu_1',

  Item_1  => {

    Text    =>      "]Convey[ has  **NOT**  been transferred",
    Convey  =>       [ `ls -1 /bin` ],
                         # normally would be a handle
                         # exp: $ftp_remote->cmd('ls -1')
    Include  =>       qr/$filter/,
    Default  =>       '*',

  },

  Item_2  => {

    Text    =>      "]Convey[ has been transferred",
    Convey  =>       [ `ls -1 /bin` ],
    Exclude =>       qr/$filter/,

  },

  Select  => 'Many',

  Banner  => '   Please select all files for immediate'.
                  ' transfer:',

);

my @output=Menu(\%Menu_1);

=====> HERE IS WHAT THE USER WILL SEE:

   Please select all files for immediate transfer:

   *  1.        addr2name.awk has  **NOT**  been transferred
   *  2.        awk.exe has  **NOT**  been transferred
   *  3.        gawk-3.1.4.exe has  **NOT**  been transferred
   *  4.        gawk.exe has  **NOT**  been transferred
   *  5.        igawk has  **NOT**  been transferred
   *  6.        pgawk-3.1.4.exe has  **NOT**  been transferred
   *  7.        pgawk.exe has  **NOT**  been transferred
      8.        822-date has been transferred
      9.        DllPlugInTester.exe has been transferred
      10.       ELFDump.exe has been transferred

   a.  Select All.   c.  Clear All.
   f.   Finish.

   929 Total Choices

   Press ENTER (or "d") to scroll downward

   OR "u" to scroll upward  (Press "q" to quit)

   PLEASE ENTER A CHOICE:

=====================================
Note the stars in front of items 1–7. These indicate preselected choices as configured in the sample code above by the Default => '*' element. As you can see, a little code can create a very powerful menu. This is possible, in large part, because of the power of Perl. With Perl's relatively recent addition of precompiled regular expressions, creating filters to preselect, exclude, and include menu items from potentially huge lists (even millions of possible entries) with incredible precision is now as easy as the code shown above.

In another engagement, I used Term::Menus to incrementally construct items with submenus. Again using Perl (and also a lot of Cygwin), I coded a deployment system for another large client. This was used for updating its rather extensive corporate web site (rumor had it that they spent in the neighborhood of $100 million building it, and it was indeed large). The site was broken down into multiple components, which were distributed to multiple environments—dev, test, qa, and prod. Each environment had multiple hosts. I've seen lots of GUIs for doing this kind of work, and found them all to be very inflexible, not to mention slow and unwieldy. With a Term::Menus implementation, I had much greater flexiblility, and using it was always faster.

Here's an example of the sort of thing that Term::Menus made easy on that job. First, I broke the job down into subtasks. The site consisted of Java, HTML, and images. So when the boss told me to create a way to deploy Java from the Version 1.0 in source control to qa, this is what I did:

use Term::Menus;

%Menu_1 = (

  Label    => 'Menu_1',

  Item_1   => {

    Text     =>       'Deploy Java',
    Convey   =>       'Java',
    Result   =>       \%Menu_2

  },

  Item_2   => {

    Text     =>       'Deploy HTML',
    Convey   =>       'HTML',
    Result   =>       \%Menu_3,

  },

  Item_3   => {

   Text      =>       'Deploy Images',
   Convey    =>       'Images',
   Result    =>       \%Menu_4,

  },

  Select   => 'One',  # Can be left out - default setting is 'One'

  Banner   => '   Please select the components to deploy:'

);

my @output=Menu(\%Menu_1);

=====> HERE IS WHAT THE USER WILL SEE:

   Please select the components to deploy:

      1.        Deploy Java
      2.        Deploy HTML
      3.        Deploy Images

   (Press "q" to quit)

   PLEASE ENTER A CHOICE:

====================================
So far, so good. Suppose we choose item —Deploy Java. At a glance, we can tell from the code above that this choice should take us to a new Menu—Menu_2:

use Term::Menus;

sub get_all_versions {  return 'LATEST', 'Version 1.0', 'Version 1.1'  }

%Menu_2 = (

  Label    => 'Menu_2',

  Item_1   => {

    Text     =>       'Deploy ]Previous[, ]Convey[',
    Convey   =>       [ &get_all_versions() ],
    Result   =>       \%Menu_5

  },

  Banner   => '   Please select the Version to deploy:'

);

%Menu_1 = (  .  .  .  );  # See Above

my @output=Menu(\%Menu_1);
  
=====> HERE IS WHAT THE USER WILL SEE:

   Please select the Version to deploy:

      1.        Deploy Java, LATEST
      2.        Deploy Java, Version 1.0
      3.        Deploy Java, Version 1.1

   (Press "q" to quit)

   PLEASE ENTER A CHOICE:

====================================
And, naturally, this leads us to Menu_5:

use Term::Menus;

sub get_all_versions {  return 'LATEST', 'Version 1.0', 'Version 1.1'  }

sub do_deployment {
  print "We will deploy $_[0], $_[1] to $_[2]\n"
}

%Menu_5 = (

  Label    => 'Menu_5',

  Item_1   => {

    Text     =>       'Deploy ]P[{Menu_1}, ]Previous[ to ]Convey[',
    Convey   =>       [ 'development','testing','qa','production' ],
    Result   =>       "&do_deployment(  ]Previous[{Menu_1},  ]P[,  ]Selected[  )",

  },

  Banner   => '   Please select the Destination Environment:'

);

%Menu_2 = (  .  .  .  );  # See Above

%Menu_1 = (  .  .  .  );  # See Above

my @output=Menu(\%Menu_1);

=====> HERE IS WHAT THE USER WILL SEE:

   Please select the Destination Environment:

      1.        Deploy Java, Version 1.0 to development
      2.        Deploy Java, Version 1.0 to testing
      3.        Deploy Java, Version 1.0 to qa
      4.        Deploy Java, Version 1.0 to production

   (Press "q" to quit)

   PLEASE ENTER A CHOICE:

====================================
Assume the user then chooses item 3. The result is that three items are sent as arguments to the subroutine &do_deployment(). The macro ]Previous[{Menu_1} is replaced with the item selected from the first Menu—in this example, "Java." (The ]P[ macro is shorthand for ]Previous[.) By itself, ]P[ indicates the selection of the immediately previous parent menu. Grandparent and higher menu selections can be accessed with the syntax ]P[{<Menu Label>} where <Menu Label> is the Label element of the Menu whose selection is needed. The second argument is populated by the ]P[ macro (without the {Menu_1} modifier—indicating the parent menu), which is replaced with "Version 1.0." Finally, the ]Selected[ macro is replaced with the selection of the current Menu—in this case, "qa."

In this way, complex choices can be broken down into small components and built-up sequentially. This allows for a very powerful ongoing verification process that helps to eliminate mistakes when launching critical production processes, or when the user is presented with choices after hours or days of processing (where a wrong choice could be quite costly).

Menus in Term::Menus are also navigable. Suppose you have the above menu, and are able to see the resulting choices, but wish to review the previous menu's banner, which might have contained critical instructions or cautions. To do this, all one needs to do is type "<" (to navigate backwards), or ">" (to navigate forward) at the "PLEASE ENTER A CHOICE:" prompt, and the preceding screen will return:

=====> HERE IS WHAT THE USER WILL SEE:

   Please select the Version to deploy:

      1.        Deploy Java, LATEST
   -  2.        Deploy Java, Version 1.0
      3.        Deploy Java, Version 1.1

   (Press "q" to quit)

   PLEASE ENTER A CHOICE:

====================================
Note the dash in front of item 2. This indicates to the user that they have navigated away from the child menu back to a parent. The dash also indicates that no choices, or selections were made in the child menu. If we continue to navigate back:

=====> HERE IS WHAT THE USER WILL SEE:

   Please select the components to deploy:

   + 1.        Deploy Java
     2.        Deploy HTML
     3.        Deploy Images

   (Press "q" to quit)

   PLEASE ENTER A CHOICE:

====================================

You now see a plus sign in front of item 1. This also tells the user that they have navigated away from a child, but that in this case, choices or selections have been made.

The pod documentation also discusses item negation, which allows you to prohibit the selection of one item if another incompatible item is selected.

Term::Menus is a work in progress, and releases in the near future will contain some powerful new features. I plan to add regular expression-based search capabilities. Like the vi editor, typing a slash at the prompt ("/") followed by text will search all items for the string, and present only those that match. Additonally, one will be able to "Select All," and then deselect only matching items. Selection logging is also on the drawing board.

I hope you've enjoyed this brief tour of Term:Menus. Its adaptibility has evolved in response to real-world needs, and with it, you can build some very powerful menu-based systems.

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.