Create menus and dialogs for shell scripts

The shell by itself is already an application programming interface, but few users still query data from a command line. To adapt to the habits of current users, the dialog tool simulates the elements of a graphical interface.

The look may be a bit old-fashioned but, in terms of speed, the technology is hard to beat. If an X server is running on the system, you can provide users some additional comfort by relying on a dialog counterpart, such as Zenity or Gtkdialog (see the "Relatives" box).

Relatives

Apart from the dialog framework described in this article, other projects exist with the same purpose. In Table 1, you can see to what extent other candidates provide dialog types. Some require a graphical user interface, among them Xdialog [3], Zenity [4], Kdialog [5], and Gtkdialog [6]. The latter deviates somewhat from the usual pattern: You create the XML files with the instructions the program reads in during the call.

Table 1

SHC Options

Dialog Command Xdialog Zenity Kdialog
--yesno --yesno --question --yesno
--msgbox --msgbox --warning or --info --msgbox
--infobox --infobox --passivepopup
--textbox --textbox --textinfo --textbox
--tailbox --tailbox
--pause
--gauge --gauge --progress --progressbar
--form
--inputmenu --2inputsbox/--3inputsbox
--calendar --calendar --calendar --calendar
--timebox --timebox
--inputbox --inputbox --entry --inputbox
--editbox --editbox --textinfo --textinfobox
--dselect --dselect --file-selection --getexistingdirectory
--fselect --fselect --file-selection --getopenfilename/--getsavefilename
--checklist --checklist --list --checklist
--radiolist --radiolist --list --radiolistl
--menu --menubox --list --menu

Screens and Menus

To read user input, the built-in Bash read command is often used together with the echo command for simple strings. If you want to change the input prompt, enter the -p option with the new text (Listing 1).

Listing 1

Read User Input

01 #! /bin/sh
02 echo "---------------------------"
03 echo "echo -n \"Input: \";read a"
04 echo -n "Input: ";read a
05 echo "---------------------------"
06 echo "read -p \"Input: \" a"
07 read -p "Input: " a
08 echo "---------------------------"

The -n option on line 4 prevents a newline at the end of the echo statement, which makes the text appear directly to the right of the read prompt.

Quotes are escaped with backslashes so that they don't have any effect on the results. The two methods shown on line 4 and line 7 achieve the same results.

The option -i <text> together with -e (readline support) provides the user with an entry already at the prompt. This works a bit easier with the Readpreprompt [1] external program. For proper use, run the command in a subshell, as in Listing 2, line 3. The tool sends the result to standard output. This command can make it easier to create dialogs for database applications.

Listing 2

Using readpreprompt

01 #! /bin/sh
02 a="Old value"
03 a=$(readpreprompt "Input: " "$a")
04 echo $a

The built-in echo command has little effect on the outcome format. To accurately position numerical values in the dialog, the printf command borrowed from the C programming language can help. The command does the rounding for decimal numbers and takes the shell language settings into account.

The basic structure of the printf command is printf "%<format>" <data>. The most important instructions for formatting appear in Table 2. The command differentiates between period and comma as decimal separator. In a pinch, pass the variable LC_NUMERIC or LANG with set and unset inside the script.

Table 2

Printf Instructions for Formatting

%5.2f Floating-point with five digits before and two digits after the decimal separator
%.10s String with a maximum 10 characters
%X\n Hexadecimal in uppercase
%x\n Hexadecimal in lowercase
%#X\n \ Hexadecimal in uppercase with leading 0x
%i\n Integer
-r \ Loosens the security settings during compilation, so that binaries can run on other computers with the same operating system. Currently mandatory for Arch Linux.
-D Enables debug mode of the binary program, which can generate a lot of additional information.
-T Creates a program traceable by Strace or similar tool.
-A Shows a short info and quits SHC without compiling the script.

An even easier method is to shoehorn output data into the desired format with tr , which you can add right in the statement (Listing 3, Example 2). Always end the printf statement with a newline (\n ). In some cases, the command also allows tabbing. The examples in Listing 3 show the main functions, and Figure 1 shows the result.

