Wednesday 21 August 2013

Creating great graphical applications with shell scripts

Summary:  The command line isn't suitable for every user. Indeed, some users may only feel comfortable when armed with a mouse. To accommodate those users or build custom desktop applications using only the shell, add GUIs to your scripts. Here's how you can do it.


If you walk into any crowded machine room, chances are you'll catch chit-chat about "shebangs," slashes, dot-dots, root, pipes, ports, and dash-dash this and that. If you speak UNIX®, you'll no doubt grok the local lingo—acronyms, command names, shortcuts, options, file names, and colloquialisms about UNIX—and feel right at home. Like practitioners of other art, UNIX users have an extensive vernacular for describing the specifics of their work.

Frequently used acronyms

  • GUI: Graphical user interface
  • HTML: Hypertext Markup Language
But not everyone speaks UNIX; in fact, some may find the command line daunting or perplexing. Further, you simply may not want to entrust the entirety of the command line to an occasional or inexperienced user. To assist those unaccustomed to the command line or to build custom solutions around the shell, you can build GUIs for your scripts. With such tools—dialog and Zenity are two worthy of mention (see Resources)—you can use dialog boxes, file browsers, and other common "windowing" controls and techniques to interact with your users. Indeed, dialog boxes provide for more natural conversations: You present information, ask for a response, and react accordingly.
This installment of "Speaking UNIX" looks at dialog and Zenity and shows how you can turn any script into a convincing GUI application. You use dialog with traditional, text-based interfaces; Zenity proffers the style of the modern, windowed desktop.
Add dialog boxes to any shell script
A command-line utility typically offers sufficient options to completely control each invocation. Some switches may enable or disable a feature, while other switches may process arguments, such as a list of names. On the command line, you present (nearly) all the information up front and let the job go. Graphical applications are very different. Choices are made through menus, check boxes, and file browsers. A graphical application takes in a little information, processes it, and then usually asks for more information. It's said that GUI applications are event driven.
The dialog utility spans the two worlds. You invoke the utility whenever you need input from the user, and then return to your script to continue processing whatever data was provided. In other words, if you write a script to use dialog, you'll likely ignore command-line arguments and instead use dialog to prompt for information when necessary.
If your system lacks the dialog utility, you can easily install it with your distribution's own package manager, or you can build it directly from source. For example, if your system uses Aptitude, you can install dialog with the command:

sudo apt-get install dialog


Otherwise, to build from source, download the code from maintainer Thomas Dickey's Web site (see Resources) and run the typical trio of commands: ./configure && make && make install:

$ wget http://invisible-island.net/datafiles/release/dialog.tar.gz
$ tar xzf dialog.tar.gz
$ cd dialog-1.1-20100428
$ ./configure
$ make
$ sudo make install


After the installation, you should have a new utility named dialog in your path. Type man dialog to see the bundled documentation.
Using dialog is simple: It's just another UNIX command. You display a dialog box of your choice using the command's options, then capture the result and perform some logic based on that value. Some variations of dialog place the result of the command directly in the special shell status variable, $?, which you should save or interrogate immediately after the dialog command exits (because a subsequent command will immediately change its value). Other, typically more complicated variations of thedialog command both set the shell status variable and generate other results. To make things simple, dialog provides the --stdout option to always emit its result to standard output, making it easy to capture data with command evaluation (a combination of a command in back quotes and an assignment statement).
For example, the command dialog --yesno is one of the simplest variants. It presents a question, prompts for either a yes or no response, and returns either 0 or 1 in $? depending on whether the user selected "Yes" or "No," respectively. You can test the value of $? and execute some conditional code. Here's is a working snippet you can add to a shell script:

dialog --yesno "Do you want to continue?" 0 0
rc=$?
if [ "${rc}" == "0" ]; then
  echo Yes
else
  echo No
fi


The --yesno option requires at least three arguments: text for the question and the height and width of the dialog box itself, measured in rows and columns. If you don't require specific dimensions, you can always use 0 for either height or width to size the dialog box automatically. (There are also options for placing the window relative to the top left corner of the window.) Figure 1 shows --yesno in operation.

Figure 1. The --yesno operation
The --yesno operation 
The dialog option --calendar presents a calendar to allow the user to choose a specific date. If the user chooses a date, and then clicks OK, the command returns 0. If, however, the user clicks Cancel, the command returns 1. Moreover, if the user clicksOK, the command emits the date selected to standard output. Here's an example using command evaluation to yield a date:

RESULT=`dialog --stdout --title "CALENDAR" 
    --calendar "Please choose a date..." 0 0 9 1 2010`
retval=$?


The --title option uses the next argument to add a title to the dialog box and can be used with any dialog command. Much like --yesno, you provide some text to prompt the user. Next, the options 0 0 again specify automatic height and width, and the options 9 1 2010 dictate the initial day, month, and year, respectively, shown in the calendar. The Tab and arrow keys alter the calendar and choose a date. After the dialog box is dismissed, if retval is 0, the value of RESULT is the date selected. Figure 2 shows the calendar dialog box.

