[ Team LiB ] Previous Section Next Section

The guestbook.cgi Script

The guestbook.cgi script computes a page that lists all the registered guests. Example 3-3 is shown first, and then each part of it is discussed in more detail later. One thing to note right away is that the HTML tags are generated by procedures that hide the details of the HTML syntax. The first lines of the script use the UNIX trick to have tclsh interpret the script. This is described on page 26:

Example 3-3 The guestbook.cgi script, version 1
#!/bin/sh
# guestbook.cgi
# Implement a simple guestbook page.
# The set of visitors is kept in a simple database.
# The newguest.cgi script will update the database.
# \
exec tclsh "$0" ${1+"$@"}

# The guestbook.data file has the database
# The datafile is in the same directory as the script

set dir [file dirname [info script]]
set datafile [file join $dir guestbook.data]

puts "text/html"
puts ""
set title "Brent's Guestbook"
puts "<HTML><HEAD><TITLE>$title</TITLE></HEAD>"
puts "<BODY BGCOLOR=white TEXT=black>"
puts "<H1>$title</H1>"

if {![file exists $datafile]} {
   puts "No registered guests, yet.
      <P>
      Be the first
      <A href='newguest.html'>registered guest!</A>"
} else {
   puts "The following folks have registered in my GuestBook.
      <P>
      <A href='newguest.html'>Register</A>
      <H2>Guests</H2>"
   catch {source $datafile}
   foreach name [lsort [array names Guestbook]] {
      set item $Guestbook($name)
      set homepage [lindex $item 0]
      set markup [lindex $item 1]
      puts "<H3><A href=$homepage>$name</A></H3>"
      puts $markup
   }
}
puts "</BODY></HTML>"

Using a Script Library File

If you write one CGI script, you are likely to write several. You could start making copies and modifying your first script, but that quickly becomes hard to maintain. If you learn something new after writing your third script, will you remember to update the first two scripts you wrote? Probably not. The best way to approach this problem is to create a collection of Tcl procedures in a file that you share among all your CGI scripts.

The Standard Tcl Library, tcllib, provides several packages of procedures that you can use. Later in this chapter, we will look at the ncgi package that helps handle form data. Before we do that, let's start a simple collection of our own procedures and learn how to share them among several different CGI scripts. Suppose you have a file cgihacks.tcl that contains your Tcl procedures. The source command loads that file into your script. The naive approach shown here probably won't work:

source cgihacks.tcl

graphics/common_icon.gif

Loading a file from the same directory as your script


The problem is that the current directory of the CGI process may not be the same as the directory that contains the CGI script or the cgihacks.tcl file. You can use the info script command to find out where the CGI script is, and from that load the supporting file. The file dirname and file join commands manipulate file names in a platform-independent way. They are described on page 108. I use the following trick to avoid putting absolute file names into my scripts, which would have to be changed if the program moves later:

set dir [file dirname [info script]]
source [file join $dir cgihacks.tcl]

You can also create script libraries as described in Chapter 12. That chapter describes tools to create an index of procedures so an application can quickly load the procedures it needs, and how to create packages of procedures so you can keep your code organized. However you set them up, it is always a good idea to have a library of procedures you share with other applications.

Beginning the HTML Page

The way you start your HTML page is a great candidate for capturing in a Tcl procedure. For example, I like to have the page title appear in the TITLE tag in the head, and repeated in an H1 tag at the beginning of the body. You may also have a favorite set of colors or fonts that you want to specify in the BODY tag. By putting all this into a Tcl procedure, you can make it easy to share this among all your scripts. If your tastes change tomorrow, then you can change the Tcl procedure in one spot and affect all CGI scripts that share the procedure. Example 3-4 shows Cgi_Header that generates a simple standard page header:

Example 3-4 The Cgi_Header procedure
proc Cgi_Header {title {body {bgcolor=white text=black}}} {
    puts stdout "Content-Type: text/html

<HTML>
<HEAD>
<TITLE>$title</TITLE>
</HEAD>
<BODY $body>
<H1>$title</H1>"
}

The Cgi_Header procedure takes as arguments the title for the page and some optional parameters for the HTML BODY tag. The procedure definition uses the syntax for an optional parameter, so you do not have to pass bodyparams to Cgi_Header. The default specifies black text on a white background to avoid the standard gray background of most browsers. Default values for procedure parameters are described on page 87.

Example 3-5 The guestbook.cgi script, version 2
#!/bin/sh
# guestbook.cgi
# Implement a simple guestbook page.
# The set of visitors is kept in a simple database.
# The newguest.cgi script will update the database.
# \
exec tclsh "$0" ${1+"$@"}

# The guestbook.data file has the database
# The datafile is in the same directory as the script

set dir [file dirname [info script]]
set datafile [file join $dir guestbook.data]

# Load our supporting Tcl procedures to define Cgi_Header

source [file join $dir cgihacks.tcl]
Cgi_Header "Brent's Guestbook"

if {![file exists $datafile]} {
   puts "No registered guests, yet.
      <P>
      Be the first
      <A href='newguest.html'>registered guest!</A>"
} else {
   puts "The following folks have registered in my GuestBook.
      <P>
      <A href='newguest.html'>Register</A>
      <h2>Guests</h2>"
   catch {source $datafile}
   foreach name [lsort [array names Guestbook]] {
      set item $Guestbook($name)
      set homepage [lindex $item 0]
      set markup [lindex $item 1]
      puts "<H3><A href=$homepage>$name</A></H3>"
      puts $markup
   }
}
puts "</BODY></HTML>"