Listing 3

Format Output Data

#! /bin/bash
# Sample values
a=987,455
b=987.455
c="A-long-word"
d=30
# Example 1
# Output floating-point rounded to 2 digits
printf "%5.2f\n" $b
# Example 2
# Output floating-point, comma converted to period decimal separator
printf "%5.2f\n" `echo $a | tr , . `
# Example 3
# Text output truncated to 10 characters
printf "%.10s\n" $c
# Example 4
# Numeric conversions at presentation:
# Integer, hexadecimal, octal
printf "%i %X %o\n" $d $d $d
# Example 5
# Output integer,
# Hexadecimal (lowercase) with leading "0x"
printf "%i  %#x\n" $d $d
Figure 2: A simple menu with numerical selections.

Note especially the string output. A space character is typically a separator, which makes every word on the line a separate variable value. In Listing 3, you would get a five-line output if you used space characters instead of the hyphens to separate the words in the c variable.

When creating menus, proceed as in Listing 4. If there are just a few menu items, use numbers to identify them, which makes keyboard entry easier. If the numbers get too high, use lowercase characters. The script handles the function variables as strings, which avoids error messages when entries don't match. Figure 2 shows the example in action.

Listing 4

Create a Menu for User Input

! /bin/sh
while true; do
  clear
  echo "(1) Function A"
  echo "(2) Function B"
  echo "(9) End"
  echo " "
  echo -n "Choose function: "; read f
  if [ "$f" = "1" ]; then
    echo "Function A";sleep 3
  elif [ "$f" = "2" ]; then
    echo "Function B";sleep 3
  elif [ "$f" = "9" ]; then
    exit
  fi
done
Figure 2: A simple menu with numerical selections.

The example in Listing 5 (Figure 3) shows a dialog to process data for address management. For practical application, the functions for retrieving and saving the data would be from and to a database.

Listing 5

Create a Dialog to Process Data

#! /bin/sh
# Address sample data
# Database access would be enabled at this point
a="Mr."
b="Tux Penguin"
c="Icy Rd.  123"
d="South Pole 10001"
while true; do
  clear
  echo "-----------------------------------------"
  echo "      Address Processing"
  echo "------+------------+---------------------"
  echo "F-No. |            |    Value"
  echo "  1   |     Title: | "$a
  echo "  2   |      Name: | "$b
  echo "  3   |    Street: | "$c
  echo "  4   | City/Code: | "$d
  echo "------+------------+---------------------"
  echo "actions: [F-No.]: change line, [s] save,"
  echo "[q] quit"
  echo -n "action: ";read wn
  if [ "$wn" = "1" ]; then
    a=$(readpreprompt "Line $wn: " "$a")
  elif [ "$wn" = "2" ]; then
    b=$(readpreprompt "Line $wn: " "$b")
  elif [ "$wn" = "3" ]; then
    c=$(readpreprompt "Line $wn: " "$c")
  elif [ "$wn" = "4" ]; then
    d=$(readpreprompt "Line $wn: " "$d")
  elif [ "$wn" = "s" ]; then
    echo "This would be written to the database"
    break
  elif [ "$wn" = "q" ]; then
    exit
  fi
done
echo "-----------------------------"
echo $a
echo $b
echo $c
echo $d
Figure 3: With just a few lines of shell code, you can create a dialog to process data from a database.

With Dialog

The dialog [2] program comes preinstalled on many current distributions. The instructions consist of commands for designing the dialog, for defining the type of dialog (e.g., message, progress, input, etc.), and for geometric specifications. At the beginning of the instructions, you specify the title and background title. The commands also might require geometric settings.

The times of manually setting the size of the dialog are thankfully a thing of the past. If you want to omit the height in row and the width in characters, simply enter 0 0 , which works for most instructions. The tool then automatically adjusts the dialog proportions.

The following code shows the command structure.

$ dialog --title "Title" --backtitle "Background Title" <more parameters> 0 0

The type instructions indicate what the lines of code should do. Do you want the user to read, enter, or choose something? Table 3 shows the possible options.

