Originally Published: Monday, 25 December 2000 Author: Mark Stone
Published to: featured_articles/Featured Articles Page: 1/1 - [Printable]

Tk: The Forgotten Language

These days there's a million programming languages, yet most everything seems to be either a C source or a Perl script. Here to remind us that the other languages are still useful is Mark Stone with an excellent script for switching network settings.

   Page 1 of 1  

I have a problem: I suffer from "Road Warrior's Syndrome." The symptoms of the Linux strain of this disorder include frantic grepping of configuration files, way too many DNS entries in the resolv.conf file, an inability to consistently get connected to the Net, and possession of a small piece of plastic from United Airlines that says "Premier Executive."

That's a polite way for United to say that I travel far too much, and I'm trying to get an Internet connection from too many different locations. At any given time I might have to connect from:

  • VA Linux Systems' headquarters office in Fremont (ethernet)
  • OSDN's headquarters office in Acton (ethernet)
  • The Lake Tahoe compound from which Trae McCombs runs Linux.com (ethernet)
  • My DSL line at home (ethernet)
  • Dial-up from home when DSL is down
  • Dial-up from Tahoe when Trae's T1 is down
  • Dial-up from some God-forsaken hotel at yet another trade show

Dial-up could be to either VA's local POP in the 650 area code or to their 800 number.

I don't even bother with a default network configuration at boot time for my laptop. I'm constantly having to remember different IP and gateway settings, and work off different configuration files for different dialup locations. My only consolation is that in Windows, I'd be even worse off, with every change of network configuration requiring yet another reboot (and don't get me started on the evils of DHCP; tough to ssh back to a box whose IP number you don't know).

What I need it is a nice little graphical application for handling network configuration, but one that's specific to my circumstances.

Every end user knows the response you get when voicing this sort of complaint in the Linux community: "This is Open Source; if what you're looking for doesn't exist, then code it yourself."

So I decided to do exactly that. Now, keep in mind that I'm more of a pointy-haired manager type than a programmer. I have no problem running

./configure
make
make install

on somebody else's code, but the last time I wrote code that required a compiler, people thought the VAX was a novel idea. What I need is rapid application development for graphical applications in a simple scripting language. If it takes more than 100 lines of code to do, I'm probably not doing it.

What Language?

GTK and Qt are not really options for me. My knowledge of C and C++ is limited, and I aim to keep it that way. Perl is serviceable enough, but lacks natural integration with a graphical environment. And although I hear good things about Python, I'm afraid all those whitespace rules would drive me crazy.

So forget all these new-fangled languages and toolkits. The application I've written could have been written in 1994 using the now venerable but perfectly serviceable Tcl/Tk. Tk may not be the trendiest graphical toolkit around, but it is sturdy, simple, and gets the job done.

Make a Wish

Tcl (Tool Command Language) and its associated graphical extension, Tk (Tool Kit), were created by then Berkeley professor John Ousterhout. Ousterhout was subsequently hired by Sun to work exclusively on Tcl/Tk development and in 1998 left Sun to start his own company, Scriptics (now part of Interwoven), dedicated to Tcl/Tk development, and providing value-added services on top of this Open Source language.

Tcl/Tk has been put to some remarkable uses. There's a very robust web server written in Tcl/Tk with enough extra features to make it a full web publishing system to rival Zope or PHP. There's a browser plug-in for running client-side Tcl/Tk much in the manner of Java applets. The fact that Tcl is an embeddable scripting language has made it very popular with those working on network managment tools.

I have a fondness for Tk, however, mainly for its simplicity. Useful utilities can be written quickly and in very few lines of code.

Tcl is a command-line scripting language that rivals Perl in its handling of regular expressions and the ease with which one can manage lists and arrays. Tcl is more procedural than object-oriented, though there are extensions like incr-Tcl that bring object-orientation to the language. Tk is an event-driven language that manages graphical objects known as widgets, and calls Tcl procedures on the back end to handle all the actual processing.