Example 3-5 is a new version of the original CGI script that loads the cgihacks.tcl file and uses Cgi_Header. The Cgi_Header procedure just contains a single puts command that generates the standard boilerplate that appears at the beginning of the output. Note that several lines are grouped together with double quotes. Double quotes are used so that the variable references mixed into the HTML are substituted properly. The output of the Cgi_Header procedure matches what we wrote by hand in Example 3-3.

Sample Output of the CGI Script

The program tests to see whether there are any registered guests or not. The file command, which is described in detail on page 108, is used to see whether there is any data. The exclamation point means "not" in a boolean expression:

if {![file exists $datafile]} {

If the database file does not exist, a different page is displayed to encourage a registration. The page includes a hypertext link to a registration page, newguest.html, which is described on page 43. The output of the program would be as below in Example 3-6 if there were no data file:

Example 3-6 Initial output of guestbook.cgi with no data
Content-Type: text/html

<HTML>
<HEAD>
<TITLE>Brent's Guestbook</TITLE>
</HEAD>
<BODY BGCOLOR=white TEXT=black>
<H1>Brent's Guestbook</H1>
<P>
No registered guests.
   <P>
   Be the first
   <A HREF="newguest.html">registered guest!</A>
</BODY></HTML>

Note the inconsistent indentation of the HTML that comes from the indentation in the puts command used for that part of the page. The browser doesn't care about white space in the HTML. You have a choice between lining up the Tcl commands in your CGI script, or lining up the HTML output. Here we have two different examples. The Cgi_Header procedure produces output that is lined up, but the procedure definition looks a bit odd. The main script, in contrast, keeps its Tcl commands neatly indented, but that shows up in the output. If you generate most of your HTML from code, you may choose to keep your code tidy.

Example 3-7 shows the output of the guestbook.cgi script when there is some data in the data file:

Example 3-7 Output of guestbook.cgi with guestbook data
Content-Type: text/html

<HTML>
<HEAD>
<TITLE>Brent's Guestbook</TITLE>
</HEAD>
<BODY BGCOLOR=white TEXT=black>
<H1>Brent's Guestbook</H1>
<P>
The following folks have registered in my guestbook.
   <P>
   <A HREF='newguest.html'>Register</A>
   <H2>Guests</H2>
<H3><A HREF="http://www.beedub.com/">Brent Welch</A></H3>
<IMG SRC="http://www.beedub.com/welch.gif">
</BODY></HTML>

Using a Tcl Array for the Database

The data file contains Tcl commands that define an array that holds the guestbook data. If this file is kept in the same directory as the guestbook.cgi script, then you can compute its name:

set dir [file dirname [info script]]
set datafile [file join $dir guestbook.data]

By using Tcl commands to represent the data, we can load the data with the source command. The catch command is used to protect the script from a bad data file, which will show up as an error from the source command. Catching errors is described in detail on page 85:

catch {source $datafile}

The Guestbook variable is the array defined in guestbook.data. Array variables are the topic of Chapter 8. Each element of the array is defined with a Tcl command that looks like this:

set Guestbook(key) {url markup}

The person's name is the array index, or key. The value of the array element is a Tcl list with two elements: their URL and some additional HTML markup that they can include in the guestbook. Tcl lists are the topic of Chapter 5. The following example shows what the command looks like with real data:

set {Guestbook(Brent Welch)} {
    http://www.beedub.com/
    {<img src=http://www.beedub.com/welch.gif>}
}

The spaces in the name result in additional braces to group the whole variable name and each list element. This syntax is explained on page 96. Do not worry about it now. We will see on page 46 that all the braces in the previous statement are generated automatically. The main point is that the person's name is the key, and the value is a list with two elements.

The array names command returns all the indices, or keys, in the array, and the lsort command sorts these alphabetically. The foreach command loops over the sorted list, setting the loop variable x to each key in turn:

foreach name [lsort [array names Guestbook]] {

The lsort command will sort the names based on the person's first name. You can have lsort sort things in a variety of ways. One trick we can use here is to have lsort treat each key as a list and sort on the last item in the list (i.e., the last name):

foreach name [lsort -index end [array names Guestbook]] {

The lsort command is described in more detail on page 70. The foreach command assigns name to each key of the Guestbook array. We get the value like this:

set item $Guestbook($name)

The two list elements are extracted with lindex, which is described on page 68.

set homepage [lindex $item 0]
set markup [lindex $item 1]

We generate the HTML for the guestbook entry as a level-three header that contains a hypertext link to the guest's home page. We follow the link with any HTML markup text that the guest has supplied to embellish his or her entry:

puts "<H3><a href=$homepage>$name</a></H3>"
puts $markup

The homepage and markup variables are not strictly necessary, and the code could be written more compactly without them. However, the variables make the code more understandable. Here is what it looks like without the temporary variables:

puts "<H3><a href=[lindex $item 0]>$name</a></H3>"
puts [lindex $item 1]
    [ Team LiB ] Previous Section Next Section