In the example, the data lines are numbers and the functions are characters. The script refreshes the dialog after each change.

Although the yes/no and message boxes await a reply action, the info box is purely informational and will time out with no reply, based on the sleep or some other external setting.

You can expand the yes/no box with another button:

--extra-button --extra-label ""

Clicking the button returns the value 3 . The second expansion possibility is the help button, --help-button , which doesn't require additional values and returns the value 2 . Thus, the yes/no box provides four possible return values.

The small script in Listing 6 shows the dialog settings in action and how to add properties. The properties in the first call are extended correspondingly. Before the --yesno , you'll find the additional parameter --ok-label "<text>".

Listing 6

Dialog Settings in Action

#!/bin/bash
#! /bin/sh
while true; do
    dialog --title "YES/NO BOX" --backtitle "BACKGROUND TITLE" \
           --help-button --extra-button --extra-label "EXTRA" \
           --ok-label "Agree" --yesno "QUERY TEXT" 0 0
    dialog --title "MESSAGE BOX" --backtitle "BACKGROUND TITLE" \
           --msgbox "Return value: $?" 0 0
    dialog --title "END SCRIPT" --backtitle "BACKGROUND TITLE" \
           --defaultno --yesno "End shell script?"  0 0
  if [ $? -eq 0 ]; then
    exit
  fi
done

The same applies to the "No" button (--no-label "<text>" ), "Yes" button (--yes-label "<text>" ), and "Help" button (--help-label "<text>" ). The --defaultno option in the last dialog statement sets the default selection to No .

For the gauge (progress) box, the percent value is piped to the dialog command (Listing 7). The element also displays units of an overall value.

Listing 7

Gauge Progress

#! /bin/sh
percent=0
while [ $percent -lt 100 ]; do
  # Set the percent value
  percent=$(echo "$percent + 10" | bc)
  # Gauge value is passed via pipe to dialog
  echo $percent | dialog --title "GAUGE" --backtitle "BACKGROUND TITLE" --gauge "PROGRESS" 0 0
  # Sleep only for demo!
  sleep 1
done

Listing 8 is a "ready to use" script. It shows the current size of the /home directory (Figure 4). Note this only works if /home is on its own partition, and the extraction of values with df can be a bit confusing. The percentage appears as a status bar, and the free space shows as clear text.

Listing 8

Show Current Size of /home