The usual invocation for a Tk application is to call Wish, the Tk interpreter. Wish creates widgets as requested by the script, and then enters an event loop. Typically the event loop endures until the last widget has been removed. User actions are trapped by the event loop, and typically used as triggers to call Tcl procedures that carry out user-requested actions.

Building an Application: Layout

The process for building a Tk application goes something like this:

  • Decide what actions need to be invoked on the backend
  • Decide what graphical front end is needed to invoke those actions
  • Build the graphical layout
  • Code the backend actions

We'll follow these steps.

If I'm bringing up an ethernet connection manually, then I'm using some variation of these two commands:

ifconfig eth0 [INSERT IP NUMBER] up netmask [INSERT NETMASK]
route add default gw [INSERT GW ADDRESS] eth0

If I'm making a dial-up connection, then my preference is to use wvdial. I will need a different wvdial.conf file depending on where I'm dialing to and from (actually wvdial supports a menu of options within a single configuration file, but I've just been too lazy to figure that part out).

So I need to either invoke ifconfig/route add with appropriate values, or wvdial with appropriate configuration.

On the front end, I'd like to be able to simply press a button for the settings that are appropriate, and then press a button to establish the network connection. In other words, I'd like a graphical utility that looks something like this:

In Tk, creating interface is a matter of first defining the widgets that make up this interface, and then actually drawing them on screen. The act of drawing widgets is called "packing" in Tk. The variety of widgets will be familiar to anyone who has done graphical programming, and actually should be familiar even if the limit of your graphical design experience is HTML.

This interface decomposes into the following widgets:

  • The root window. This is the container into which other widgets are packed, and it must exist before any widgets can be displayed.
  • Two frames. A frame is mostly a design convenience. Since widgets are laid out relationally, it is often helpful to group widgets that must be in a certain position relative to each other within a common frame.
  • Two labels. Each frame contains a label; this is the text visible as "Network Hosts:" and "Dial-up Locations:" respectively.
  • Two sets of radio buttons. All radio buttons in a set are mutually exclusive; only one can be selected at a time. This is the behavior we want (we have only one network connection at a time).
  • Two buttons. This are marked as "Start eth0" and "Start ppp" respectively. In our case these will function analogously to submit buttons in an HTML form.

Let's look at the whole code needed to create these, and then walk through it by command:

frame .netchoices -relief groove -borderwidth 3
label .netchoices.title -text "Network Hosts:"
radiobutton .netchoices.osdn -text "OSDN (Acton)" -variable netHost -value "osdn"
radiobutton .netchoices.dsl -text "DSL (from home)" -variable netHost -value "dsl"
radiobutton .netchoices.va -text "VA (Fremont)" -variable netHost -value "va"
button .netchoices.sub -text "Start eth0" -command {

  startNet $netHost
}
frame .pppchoices -relief groove -borderwidth 3
label .pppchoices.title -text "Dial-up Locations:"
radiobutton .pppchoices.h800 -text "800 (Hotel)" -variable dialUp -value "h800"
radiobutton .pppchoices.d800 -text "800 (Direct)" -variable dialUp -value "d800"
radiobutton .pppchoices.h650 -text "650 (Hotel)" -variable dialUp -value "h650"
radiobutton .pppchoices.d650 -text "650 (Direct)" -variable dialUp -value "d650"
button .pppchoices.sub -text "Start ppp" -command {   startPpp $dialUp
}

Tk commands used for creating widgets generally take the form:
[widget type] [widget name] [widget options]

The root window has the name "." and widget names follow a dot-separated hierarchy where all other widgets must be below the root window. So in the command:

frame .netchoices -relief groove -borderwidth 3

we are creating a widget of type frame called netchoices that is a "child" of the root window. We've invoked two options with this widget. We've elected to give it a grooved border with a width of three. Other relief options would have been raised, sunken, flat, ridge, or solid.

We want to have a name for this frame displayed to the user:

label .netchoices.title -text "Network Hosts:"

We created a widget of type label, named title, that is a child of netchoices, and we've designated a text string to be displayed in that label. Something we haven't done, but could have, would be to give the label a state, which can be either normal, active, or disabled. This would give us a means of having labels behave visually somewhat like radio buttons. In our case this could actually be useful. A user can only be connected by dial-up or ethernet, not both (at least on my laptop). A more sophisticated version of this script might gray out the "Network Hosts" label as inactive when a dial-up radio button has been selected, and gray out the "Dial-up Locations:" label as inactive when an ethernet radio button has been selected.

We next create three radio buttons for three different possible locations where I can connect via ethernet:

radiobutton .netchoices.osdn -text "OSDN (Acton)" -variable netHost -value "osdn"
radiobutton .netchoices.dsl -text "DSL (from home)" -variable netHost -value "dsl"
radiobutton .netchoices.va -text "VA (Fremont)" -variable netHost -value "va"

Note that all three radio buttons are assigned a common variable name, namely netHost. To Tk, that's what identifies this as a family of radio buttons where selection between them is mutually exclusive. Note also that we've picked a value to be assigned to that variable, depending on which button was selected. You'll see in the next command how we make use of that information.

Our first opportunity to do some real work in this program comes with the next widget:

button .netchoices.sub -text "Start eth0" -command {

  startNet $netHost
}

The cool thing about the button widget is its support for the command option. In other words, you can designate a command, or command block to be executed any time that the button is depressed. This is what we mean by event-driven programming. The event loop waits for an appropriate user event (depressing a button), which can then initiate a sequence of action. Command blocks in Tcl/Tk, by the way, are delineated in the usual manner, with braces.

We could, of course, stuff the actual code we want executed in at this point. But that would not be a good, modular design. Tcl/Tk provides for procedures, and we're going to make use of them. We're going to have a procedure called startNet, and we're going to pass to it the value of the variable netHost.

The widget definitions for the second frame are exactly parallel to the first. We have a different variable, namely dialUp, and we have different values for it, and in the end we'll invoke a different procedure, called startPpp.

frame .pppchoices -relief groove -borderwidth 3
label .pppchoices.title -text "Dial-up Locations:"
radiobutton .pppchoices.h800 -text "800 (Hotel)" -variable dialUp -value "h800"
radiobutton .pppchoices.d800 -text "800 (Direct)" -variable dialUp -value "d800"
radiobutton .pppchoices.h650 -text "650 (Hotel)" -variable dialUp -value "h650"
radiobutton .pppchoices.d650 -text "650 (Direct)" -variable dialUp -value "d650"
button .pppchoices.sub -text "Start ppp" -command {

  startPpp $dialUp
}

What we've done here is pretty simple. We've established that we'll have two procedures for handling two different types of connections, and we have an identifying string that can be passed to each procedure by selecting a radio button and then pressing a "Start" button.

Of course none of this widget creation does us any good until we've actually displayed widgets for the user to interact with. To accomplish this, we'll use the pack command:

pack .netchoices .pppchoices -side top -fill x
pack .netchoices.title .netchoices.osdn .netchoices.dsl .netchoices.va .netchoices.sub -side top -padx 10 -pady 2
pack .pppchoices.title .pppchoices.h800 .pppchoices.d800 .pppchoices.h650 .pppchoices.d650 .pppchoices.sub -side top -padx 10 -pady 2

What we've done here is use three commands that follow our own hierarchy. First we've packed the two frames inside the root window. Then we've packed the buttons inside each frame. Packing order is very important. Tk packs sequentially, and relationally. In other words, it packs widgets one at a time in the order presented, and it packs each widget relative to the one before it.

The pack command makes use of a range of options that give you some control over this relational geometry. Good packing is a fine art. You want a layout that makes sense to you and the user, and one that will survive resizing of the root window by the user.

The pack command has many options, but the ones we've used here are pretty standard. The side option designates where to pack from. We've used top for the two frames; had we used bottom their order would have been reversed. You can also use left and right.

Note that we haven't indicated an exact size for any widgets. Exact sizes should be avoided as they tend to break on resizing. Normally a widget occupies the minimum space needed to display the required elements. What we have done is to add the fill option on the x axis. This means that if either frame widget occupies less width than the root window, it should expand to fill the extra width.

On the radio buttons, we've added a little spacing using the padx and pady options to make for a neater appearance. While the syntax is different, the concept should be familiar to anyone who has worked with HTML tables.

With the pack commands, we now have a complete layout. All that remains is to code the back end procedures to this script. It's important to realize that at this stage we don't have to know anything about how the procedures are actually going to work. That means we're free to play around with the layout until we have a layout we're happy with. In fact, the following is a valid Tk script:

#!/usr/bin/wish

proc startNet {netHost} {
}

proc startPpp {dialFrom} {
}

wm title . "Network Configuration"
frame .netchoices -relief groove -borderwidth 3
label .netchoices.title -text "Network Hosts:"
radiobutton .netchoices.osdn -text "OSDN (Acton)" -variable netHost -value "osdn"
radiobutton .netchoices.dsl -text "DSL (from home)" -variable netHost -value "dsl"
radiobutton .netchoices.va -text "VA (Fremont)" -variable netHost -value "va"
button .netchoices.sub -text "Start eth0" -command {

  startNet $netHost
}
frame .pppchoices -relief groove -borderwidth 3
label .pppchoices.title -text "Dial-up Locations:"
radiobutton .pppchoices.h800 -text "800 (Hotel)" -variable dialUp -value "h800"
radiobutton .pppchoices.d800 -text "800 (Direct)" -variable dialUp -value "d800"
radiobutton .pppchoices.h650 -text "650 (Hotel)" -variable dialUp -value "h650"
radiobutton .pppchoices.d650 -text "650 (Direct)" -variable dialUp -value "d650"
button .pppchoices.sub -text "Start ppp" -command {   startPpp $dialUp
}


pack .netchoices .pppchoices -side top -fill x
pack .netchoices.title .netchoices.osdn .netchoices.dsl .netchoices.va .netchoices.sub -side top -padx 10 -pady 2
pack .pppchoices.title .pppchoices.h800 .pppchoices.d800 .pppchoices.h650 .pppchoices.d650 .pppchoices.sub -side top -padx 10 -pady 2

Designing scripts in this way -- full layout commands, but procedures that are empty shells -- is a valuable design approach in Tk. By doing this, you can actually run the program, test your layout, and tweak it until you're satisfied with the results before you focus on the actual work the script is intended to do.

Building an Application: Procedures

So let's look at how we use this script to bring up an ethernet connection. This work gets done by the procedure startNet, which gets passed one parameter: a text string with a value of either "osdn", "dsl", or "va".

Tcl is a full featured scripting language with all of the control structures and data structures one would expect. We have relatively little control flow to worry about in this script; mainly the script is a front end to bash shell commands. Let's look at the whole startNet procedure, and then walk through it:

proc startNet {netHost} {

 switch $netHost {
  osdn { set netList [osdnNet] }  dsl { set netList [dslNet] }  va { set netList [vaNet] }
 } set ipNum [lindex $netList 0] set netM [lindex $netList 1] set gateW [lindex $netList 2]  exec ifconfig eth0 $ipNum up netmask $netM exec route add default gw $gateW eth0
}

Our goal here is simple: we need correct settings for IP number, netmask, and gateway for each of three connection locations. Once we have those values, we'll insert them into the ifconfig and route commands to bring up an ethernet connection.

A word about Tcl variable notation and variable assignments. In Tcl you refer to a variable directly simply by using the variable name. You refer to the value assigned to that variable by preceding the variable name with a "$". The command to assign a value to a variable is the set command, which has the format "set variable value". You can send the value of a variable to standard output using the puts command. In other words:

set foo bar
puts $foo

would print the string "bar" to standard output.

Tcl has a very sophisticated set of commands for manipulating lists, so lists are the fundamental data structure in Tcl. Once you become adept at handling lists, you'll find you seldom need to use arrays.

Finally, note that command execution can be nested within a Tcl command line, using brackets for delineation; commands nested within the innermost brackets are executed first.

Now let's look at what we've done. Our first command is the switch command:

switch $netHost {

  osdn { set netList [osdnNet] }   dsl { set netList [dslNet] }   va { set netList [vaNet] }
}

.Switch is a convenience that's equivalent to a series of "if... then... elseif" statements. The switch command says to check a value, $netHost in our case, and to execute a command sequence flagged by a string match to that value. In other words, if the value of netHost is "osdn", execute the command sequence following the string "osdn", etc.

The command sequence, in our case, is just an instance of the set command. We're going to assign a value to the netList variable. As the variable name implies, the value assigned to netList is a list, in our case a three element list consisting of an IP number, an netmask value, and a gateway address. We'll get this list by another procedure call.

In other words, when we execute the command:

set netList [osdnNet]

We are calling a procedure named osdnNet, passing no parameters to it, and asking it to return a list that will be assigned to netList.

The three procedures osdnNet, dslNet, and vaNet couldn't be simpler. Here they are:

proc osdnNet {} {

 set e0 "192.168.254.200"  set nm "255.255.255.0"  set gw "192.168.254.1"  lappend nList $e0 $nm $gw  return $nList
}

proc dslNet {} {

 set e0 "192.168.1.124"  set nm "192.168.255.0"  set gw "192.168.1.1"  lappend nList $e0 $nm $gw  return $nList }

proc vaNet {} {

 set e0 "192.168.4.209"  set nm "255.255.255.0"  set gw "192.168.4.1"  lappend nList $e0 $nm $gw  return $nList }

The lappend command stands for list append. It takes a list and elements to the end of the list. If the list does not already exist, then it creates a list with the indicated elements. So the value of nList will look something like this:

{192.168.254.200 255.255.255.0 192.168.254.}

Technically, this use of procedures violates good coding practice. You should only use a procedure for code that will be invoked more than once in a program, or that could be called from more than one place in the program. The proper way to set this up would be to store this information in a file rather than in a procedure, and then read from the file.

Proper, yes. But this approach was quick and easy, and is still simple to read, update and modify. It would be trivial to add a fourth network location to this script.

Once our list has been constructed, the actual remaining program exeuction is almost anticlimactic:

set ipNum [lindex $netList 0]
set netM [lindex $netList 1]
set gateW [lindex $netList 2]
exec ifconfig eth0 $ipNum up netmask $netM
exec route add default gw $gateW eth0

Here we take the values from our list and pass them on to ifconfig, which brings up the ethernet interface with the correct IP number and netmask, and pass them to route, which adds eth0 as a route with the proper gateway assignment. Note that the commands are set up this way for purely cosmetic reasons. It would be just as effective, but less readable, to simply use the following two commands:

exec ifconfig eth0 [lindex $netList 0] up netmask [lindex $netList 1]
exec route add default [lindex $netList 2] eth0

The lindex command is used to extract one element from a list. List numbering begins with 0, hence "lindex $netList 0" returns the first element of netList.

The exec (stands for execute) command is what makes Tcl a real system administrator's friend. I've actually never learned how to do bash shell scripting, because it's just as easy for me to set up all the control structures in Tcl and then call any needed shell commands with exec.

When making a dial-up connection, we call the startPpp procedure. This is an even simpler procedure. I need a few different variations on my wvdial.conf file for these reasons: sometimes I'm calling an 800 number to connect; sometimes I'm calling a number in my local 650 area code. I may also be calling from a hotel, in which case I need to precede the phone number with a "9." You'd think I'd only need the 800 number for hotel connections, but in my experience when calling from hotels with older phone systems, I've had better luck making the connection to the 650 number; I have no idea why.

I have four different wvdial.conf files for these four scenarios. All that startPpp has to do is copy the correct file into the correct place, and then call wvdial. Here's the procedure:

proc startPpp {dialFrom} {

 switch $dialFrom {
  h800 { exec cp /etc/ldh.wvdial.conf /etc/wvdial.conf }   d800 { exec cp /etc/ldd.wvdial.conf /etc/wvdial.conf }   h650 { exec cp /etc/pah.wvdial.conf /etc/wvdial.conf }   d650 { exec cp /etc/pad.wvdial.conf /etc/wvdial.conf }
 } exec wvdial

}

Conclusion

That's all there is to it. We've built a complete graphical utility for making a network connection. It's one that's specific to my particular network configuration and my particular situation, but that's what scripting is all about: rolling your own solution when a canned solution isn't good enough.

What's attractive about Tk is the simplicity with which this kind of graphical programming can be done. This entire script involves only 64 lines of code:

#!/usr/bin/wish

proc osdnNet {} {

 set e0 "192.168.254.200"  set nm "255.255.255.0"  set gw "192.168.254.1"  lappend nList $e0 $nm $gw  return $nList
}

proc dslNet {} {

 set e0 "192.168.1.124"  set nm "192.168.255.0"  set gw "192.168.1.1"  lappend nList $e0 $nm $gw  return $nList
}

proc vaNet {} {

 set e0 "192.168.4.209"  set nm "255.255.255.0"  set gw "192.168.4.1"  lappend nList $e0 $nm $gw  return $nList
}

proc startNet {netHost} {

 switch $netHost {
  osdn { set netList [osdnNet] }  dsl { set netList [dslNet] }  va { set netList [vaNet] }
 } set ipNum [lindex $netList 0] set netM [lindex $netList 1] set gateW [lindex $netList 2]  exec ifconfig eth0 $ipNum up netmask $netM exec route add default gw $gateW eth0
}

proc startPpp {dialFrom} {

 switch $dialFrom {
  h800 { exec cp /etc/ldh.wvdial.conf /etc/wvdial.conf }  d800 { exec cp /etc/ldd.wvdial.conf /etc/wvdial.conf }  h650 { exec cp /etc/pah.wvdial.conf /etc/wvdial.conf }  d650 { exec cp /etc/pad.wvdial.conf /etc/wvdial.conf }
 } exec wvdial
}

wm title . "Network Configuration"
frame .netchoices -relief groove -borderwidth 3
label .netchoices.title -text "Network Hosts:"
radiobutton .netchoices.osdn -text "OSDN (Acton)" -variable netHost -value "osdn"
radiobutton .netchoices.dsl -text "DSL (from home)" -variable netHost -value "dsl"
radiobutton .netchoices.va -text "VA (Fremont)" -variable netHost -value "va"
button .netchoices.sub -text "Start eth0" -command {

  startNet $netHost
}
frame .pppchoices -relief groove -borderwidth 3
label .pppchoices.title -text "Dial-up Locations:"
radiobutton .pppchoices.h800 -text "800 (Hotel)" -variable dialUp -value "h800"
radiobutton .pppchoices.d800 -text "800 (Direct)" -variable dialUp -value "d800"
radiobutton .pppchoices.h650 -text "650 (Hotel)" -variable dialUp -value "h650"
radiobutton .pppchoices.d650 -text "650 (Direct)" -variable dialUp -value "d650"
button .pppchoices.sub -text "Start ppp" -command {   startPpp $dialUp
}

pack .netchoices .pppchoices -side top -fill x
pack .netchoices.title .netchoices.osdn .netchoices.dsl .netchoices.va .netchoices.sub -side top -padx 10 -pady 2
pack .pppchoices.title .pppchoices.h800 .pppchoices.d800 .pppchoices.h650 .pppchoices.d650 .pppchoices.sub -side top -padx 10 -pady 2

Tcl/Tk has never achieved the popularity, or at least the visibility, of other scripting languages like Perl. I'm not sure why that is. It's an easy language to learn, has all the power and sophistication one could want, and it's also cross-platform: many Tcl/Tk scripts will run without modification on all flavors of Unix, all 32-bit flavors of Windows, and MacOS.

Tcl/Tk is getting to be one of the older scripting languages now, but it's still a good one. Anyone interested in graphical applications should think about dusting off this jewel and seeing if they can make it shine.

Mark Stone is Director of Developer Services for OSDN. He wrote this script on a flight from San Francisco to Boston. If OSDN insists that he maintain this kind of travel schedule, they might make a programmer out of him yet.





   Page 1 of 1