Figure 2. The calendar dialog box
The calendar dialog box 
The dialog command offers most of the controls typically found in a graphical application:
  • --infobox simply presents information: It does not expect any input. The information box remains on screen only briefly. To prolong its display, place a sleep command between it and the next command.
  • --input collects a single, typed response. You might use this command to collect your user's name or zip code.
  • --textbox displays the contents of a text file. If the file exceeds the vertical height of the dialog box, a control allows for simple scrolling up and down.
  • --menu and --radiolist present a list of choices and allow the user to select one. The two kinds of dialog box are functionally equivalent but have slightly different visual styles to better simulate what a GUI might present. Specifically, the--radiolist command renders ( ) to mimic radio buttons.
  • --checklist displays a list of items that the user can enabled or disabled individually.
The output of each dialog variant differs but is either a single value or a list of quoted values separated by white space. For instance, --checklist, which is great for choosing one or more options, emits a list of quoted values, where each value is associated with an enabled option. An example demonstrates the operation:

RESULT=`dialog --stdout 
   --checklist "Enable the account options you want:" 10 40 3 \
  1 "Home directory" on \
  2 "Signature file" off \
  3 "Simple password" off`


The backslash (\) at the end of lines 1, 2, and 3 are continuations; everything from RESULT to off` is one command. If the user enabled Home directory and Simple password$RESULT would be "1" "3". The arguments to --checklist are the height and width, the number of list elements at any time (you can scroll to see additional items if some are occluded), and the checklist options, where each option is a value, a description, and whether the option is initially enabled or disabled.
You can type dialog --help at any time to see the list of general and dialog-specific options. There are tons of uses fordialog.
Got pixels? Use Zenity.
Zenity is to the UNIX desktop what dialog is to simple terminal windows. You can use Zenity to open GTK+ dialog boxes from any shell script. In fact, Zenity shares many of the same features as dialog; the only difference is that Zenity works in an X Window System environment. Zenity comes bundled with GNOME. If you don't run GNOME, you can install Zenity separately (however, expect a large number of GTK+ libraries to be installed, too). You can also download the source of Zenity from the GNOME project pages (see Resources for a link).
Here's a quick example. The command:

zenity --question --text "Do you want to continue?"


produces something like Figure 3. (The machine used for demonstration is running Ubuntu 10.) If you click OK, the command returns 0. Otherwise, it returns 1.

Figure 3. A simple question
A simple question 
Like dialog, Zenity has a good number of options—perhaps even more than dialog—but the options are well named and thus self-explanatory. You'll likely find Zenity more advantageous than dialog, especially as most computer users have an X desktop of some sort.
Zenity offers many of the same controls as dialog. Here is a snippet to collect a name:

ENTRY=`zenity --entry --text "Please enter your name" 
   --entry-text "Your name" --title "Enter your name"
if [ $? == 0 ]; then
  zenity --info --text "Hello $ENTRY\!"
fi


Again, if the exit code of zenity is 0, then ENTRY has the person's name. Here is the calendar example from above rewritten to use Zenity:

