Interactive Scripts

Core Technology

Article from Issue 201/2017
Author(s):

Some shell scripts are silent; others communicate to users extensively. Learn how to make their dialog smoother with, er … , dialogs.

As an administrator, you may view shell scripts as wordless minions that do their job and die silently – unless an error occurs, of course. At least, that's what we expect from a well-behaving Unix command. There are good reasons for that (think pipelining), but it doesn't mean you must have it that way all the time.

In this Core Tech, we'll learn some tricks to make shell scripts interact with the user via command prompts and dialog boxes. This is how you got Slackware installed, right? (You did try Slackware, didn't you?) Yes, the Slackware installer is basically a shell script, and with some tools under your belt, you can do no worse than Patrick Volkerding. Sound good? If so, let's go and have some text-mode fun.

Command Prompt

Perhaps the simplest form of interactivity you can have in your scripts is a command prompt. That's how shell itself is made interactive, after all. Getting pieces of data from the user is possible with the read built-in command [1]. But, to make things fancier, let's throw some history support and other readline goodies into the mix.

To enable readline support in read, pass the -e switch to the built-in. This provides a bunch of keyboard shortcuts that you got used to in the shell. Tab completion works; you can move word-wise forward and backward with C-f and C-b (where C is typically the Control key); using C-a and -e bring you to the beginning and the end of the line, respectively. The Home and End keys should work, too. You can even use C-r to search the history. These are just a few examples; readline is really powerful. It comes with its own configuration file, usually ~/.inputrc, and the complete list of commands, including default key bindings, is in the Bash man page [2].

While readline typically binds history-related keyboard shortcuts like C-R automatically, it needs something extra to fill in the actual history contents. First, you read the history file via history -r ~/.script_history at the beginning of your code. Using history -s args stores args as a single line of history. When your script ends, you can save updated history to the file via history -w ~/.script_history. You can also customize Tab completion behavior in your scripts, but that's another story.

Next, let's consider a simple example. What you see in a Listing 1 is what could be a command loop in some installer. Remember those proprietary packages that come as an executable shell script combined with a tarball? The script implements several install stages and rudimentary state transitions. Other functions, such as show_license and ask_install_root, are irrelevant and not shown here. As installation is typically a one-shot procedure, I don't make use of the history feature here. And, because it's rather trivial to type "yes" and "no" (and because our legal department wants this answer to be very explicit), I haven't enabled readline in that stage. I do enable it on the next stage though, as Tab completion for filesystem paths is typically a good idea.

Listing 1

Read Built-In Usage Example

 

Making Dialogs

Although command prompts certainly work, they are hardly intriguing in 2017. Today, we demand a dialog-based "experience," preferably a nice graphical one, but an old-school ncurses-based approach would also do. Bash can't play such tricks on its own (at the very least, it's counter-Unix-way). But there are external commands that can.

Of course, I'm referring to the dialog. Although it's not the only choice, the dialog tool is ubiquitous and usually comes installed by default. If not, you can probably find it in the distribution repositories under this very name.

The dialog tool provides interface components (or widgets) you'd expect from a typical UI toolkit. I mean menus, checkboxes, text and password inputs, progress bars (called "gauges" here), file selectors, and so on. There are many widget types, but not everything is necessarily compiled into the tool. You tell dialog which widget you want via a command-line argument, and it is possible to display more than one widget in a single command invocation. In turn, dialog dumps user selections (if any) into stderr (or whatever file descriptor you supplied via --output-fd). Simple choices, such as "yes" or "no," are returned as exit codes. You can use environment variables to define which codes are actually returned [3]. The default is 0 (i.e., success) for buttons such as Yes or OK, 1 for No or Cancel, 2 for Help, and a few others as well.

Let's go through a simple "Hello, World!" example (Figure 1):

Figure 1: An obligatory "Hello, World!" example.
dialog --msgbox "Hello, World!" 5 20

That's pretty straightforward. You tell dialog you want a plain message box (--msgbox); 5 and 20 are the dialog box height and width, respectively. For a modern flat look, consider adding the --noshadow argument to disable shadow.

Now, back to our wannabe-installer script. Remember the stage where it shows you the license and asks if you accept it? It's doable with a single dialog call (Listing 2).

Listing 2

Dialog Call

 