#! /bin/sh
# Percentage derived from df -h
PERCENT=$(df -h | grep "/home" | tr -s , , | cut -d' ,  -f5 | cut -d% -f1)
FREE=$(df -h | grep "/home" | tr -s , , | cut -d' , -f4)
echo ${PERCENT} | dialog --title "Size of /home directory" --backtitle "System Information" \
  --gauge "\n Current free space: ${FREE}B" 10 50
sleep 5
Figure 4: At a glance, you can see how full the /home partition is.

Forms

If you need not only simple forms but also a complex input box, dialog provides you with the necessary tools. You can navigate in the forms using the tab and arrow keys. The syntax might seem confusing at first, in that there are size and position adjustments to make.

Listing 9 is a script that allows editing of data and shows how to return it as variables that can stored permanently in a file. Figure 5 shows how the position and size values are rendered.

Listing 9

Return Data as Variables

#! /bin/sh
a="Mr."
b="Tux"
c="Penguin"
dialog --title "Title" --backtitle "Customer Data" --ok-label "Save" \
  --stdout --form "Catalog" 10 60 3 "Salutation  " 1 1 "$a" 1 15 30 0 \
  "Family Name " 2 1 "$b" 2 15 30 0 "First Name  " 3 1 "$c" 3 15 30 0 > output.txt
a=$(cat output.txt | head -1)
b=$(cat output.txt | head -2 | tail -1)
c=$(cat output.txt | head -3 | tail -1)
rm output.txt
dialog --title "Title" --backtitle "Background Title" --msgbox \
  "Saved values: \n $a \n $b \n $c " 0 0
Figure 5: Position and size values for form elements.

To ensure that redirecting output to a file works, you can precede --form with the additional --stdout option. In msgbox , use the newline character (\n ) to output each variable value on a separate line.

Forms with the inputmenu function need fewer size and position parameters, but you may not get the data of all the fields at the end. You can change only single fields with each dialog call. The output includes the action (RENAMED ), field identifier and data. To be sure the function works, use the --stdout option, as you do for --inputmenu . Post-processing of data occurs with cut (Listing 10). Here, it's important that the field names do not have space characters. Databases can provide the corresponding field names so that it's easy to provide SQL statements with matching variables.

Listing 10

Post-Processing of Data

#! /bin/sh
newval=$(dialog --title "Title" --backtitle "Background Title" --stdout --inputmenu "MENU HEADING" \
  17 60 15 "row-1 >" "Value 1" "row-2 >" "Value 2" "row-3 >" "")
column=$(echo $newval | cut -d \> -f 1 | cut -b 9-)
entry=$(echo $newval | cut -d \> -f 2 | cut -b 2-)
dialog --title "Title" --backtitle "Background Title" --msgbox "Stored Values: \n $newval \n $column \n $entry " 0 0
dialog --title "Title" --backtitle "Background Title" --msgbox "Saved values: \n $a \n $b \n $c " 0 0

The --calendar function provides an easy way to input date information. Calling it displays the current month and the day of the week in the left column. Use the cursor and up/down arrows to move to other data. Clicking the OK button returns the highlighted value.

To fill a variable, again use the --stdout option for output. Because dialog separates the data parts with a slash character, tr translates them into commas. Listing 11 shows how to construct a script that does just that.

Listing 11

Translate Data with tr

#! /bin/sh
a=$(dialog --title "Title" --backtitle "Background Title" --stdout --calendar "TEXT" 0 0 | tr \/ .)
dialog --title "Title" --backtitle "Background Title" --msgbox "Selected date: $a " 0 0

You enter time values with --timebox . To accept the entered value, again use --stdout . The window shows the time the call to the function was made. To enter different values, use the arrow key to proceed to the next entry field and provide another date.

You can enter any alphanumeric values using the input box (--inputbox ). Register a value in a script by using the prefix --stdout option. You can provide an editable default value. The process requires only one line:

a=$(dialog --title "Title" \
  --backtitle "Background Title" \
  --stdout --inputbox "HEADLINE" \
  0 0 "DEFAULT")

You can edit small text files in a mini-editor (--editbox ). Give the filename as an argument. If no file exists, create one with touch .

You can write enter or modify text and then save to another file or overwrite the original file. Specify a height and width for the widget so that the inner window doesn't displace the headline. Again, use --stdout so that the entries don't end up in the standard error output. This function again requires a simple line of shell code, as follows:

dialog --title "Title" --backtitle \
  "Background Title" --stdout \
  --ok-label "Save" --editbox text.txt \
  20 75 > new.txt

You can select directories and filenames from within the script with --dselect and --fselect , respectively.

These two parameters are usually used together. It's usually a good idea to prompt users unfamiliar with the context. You select a directory at the same level as the default with arrow keys and pressing the spacebar.

Choose the path on the left with --fselect ; press the spacebar twice and choose the file on the right. Navigate between fields with the tab key and inside fields with the arrow keys.

The bottom field allows for manual entries. With the cursor in it, move to the next highest level by deleting everything up to the right-most slash (Figure 6). You can see how to select a directory and file in Listing 12.

Figure 6: The --dselect and --fselect widgets make choosing directories and files easy.

Listing 12

Select a Directory and File

#!/bin/sh
a=$(echo $HOME)
while true
do
  a=$(dialog --title "Title" --backtitle "Background Title" --stdout --fselect $a 0 0)
  dialog --title "Title" --backtitle "Background Title" --defaultno --yesno "$a Apply"  0 0
  if [ $? -eq 0 ]; then
    break
  fi
done

You can provide multiple selections using --checklist . This option returns the designator of each entry enclosed in quotes. Field separators are space characters. You give each entry a status of on for marked or off for unmarked. You can specify space-separated values for height, width, and list height.

The list height should correspond to the actual number of items. If all the items don't fit, the box is scrollable.

Listing 13 shows the code together with tr , which removes the quotes. You can build a single-selection box in the same way by using the --radiolist dialog type and setting a single entry to on , getting the tag value without the quotes. Use the space bar to choose the value:

a=$(dialog --title "Title" \
  --backtitle "Background Title" --stdout \
  --radiolist "SELECTION HEADING" \
  10 40 3 TAG-1 "INFO-1" on \
  TAG-2 "INFO-2" off TAG-3 "INFO-3" off)

Listing 13

Provide Multiple Selections

#!/bin/sh
a=$(dialog --title "Title" --backtitle "Background Title" --stdout \
           --checklist "SELECTION HEADING" 10 40 3 TAG-1 "INFO-1" on \
           TAG-2 "INFO-2" off TAG-3 "INFO-3" on)
a=$(echo $a | tr -d \")
sleep 5

This changes the item selection to a single one. The script creates the data query dynamically. The tag consists of the record value or a unique value that the script uses to read in the record value.

The example in Listing 14 shows an SQL query with the psql client for the PostgreSQL database. The aim is to pass the customer number orgnr to other parts of the script. The script writes the instructions for the menu with --radiolist into a variable and executes the code with eval . For further development, the --form or --inputmenu dialog type can be used depending on programming preference.

Listing 14

Sample SQL Query

#!/bin/sh
# Search dialog for the database query
sube=$(dialog --title "Customer Search" --backtitle "Customer Management" \
  --stdout --inputbox "Enter customer name" 0 0 "")
# Check for sufficient data
a=$(psql -t -c "select orgnr from customers where name = '$sube';")
if [ -z "$a" ]; then
  dialog --title "Customer Search" --backtitle "Customer Management" \
         --msgbox "No matching customers found! " 0 0
  exit  # or break when in loop!
fi
# Get data for single-selection and select structure
# Counters for first "on" status
v=0
teil1=$(echo "a=\`dialog --title \"Customer Search\" --backtitle \"Customer Management\" \
  --stdout --radiolist \"found: \" 10 40 3 ")
# Determine customer numbers
for i in $(psql -t -c "select orgnr from customers where name = '$sube' \
  order by lastname, firstname, birthday ;"); do
  if [ $v -eq 0 ]; then
    status="on"
    v=1
  else
    status="off"
  fi
  info=$(psql -t -c "select (lastname || , , || firstname || , , || birthday ) \
  from customers where orgnr = $i;";)
  part2=`echo $part2 $i \"$info\" $status`
done
selection_mask=$(echo "$part1 $part2\`")
echo $selection_mask
eval $selection_mask
dialog --title "Customer Search" --backtitle "Customer Management" \
  --msgbox "Record number $a selected " 0 0

You can add menus to your program with the --menu dialog type. It provides a tag that you later evaluate. As with almost all other value output, use --stdout before the dialog type. Listing 15 shows an example.

Listing 15

Add Menus

#!/bin/sh
while true; do
  a=`dialog --title "TITLE" --backtitle "BACKGROUND TITLE" \
    --stdout --menu "MENU HEADING" 0 0 0 1 "FIRST" 2 "SECOND" 9 "END"`
  if [ $a -eq 1 ]; then
    dialog --title "Title" --backtitle "Background Title" \
    --msgbox "First entry selected" 0 0
  elif [ $a -eq 2 ]; then
    dialog --title "Title" --backtitle "Background Title" \
    --msgbox "Second entry selected" 0 0
  elif [ $a -eq 9 ]; then
    dialog --title "Title" --backtitle "Background Title" \
    --no-label "End of program" --yes-label "Continue" \
      --yesno "End of program?" 0 0
    if [ $? -eq 1 ]; then
      break # or exit
    fi
  fi
done
dialog --title "Title" --backtitle "Background Title" \
  --msgbox "Saved values: \n $a \n $b \n $c " 0 0

Conclusion

If you want to enhance your shell scripts with a user-friendly interface, you'll find everything you need with the dialog tool.

If you require simple queries only, you can benefit from prefabricated building blocks. Otherwise, you can always use something other than shell code.