DATE=`zenity --calendar --day "9" --month "1" --year "2010" --format "%Y-%m-%d"
if [ $? == 0 ]; then
  echo $DATE
fi


Although Zenity is a little more verbose—there are separate options for day, month, and year, for example—the additional switches free you from remembering the precise usage sequence of arguments. Zenity's calendar also allows you to specify the format for output, using standard strftime() codes. The result of this command would be something like 2010-1-9 for 9 January 2010.
Zenity also provides a progress meter to show the state of an operation. It reads data from standard input line by line. If a line is prefixed with the octothorpe, or pound sign (#), the text is updated with the text on that line. If a line contains only a number, the percentage is updated with that number. Listing 1 shows is an example from the Zenity documentation.

Listing 1. The Zenity progress meter

 
#!/bin/sh
(
  echo "10" ; sleep 1
  echo "# Updating mail logs" ; sleep 1
  echo "20" ; sleep 1
  echo "# Resetting cron jobs" ; sleep 1
  echo "50" ; sleep 1
  echo "This line will just be ignored" ; sleep 1
  echo "75" ; sleep 1
  echo "# Rebooting system" ; sleep 1
  echo "100" ; sleep 1
) |
zenity --progress \
  --title="Update System Logs" \
  --text="Scanning mail logs..." \
  --percentage=0

if [ "$?" = -1 ] ; then
  zenity --error \
    --text="Update canceled."
fi


The sub-shell (wrapped in parentheses) performs a series of tasks—albeit sleep delays in this contrived example—and emits output to a Zenity progress meter via a pipe. Before each step, the sub-shell emits a number to advance the progress meter, which starts at 0 per --percentage 0, and then emits a string prefaced with # to change the status message. Thus, the progress meter steps along to mark the work of the script. If Zenity exits with code -1, the Cancel button was clicked.
Again, to use dialog or Zenity, replace code where you previously referenced a command-line argument with a dialog box. With a little creativity, you can turn your shell scripts into first-class desktop citizens.
Additional advanced tools
At some point, you may find that your requirements exceed the capabilities of both shell scripting and the dialog and Zenity tools. In those instances, you may turn to C/C++ and build native applications for the desktop, but you can also turn to advanced scripting languages and language bindings for any number of robust GUI frameworks.
One combination is the Ruby scripting language and the Ruby bindings for the wxWidgets framework. Ruby is object oriented, expressive, concise, and runs on most operating systems. The wxWidgets framework is also available on every major platform, including Mac OS X, Windows®, Linux®, and UNIX. Because both are portable, you can write an application once in Ruby and run it everywhere. Another, simpler option is Shoes. Although not as rich as wxWidgets, Shoes is fairly quick to learn and use. The code in Listing 2 realizes a calculator in 70 lines of code.

Listing 2. A calculator in Shoes

 
class Calc
  def initialize
    @number = 0
    @previous = nil
    @op = nil
  end

  def to_s
    @number.to_s
  end

  (0..9).each do |n|
    define_method "press_#{n}" do
      @number = @number.to_i * 10 + n
    end
  end

  def press_clear
    @number = 0
  end

  {'add' => '+', 'sub' => '-', 'times' => '*', 'div' => '/'}.each do |meth, op|
    define_method "press_#{meth}" do
      if @op
        press_equals
      end
      @op = op
      @previous, @number = @number, nil
    end
  end

  def press_equals
    @number = @previous.send(@op, @number.to_i)
    @op = nil
  end
end

number_field = nil
number = Calc.new
Shoes.app :height => 250, :width => 200, :resizable => false do
  background "#EEC".."#996", :curve => 5, :margin => 2

  stack :margin => 2 do

    stack :margin => 8 do
      number_field = para strong(number)
    end

    flow :width => 218, :margin => 4 do
      %w(7 8 9 / 4 5 6 * 1 2 3 - 0 Clr = +).each do |btn|
        button btn, :width => 46, :height => 46 do
          method = case btn
            when /[0-9]/; 'press_'+btn
            when 'Clr'; 'press_clear'
            when '='; 'press_equals'
            when '+'; 'press_add'
            when '-'; 'press_sub'
            when '*'; 'press_times'
            when '/'; 'press_div'
          end

          number.send(method)
          number_field.replace strong(number)
        end
      end
    end
  end
end


An introduction to Ruby and Shoes is beyond the scope of this article, but here are some of the most important constructs:
  • The bulk of the Ruby class Calc uses Ruby's metaprogramming features to define functions at run time for all the digit keys and for the math operation keys.
  • The code beginning Shoes.app... creates the GUI for the calculator, rendering the layout and the buttons for it. Shoes provides two containers to assemble layouts: the stack and the flow. A stack is a vertical stack of elements, with each element placed directly beneath the element preceding it. A flow packs elements in as tightly as it can until it reaches the limits of its bounding box, and then wraps the remaining elements. (You can think of a stack as an HTML <div> and a flow as an HTML <p>.) You create a stack or flow using a Ruby block.
  • The innermost flow block loops, creating all the buttons in the application and effectively binding each button to its method. (The case statement returns a method name; the line number.send(method) calls that method on the instantiated calculator.)
  • The line number_field.replace strong(number) updates the calculator display with the result of the most recent calculation. Emitting number causes the class to call its own to_s ("to string") method.
Other scripting languages have similar libraries, and there are many more choices for Ruby itself, including Ruby Cocoa to develop Cocoa applications on Mac OS X with Ruby. Pick your favorite open source scripting language, find a lightweight GUI toolkit, and start coding.
We don't need no stinkin' compiler!
If you've already mastered shell scripting, combine your work with dialog or Zenity to add interactivity. And once you need more programming power than what the shell provides, consider a language such as Ruby or Python and any one of several windowing toolkits. You don't need a compiler to write great desktop applications.

Resources
Learn
  • Speaking UNIX: Check out other parts in this series.
  • wxWidgets: Read more about wxWidgets, the cross-platform GUI framework.
  • AIX and UNIX developerWorks zone: The AIX and UNIX zone provides a wealth of information relating to all aspects of AIX systems administration and expanding your UNIX skills.
  • New to AIX and UNIX? Visit the New to AIX and UNIX page to learn more.
  • Technology bookstore: Browse the technology bookstore for books on this and other technical topics.
Get products and technologies
  • dialog project page: Download the source code for dialog.
  • Zenity: Get the source code for Zenity from the GNOME project pages.
  • Shoes: Learn how to script applications with Shoes, a GUI library for Ruby

0 blogger-disqus:

Post a Comment