Let's go through this step by step. First, we tell the first widget that we want it to clear the screen after itself. This is what you typically expect from a shell script. However, omitting it creates an interface that feels more like a real, multi-window one (Figure 2). There are some options you can use to tweak it even further, as shown in the man page [3].

Figure 2: The --textbox widget and chaining two dialogs in one call. Note that in absence of --clear, dialogs are overlaid.

Then, we redefine the label that is shown on exit buttons. It defaults to "EXIT" (surprise!), which is not what we want here, because in this case, the exit button doesn't terminate the script.

Then comes the textbox widget itself. It's basically a file viewer. Here, we show the GPL v3 license text that comes with many Debian-based distributions. Licenses are a long read, so this time the dialog is whopping 80x20 in size. And, of course, the text is scrollable.

To display (unconditionally) the second widget, you prefix it with --and-widget. Here, it is a "yes/no box" which asks a simple question. It is also possible to override the "Yes" and "No" button labels, yet we don't need it here. To determine whether or not the user agreed, you can check the exit code. Zero means the license was accepted.

You Choose

In previous examples, interactivity was somewhat limited: a dialog box, yes or no, and that's it. This might be good for a wedding, but it doesn't build a complete user interface. It's time for users to make some real decisions.

Perhaps the most common form of choice in user interfaces is a menu. Most GUI applications these days provide a menubar, or at least a button (or ribbon). With dialog, you are limited to a --menu widget. Listing 3 explains how you create one.

Listing 3

Menu Dialog Sample

 

The first three arguments to --menu should be already familiar. The next one, dubbed menu-height, is the number of menu items to show simultaneously. If your menu has more, dialog will scroll the contents automatically.

What follows is a number of pairs consisting of a tag and a caption. You can think of a tag as the menu item identifier. When the user selects anything, you get the tag in stderr or to wherever --output-fd points.

Here, we build a simple four-item menu, which allows a user to either back up or restore data, get help, and exit the script. Only the latter option is really implemented. However, --dselect and --fselect [3], the directory and file selection widgets, are here to implement two remaining options. The dialog tool also displays both tags and captions and assigns unique single-letter hotkeys (shown in red in Figure 3) to each option. Pressing hotkeys selects the corresponding option but doesn't confirm the selection: you need to press Enter or click OK for that. I'd suggest not to using spaces within tags, as it makes things a bit more complicated. We redirect stderr to a temporary file and read it back to learn the user's choice. If it is empty, it means the user pressed Escape or otherwise canceled the script.

Figure 3: The --menu widget in dialog is well-suited for choosing one of the mutually exclusive options.

Menus are great for setting up one choice. But, sometimes you may want to select several options (Figure 4). The checklist widget is one way to do it with dialog. It is very similar to menu except it places a checkbox next to each item. You can mark items with Space and confirm your choice with Enter. Mouse should work as well (and it's not limited to a checklist widget, naturally).

Figure 4: Checklists are like menus that let you choose more than one option.

Now, please have a look at Listing 4, which shows a complete example. The first four options describing the dialog appearance are the same both for menu and checklist. Consult the description above for the refresher. Setting contents is a bit different, though. For a menu, tag and item suffice; for a checklist, we also need a way to tell which options are initially on. So, each checklist entry comes via tag item status triplet, where status is on if you want the option checked.

Listing 4

Checklist Dialog Sample

 

After a user is done with the selection, dialog dumps selected items' tags into stderr or --output-fd, as usual. By default, all tags go on a single line separated by spaces and quoted as necessary. To make parsing easier, we tell dialog to dump tags one per line with --separate-output. You can use our good old friend read (sans -e this time) to learn the user's selection.

while read tag; do
    # Do something
done <"$choice"

Buy Linux Magazine

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Zenity and KDialog

    Zenity and KDialog let you integrate your scripts with the native KDE or Gnome environment.

  • Ruby and Glade

    Application development shouldn’t be a chore. We’ll show you how to simplify the development process with the Ruby programming language and the Glade interface design utility.

  • Zenity Dialogs

    The Zenity command-line utility lets you create simple dialog boxes with your own data or with the output of utilities and applications.

  • Perl: PerlPanel

    One panel has a neat collection of applets and another has spectacular looks – but a combination of the two is rare. Now help draws nigh for the desktop: PerlPanel is extensible with do-it-yourself widgets.

  • Command Line: KDE Kiosk

    KDE Kiosk lets administrators control user environments, including settings, themes, and access to the command shell and designated peripherals.

comments powered by Disqus