Chapter 20. Extended Examples

Examples are an essential component in learning how to program. The explanations in this book are littered with examples. And while many are complete programs, most of them are small.

In contrast, this chapter is composed of several extended examples. Each is a complete Expect script drawing together many different concepts described in other chapters.

Encrypting A Directory

The UNIX crypt command encrypts a single file. Because it interactively prompts for a password, crypt is a pain to use if you want to encrypt a number of files all at the same time.

The cryptdir script, shown here, encrypts all the files in a directory. The current directory is used unless an argument is given, in which case that is used instead. If the script is called as decryptdir, the files are decrypted. Here is the beginning where the script figures out what it should do based on its name and arguments.

#!/usr/local/bin/expect —

# encrypt/decrypt an entire directory
# optional arg is dirname, else cwd

if {[llength $argv] > 0} {
    cd $argv
}

# encrypt or decrypt?
set decrypt [regexp "decrypt" $argv0]

Next, the script queries for a password. If the script is encrypting files, it asks for the password twice. This lowers the chance of encrypting files with an accidentally mistyped password.

set timeout −1
stty -echo
send "Password:"
expect -re "(.*)
"
send "
"
set passwd $expect_out(1,string)

# wouldn't want to encrypt files with mistyped password!
if !$decrypt {
    send "Again:"
    expect -re "(.*)
"
    send "
"
    if ![string match $passwd $expect_out(1,string)] {
        send_user "mistyped password?"
        stty echo
        exit
    }
}
stty echo

Once the password is known, the script loops through the list of files encrypting (or decrypting) each one. The suffix .crypt is used to store the encrypted version. Not only is this helpful to the user, but the script also uses this convention to avoid encrypting files that have already been encrypted.

log_user 0
foreach f [glob *] {
    set strcmp [string compare .crypt [file extension $f]]
    if $decrypt {
        # skip files that don't end with ".crypt"
        if 0!=$strcmp continue
        spawn sh -c "exec crypt < $f > [file root $f]"
    } else {
        # skip files that already end with ".crypt"
        if 0==$strcmp continue
        spawn sh -c "exec crypt < $f > $f.crypt"
    }
    expect "key:"
    send "$passwd
"
    expect
    wait
    exec rm -f $f
    send_tty "."
}
send_tty "
"

File Transfer Over telnet

The ftp program is handy for transferring files but it only works if the remote host is directly reachable via TCP. Suppose you have to telnet to a modem pool and then dial out to another modem to reach the remote host. Not only can ftp not handle this but neither can a lot of other communications programs. In Chapter 16 (p. 346), I presented a file transfer script that used rz and sz. Like many other communications programs, rz and sz require binary copies of their counterpart at each end of the link. If you do not have both, copying one to the other end can be a problem—if it was easy, you would not need the programs in the first place! Even worse, many versions of telnet and other programs do not provide 8-bit clean connections. So even if you had rz and sz, you might not be able to use them over a telnet connection.

The script below works over many kinds of links and does not require a copy of itself on the other end. The only assumptions made are that the usual UNIX utilities (such as cat and compress) exist and that the line is error free. If you do not have compress, that can be removed from the script as well. It is used only to speed the transfers.

The script is quite a bit fancier than the rz/sz script. This one interactively prompts for file names and other commands.

The script starts off by finding out what the prompt looks like. It then disables the default timeout. The script has a verbose mode so that the user can see what is happening internally. By default this mode is disabled.

#!/usr/local/bin/expect —

if [info exists env(EXPECT_PROMPT)] {
    set prompt $env(EXPECT_PROMPT)
} else {
    set prompt "(%|#|\$) $"     ;# default prompt
}

set timeout −1
set verbose_flag 0

As is usually the case, procedures are defined before they are used. Describing the procedures in that order is hard to understand. Instead, I will present the rest of the script out of order.

The final piece of code in the script starts a shell, tells the user about the commands, and then gives control to the user in an interact.

spawn -noecho $env(SHELL)

send_user "Once logged in, cd to directory to transfer
            to/from and press: ~~
"
send_user "One moment...
"
interact ~~ cmd

At this point, the user connects to the remote system using external programs such as telnet and tip. Once in the remote directory where transfers should take place, the user invokes the cmd procedure by entering "~~“. A special prompt appears and a single additional character selects the specific action that should take place such as p for “put file” and g for “get file”. The user can enter another ~ to send a literal tilde, ^Z to suspend the process, ? for help, or c to change directory on the local system.

proc cmd {} {
    set CTRLZ 32

    send_user "command (g,p,? for more): "
    expect_user {
        g get_main
        p put_main
        c chdir
        v verbose
        ~ {send "~"}
        "\?" {
            send_user "?
"
            send_user "~~g  get file from remote system
"
            send_user "~~p  put file to remote system
"
            send_user "~~c  change/show directory on local system
"
            send_user "~~~  send ~ to remote system
"
            send_user "~~?  this list
"
            send_user "~~v  verbose mode toggle
                              (currently [verbose_status])
"
            send_user "~~^Z suspend
"
        }
        $CTRLZ {
            stty -raw echo
            exec kill -STOP 0
            stty raw -echo
        }
        -re . {send_user "unknown command
"}
    }
    send_user "resuming session...
"
}

After executing a command, users are returned to the shell. They can then execute more shell commands or enter ~~ to do more file transfers.

The v command is the simplest one that executes a procedure, verbose. The procedure just toggles a variable. Another procedure, verbose_status, is a simpler version which just tells the user what the value is. It is called if the user asks for help. Finally, there is a send_verbose which is called many places in the code. It prints its arguments, but only if the script is in verbose mode.

proc verbose {} {
    global verbose_flag

    set verbose_flag [expr !$verbose_flag]
    send_user "verbose [verbose_status]
"
}

proc verbose_status {} {
    global verbose_flag

    if $verbose_flag {
        return "on"
    } else {
        return "off"
    }
}

proc send_verbose {msg} {
    global verbose_flag

    if $verbose_flag {
        send_user $msg
    }
}

The c command is the simplest command that interacts with users. It starts by resetting the mode to cooked and enabling echo. This enables users to see what they are typing and fix any typos. Once the new directory is entered, the process sets it with cd. (If a user types cd instead of ~~c, only the remote host is affected.) Finally, the terminal mode is reset to how interact left it. The get and put functions handle the terminal mode the same way.

To make this bullet-proof, the cd should be wrapped in a catch. A lot more error checking could be added throughout the script.

proc chdir {} {
    stty -raw echo
    send_user "c
"
    send_user "current directory: [pwd], new directory: "
    expect_user -re "(.*)
" {
        cd $expect_out(1,string)
    }
    stty raw -echo
}

The get_main and put_main procedures get the names of the files to be copied. They are written here with expect_user commands although gets could have been used as well. If one name is entered, it is used as both source and destination name. Otherwise different names are used.

proc get_main {} {
    stty -raw echo
    send_user "g
get remote file [localfile]: "
    expect_user {
        -re "([^ ]+) +([^ ]+)
" {
            send_user "copying (remote) $expect_out(1,string) to
                (local) $expect_out(2,string)
"
            get $expect_out(1,string) $expect_out(2,string)
        } -re "([^ ]+)
" {
            send_user "copying $expect_out(1,string)
"
            get $expect_out(1,string) $expect_out(1,string)
        } -re "
" {
            send_user "eh?
"
        }
    }
    stty raw -echo
}

proc put_main {} {
    stty -raw echo
    send_user "p
put localfile [remotefile]: "
    expect_user {
        -re "([^ ]+) +([^ ]+)
" {
            send_user "copying (local) $expect_out(1,string) to
                (remote) $expect_out(2,string)
"
            put $expect_out(1,string) $expect_out(2,string)
        } -re "([^ ]+)
" {
            send_user "copying $expect_out(1,string)
"
            put $expect_out(1,string) $expect_out(1,string)
        } -re "
" {
            send_user "eh?
"
        }
    }
    stty raw -echo
}

The get and put procedures do the real work of transferring the files. They are rather entertaining and illustrate how to do a lot of work with very little in the way of tools.

The get procedure gets a file from the remote system and stores it locally. In essence, the script does this by sending a cat command to the local system. Locally, a file is created, and as lines arrive, they are written to the new file.

To be able to detect the end of the file, it is first uuencoded which leaves it with an obvious endmarker. This also solves the problem of transferring binary files. Since binary files are no problem, the file is compressed first on the remote system and uncompressed after reception on the local system. This speeds up the transfer.

The process id is used both locally and remotely to prevent collisions between multiple users. Of course, the remote process id is used on the remote side.

It is amusing to note that the conversation on the remote side is done entirely with vanilla UNIX commands, such as cat and stty—Expect is only required locally.

The stty command is sent immediately to disable echo. Later this drastically simplifies what is returned. For example, the echo of the cat command does not have to be stripped out from the beginning of the file listing.

The put procedure is similar in design to get although the details are different. Here, it is critical that echoing be disabled so that the whole file is not echoed back. There is nothing smart waiting to read the file on the other remote system—just cat. It suffices to send a ^D to close the file. Of course, the same compression and encoding occurs in put; however, it occurs in reverse with the remote system ultimately doing the uudecoding and uncompression.

proc get {infile outfile} {
    global prompt verbose_flag

    if (!$verbose_flag) {
        log_user 0
    }

    send_verbose "disabling echo: "
    send "stty -echo
"
    expect -re $prompt

    send_verbose "remote pid is "
    send "echo $$
"
    expect -re "(.*)
.*$prompt" {
        set rpid $expect_out(1,string)
    }

    set pid [pid]
    # pid is local pid, rpid is remote pid

    set infile_plain "/tmp/$rpid"
    set infile_compressed "$infile_plain.Z"
    set infile_encoded "$infile_compressed.uu"
    set outfile_plain "/tmp/$pid"
    set outfile_compressed "$outfile_plain.Z"
    set outfile_encoded "$outfile_compressed.uu"

    set out [open $outfile_encoded w]

    send_verbose "compressing
"
    send "compress -fc $infile > $infile_compressed
"
    expect -re $prompt

    # use label corresponding to temp name on local system
    send_verbose "uuencoding
"
    send "uuencode $infile_compressed $outfile_compressed > 
        $infile_encoded
"
    expect -re $prompt

    send_verbose "copying
"
    send "cat $infile_encoded
"

    log_user 0

    expect {
        -re "^end
" {
            puts $out "end"
            close $out
        } -re "^([^
]*)
" {
            puts $out $expect_out(1,string)
            send_verbose "."
            exp_continue
        }
    }

    if ($verbose_flag) {
        send_user "
"         ;# after last "."
        log_user 1
    }

    expect -re $prompt            ;# wait for prompt from cat

    send_verbose "deleting temporary files
"
    send "rm -f $infile_compressed $infile_encoded
"
    expect -re $prompt

    send_verbose "switching attention to local system

        uudecoding
"
    exec uudecode $outfile_encoded

    send_verbose "uncompressing
"
    exec uncompress -f $outfile_compressed

    send_verbose "renaming
"
    if [catch "exec cp $outfile_plain $outfile" msg] {
        send_user "could not move file in place, reason: $msg
"
        send_user "left as $outfile_plain
"
        exec rm -f $outfile_encoded
    } else {
        exec rm -f $outfile_plain $outfile_encoded
    }

    # restore echo and serendipitously reprompt
    send "stty echo
"

    log_user 1
}

proc put {infile outfile} {
    global prompt verbose_flag

    if (!$verbose_flag) {
        log_user 0
    }

    send_verbose "disabling echo: "
    send "stty -echo
"
    expect -re $prompt

    send_verbose "remote pid is "
    send "echo $$
"
    expect -re "(.*)
.*$prompt" {
        set rpid $expect_out(1,string)
    }

    set pid [pid]
    # pid is local pid, rpid is remote pid

    set infile_plain  "/tmp/$pid"
    set infile_compressed  "$infile_plain.Z"
    set infile_encoded  "$infile_compressed.uu"

    set outfile_plain  "/tmp/$rpid"
    set outfile_compressed  "$outfile_plain.Z"
    set outfile_encoded  "$outfile_compressed.uu"

    set out [open $outfile_encoded w]
    send_verbose "compressing
"
    exec compress -fc $infile > $infile_compressed

    # use label corresponding to temporary name on local
    # system
    send_verbose "uuencoding
"
    exec uuencode $infile_compressed $outfile_compressed > 
        $infile_encoded

    send_verbose "copying
"
    send "cat > $outfile_encoded
"

    log_user 0

    set fp [open $infile_encoded r]
    while 1 {
        if {-1 == [gets $fp buf]} break
        send_verbose "."
        send "$buf
"
    }

    if ($verbose_flag) {
        send_user "
"       ;# after last "."
        log_user 1
    }

    send "04"                ;# eof
    close $fp

    send_verbose "deleting temporary files
"
    exec rm -f $infile_compressed $infile_encoded

    send_verbose "switching attention to remote system
"

    expect -re $prompt    ;# wait for prompt from cat

    send_verbose "uudecoding
"
    send "uudecode $outfile_encoded
"
    expect -re $prompt

    send_verbose "uncompressing
"
    send "uncompress -f $outfile_compressed
"
    expect -re $prompt

    send_verbose "renaming
"
    send "cp $outfile_plain $outfile
"
    expect -re $prompt
    send_verbose "deleting temporary files
"
    send "rm -f $outfile_plain $outfile_encoded
"
    expect -re $prompt

    # restore echo and serendipitously reprompt
    send "stty echo
"

    log_user 1
}

You Have Unread News—tknewsbiff

biff is a UNIX program that reports when mail is received. In its fancier forms, it can pop up a picture of the sender or play an audio clip. If you receive mail from your boss, for example, you could have biff shout “Red Alert!”

tknewsbiff is a script to do the same thing but for Usenet news. When you have unread news, an audio clip can be played or some other action can be taken. By default, newsgroups with unread news are shown in a window along with the numbers of unread articles. Here is an example:

Figure 20-1. 

tknewsbiff is quite different from the other examples in this book. The script is customizable by additional Tcl commands supplied by the user. Customizations are stored in the file ~/.tknewsbiff. A simple version might look like this:

set server news.nist.gov
set delay 120
set server_timeout 60
set height 10

watch comp.unix.*
watch *.sources.*
watch dc.dining

ignore *.d

The first four commands set variables that control how often tknewsbiff checks for news, the news server to check, how long to wait for a response, and the maximum number of newsgroups to display at a time in the window. There are other variables which I will mention later. All are defined using set commands.

Next are definitions of which newsgroups to watch. The first command requests that all of the comp.unix groups be watched. Next is a pattern which matches all of the source related newsgroups. Finally, the dc.dining newsgroup (Washington DC restaurant news) is watched.

The watch command is just a Tcl procedure which I will show later. Another procedure is ignore. The example here causes all discussion groups to be ignored.

The watch command supports several flags. The -display flag names a command to execute when a newsgroup has unread news. The default action causes the newsgroup in the newsgroup variable to be scheduled for display when the window is redrawn. The -new flag names a command to execute when unread news first appears in a newsgroup. For example, the following lines invoke the UNIX command play to play a sound.

  watch *.dining  -new "exec play /usr/local/sound/yum.au"
  watch rec.auto* -new "exec play /usr/local/sound/vroom.au"

By default, -new and -display are evaluated when more than zero articles are unread. The -threshold flag specifies the number of articles after which actions should be evaluated. For instance, "-threshold 10" means that the newsgroup will not be displayed until at least 10 articles are unread.

You can cut down on the verbosity of actions by defining procedures. For example, if you have many -new flags that all play sound files, you could define a sound procedure. This allows the -new specifications to be much shorter.

proc play {sound} {
    exec play /usr/local/sound/$sound.au
}

Using play, the watch commands can be rewritten:

watch *.dining -new "play yum"
watch rec.auto* -new "play vroom"

The user-defined user procedure is run immediately after the newsgroups are scheduled to be written to the display and before they are actually written. Why is this useful? Suppose unread articles appear in several rec.auto groups and the same sound is to be played for each one. To prevent playing the sound several times in a row, the -new command can set a flag so that in the user procedure, the sound is played once and only if the flag is set.

The user procedure could also be used to start a newsreader. This would avoid the possibility of starting multiple newsreaders just because multiple newsgroups contained unread articles. If started with exec, a check should, of course, be made to verify that a newsreader is not already running. Alternatively, you could send a command to a Tk-based newsreader to switch to a new group or, perhaps, pop open a new window for the new group.

The tknewsbiff Script

The script starts by removing the default window from the screen. The window will be replaced when there is something to be displayed and removed when empty. Since this can happen as news is read or more arrives, two utility procedures are immediately defined and one is invoked. They also keep track of whether the user iconified the window or not.

#!/usr/local/bin/expectk —

proc unmapwindow {} {
    global _window_open

    switch [wm state .] 
    iconic {
        set _window_open 0
    } normal {
        set _window_open 1
    }
    wm withdraw .
}
unmapwindow
# force window to be open when mapped for the first time
set _window_open 1

proc mapwindow {} {
    global _window_open

    if $_window_open {
        wm deiconify .
    } else {
        wm iconify .
    }
}

Notice that the variable _window_open begins with an underscore. This is a simple attempt to keep things out of the user’s namespace. Anything beginning with an underscore is off-limits to the user (the tknewsbiff man page describes this). On the other hand, the procedures mapwindow and unmapwindow are public. The user can call them directly.

Another utility procedure is defined below. _abort is called when an error is encountered that is too severe for tknewsbiff to continue.

proc _abort {msg} {
    global argv0

    puts "$argv0: $msg"
    exit 1
}

The environment is now initialized, primarily by giving variables reasonable defaults. The directory in which to find the configuration files is located.

if [info exists env(DOTDIR)] {
    set home $env(DOTDIR)
} else {
    set home [glob ~]
}

set delay                 60
set width                 27
set height                10
set _default_config_file  $home/.tknewsbiff
set _config_file          $_default_config_file
set _default_server       news
set server                $_default_server
set server_timeout        60

log_user 0

A few Tk commands define the window that displays the newsgroups. More configuration will take place later when the newsgroups to be displayed are known.

listbox .list -yscroll ".scrollbar set" -font "*-m-*"
-setgrid 1
scrollbar .scrollbar -command ".list yview" -relief raised
pack .scrollbar -side left -fill y
pack .list -side left -fill both -expand 1

Next, the command-line arguments are parsed. The script accepts either a configuration file name or a hostname. If a hostname is given, it is used to find a host-specific configuration file. This enables a user to run multiple copies of tknewsbiff simultaneously, each monitoring news from a different host.

while {[llength $argv]>0} {
    set arg [lindex $argv 0]

    if [file readable $arg] {
        if 0==[string compare active [file tail
$arg]] {
            set active_file $arg
            set argv [lrange $argv 1 end]
        } else {
            # must be a config file
            set _config_file $arg
            set argv [lrange $argv 1 end]
        }
    } elseif {[file readable $_config_file-$arg]} {
        # maybe it's a hostname suffix for a newsrc file?
        set _config_file $_default_config_file-$arg
        set argv [lrange $argv 1 end]
    } else {
        # maybe just a hostname for a regular newsrc file?
        set server $arg
        set argv [lrange $argv 1 end]
    }
}

Once the configuration file is determined, it is read for additional information such as the newsrc location, server name, timeout, etc. The _read_config_file procedure sources the configuration file, allowing any user-written Tcl code to be executed in the global scope. This allows the user complete access to all other procedures and variables in the script, providing tremendous flexibility.

For simple configurations, the user does not have to know much about the syntax of Tcl. For example, commands such as set are common to most .rc files. On the other hand, the sophisticated user can write Tcl code and make tknewsbiff perform in very unusual ways. By providing these hooks, tknewsbiff avoids the burden of having a lot of special-case code. For example, tknewsbiff does not have to know how to play sounds on each computer since the user can call any external program to do it via exec.

The watch and ignore commands merely append their arguments to the lists watch_list and ignore_list. Making watch and ignore procedures is a little friendlier to the user. People unfamiliar with Tcl might be put off by having to learn about a command named lappend. (“What does that have to do with news!?”)

The user procedure is deleted just in case the user has also deleted it from the configuration file. If they have not deleted user, it will be recreated when the file is re-sourced. If no configuration file exists, the last command in the _read_config_file ensures that all newsgroups are watched.

proc _read_config_file {} {
    global _config_file argv0 watch_list ignore_list

    proc user {} {}
    set watch_list {}
    set ignore_list {}

    if [file exists $_config_file] {
        # uplevel allows user to set global variables
        if [catch {uplevel source $_config_file} msg] {
            _abort "error reading $_config_file
$msg"
        }
    }

    if [llength $watch_list]==0 {
        watch *
    }
}

proc watch {args} {
    global watch_list

    lappend watch_list $args
}

proc ignore {ng} {
    global ignore_list

    lappend ignore_list $ng
}

_read_config_file

Once the configuration file is read, a few last details are pinned down. The newsrc file can be located and the window can be titled to differentiate it from other tknewsbiff instances.

# if user didn't set newsrc, try ~/.newsrc-server
# if that fails, fall back to just plain ~/.newsrc
if ![info exists newsrc] {
    set newsrc $home/.newsrc-$server
    if ![file readable $newsrc] {
        set newsrc $home/.newsrc
        if ![file readable $newsrc] {
            abort "cannot tell what newgroups you read - found
                    neither $home/.newsrc-$server nor $home/.newsrc"
        }
    }
}
# initialize display
set min_reasonable_width 8
wm minsize . $min_reasonable_width 1
wm maxsize . 999 999
if {0 == [info exists active_file] &&
    0 != [string compare $server $_default_server]} {
    wm title . "news@$server"
    wm iconname . "news@$server"
}

A number of other procedures are created, and then tknewsbiff loops alternating between checking for news and sleeping. In the real script, the procedures have to be defined first, but I will show the loop now because it is easier to understand this way.

for {} 1 {_sleep $delay} {
    _init_ngs

    _read_newsrc
    if [_read_active] continue
    _read_config_file

    _update_ngs
    user
    _update_window
}

After some initialization, the body of the loop goes on to read the user’s newsrc file. This tells how many articles the user has read. tknewsbiff then checks to see how many new articles exist. Next, the user’s configuration file is read.

Once all the raw data has been collected, tknewsbiff decides what actions to take. _update_ngs creates an internal description of the newsgroups that contain new articles based on the work earlier in the loop. Prior to updating the visible window, a user-defined procedure, user, is called. This allows the user to look at and play with the internal description. For example, the user procedure could execute an action to start a newsreader.

Now I will describe each procedure. _read_newsrc reads the user’s newsrc file. The most recently read article is stored in the array db. For example, if the user has read article 5 in comp.unix.wizards, db(comp.unix.wizards,seen) is set to 5.

proc _read_newsrc {} {
    global db newsrc

    if [catch {set file [open $newsrc]} msg] {
        _abort $msg
    }
    while {-1 != [gets $file buf]} {
        if [regexp "!" $buf] continue
        if [regexp "([^:]*):.*[-, ]([0-9]+)" $buf dummy ng seen] {
            set db($ng,seen) $seen
        }
        # 2nd regexp can fail on lines that have : but no #
    }
    close $file
}

Next, tknewsbiff checks the number of articles in each group. By default, an NNTP connection is spawned to a news server. However, if the variable active_file exists, the local active file is read instead. Notice how the same code reads the data from either the file or the spawned process.

Each newsgroup is appended to the list active_list. The highest numbered article in each newsgroup is stored in db(newsgroup,hi).

proc _read_active {} {
    global db server active_list active_file
    upvar #0 server_timeout timeout

    set active_list {}

    if [info exists active_file] {
        spawn -open [open $active_file]
    } else {
        spawn telnet $server nntp
        expect {
            "20*
" {
                # should get 200 or 201
            } "NNTP server*
" {
                puts "tknewsbiff: unexpected response from server:"
                puts "$expect_out(buffer)"
                return 1
            } "unknown host" {
                _unknown_host
            } timeout {
                close
                wait
                return 1
            } eof {
                # loadav too high probably
                wait
                return 1
            }
        }
        exp_send "list
"
        # ignore echo of "list" command
        expect "list
"
        # skip "Newsgroups in form" line
        expect -re "215[^
]*
"
    }

    expect {
        -re "([^ ]*) 0*([^ ]+) [^
]*
" {
            set ng $expect_out(1,string)
            set hi $expect_out(2,string)
            lappend active_list $ng
            set db($ng,hi) $hi
            exp_continue
        }
        ".
" close
        eof
    }

    wait
    return 0
}

The _unknown_host procedure is called if telnet fails with that error.

proc _unknown_host {} {
    global server _default_server

    if 0==[string compare $_default_server $server] {
        puts "tknewsbiff: default server <$server> is not known"
    } else {
        puts "tknewsbiff: server <$server> is not known"
    }

    puts "Give tknewsbiff an argument - either the name
          of your news server or active file.  I.e.,

    tknewsbiff news.nist.gov
    tknewsbiff /usr/news/lib/active
    

    If you have a correctly defined configuration file
    (.tknewsbiff), an argument is not required.  See the
    man page for more info."
    exit 1
}

In the main loop of tknewsbiff, the next step is to reread the user’s configuration file. This is done so that the user can change it without having to restart tknewsbiff.

After reading the configuration file, tknewsbiff turns to the job of deciding what to do with all the data. The _update_ngs procedure looks through the newsgroup data and figures out which newsgroups should have their -display or -new actions executed. The code is careful to do actions in the same order that the user specified the newsgroups in the configuration file. Also, actions are not executed twice even if a newsgroup matches two different patterns. Following _update_ngs are two utility procedures that calculate whether newsgroup actions should be shown.

proc _update_ngs {} {
    global watch_list active_list newsgroup

    foreach watch $watch_list {
        set threshold 1
        set display display
        set new {}

        set ngpat [lindex $watch 0]
        set watch [lrange $watch 1 end]

        while {[llength $watch] > 0} {
            switch—[lindex $watch 0] 
            -threshold {
                set threshold [lindex $watch 1]
                set watch [lrange $watch 2 end]
            } -display {
                set display [lindex $watch 1]
                set watch [lrange $watch 2 end]
            } -new {
                set new [lindex $watch 1]
                set watch [lrange $watch 2 end]
            } default {
                _abort "watch: expecting -threshold, -display or
                    -new but found: [lindex $watch 0]"
            }
        }

        foreach ng $active_list {
            if [string match $ngpat $ng] {
                if [_isgood $ng $threshold] {
                    if [llength $display] {
                        set newsgroup $ng
                        uplevel $display
                    }
                    if [_isnew $ng] {
                        if [llength $new] {
                            set newsgroup $ng
                            uplevel $new
                        }
                    }
                }
            }
        }
    }
}

# test in various ways for good newsgroups
# return 1 if good, 0 if not good
proc _isgood {ng threshold} {
    global db seen_list ignore_list

    # skip if we don't subscribe to it
    if ![info exists db($ng,seen)] {return 0}

    # skip if the threshold isn't exceeded
    if {$db($ng,hi) - $db($ng,seen) < $threshold} {
        return 0
    }

    # skip if it matches an ignore command
    foreach igpat $ignore_list {
        if [string match $igpat $ng] {return 0}
    }

    # skip if we've seen it before
    if [lsearch -exact $seen_list $ng]!=-1 {return 0}

    # passed all tests, so remember that we've seen it
    lappend seen_list $ng
    return 1
}

# return 1 if not seen on previous turn
proc _isnew {ng} {
    global previous_seen_list

    if [lsearch -exact $previous_seen_list $ng]==-1 {
        return 1
    } else {
        return 0
    }
}

The display procedure schedules a newsgroup to be displayed. Internally, all it does is to append the newsgroup to the display_list variable. The current newsgroup is taken from the global newsgroup variable. The display procedure is the default action for the -display flag.

proc display {} {
    global display_list newsgroup

    lappend display_list $newsgroup
}

The final procedure in the main loop is _update_window which redraws the window, resizing and remapping it if necessary. The procedure _display_ngs is a utility procedure which rewrites the newsgroups in the window.

proc _update_window {} {
    global server display_list height width
    global min_reasonable_width

    if {0 == [llength $display_list]} {
        unmapwindow
        return
    }

    # make height correspond to length of display_list or
    # user's requested max height, whichever is smaller

    if {[llength $display_list] < $height} {
        set current_height [llength $display_list]
    } else {
        set current_height $height
    }

    # force reasonable min width
    if {$width < $min_reasonable_width} {
        set width $min_reasonable_width
    }

    wm geometry . ${width}x$current_height
    wm maxsize . 999 [llength $display_list]

    _display_ngs $width

    if [string compare [wm state .] withdrawn]==0 {
        mapwindow
    }
}

# write all newsgroups to the window
proc _display_ngs {width} {
    global db display_list

    set str_width [expr $width-7]

    .list delete 0 end
    foreach ng $display_list {
        .list insert end [
            format "%-$str_width.${str_width}s %5d" 
                $ng [expr $db($ng,hi) - $db($ng,seen)]
        ]
    }
}

The newsgroup window is initialized with a few simple bindings. The left button pops up a help window. (The help procedure is not shown here.) The middle button causes tknewsbiff to stop sleeping and check for new unread news immediately. The right mouse button causes the window to disappear from the screen until the next update cycle. Finally, if the user resizes the window, it is redrawn using the new size.

bind .list <1> help
bind .list <2> update-now
bind .list <3> unmapwindow
bind .list <Configure> {
    scan [wm geometry .] "%%dx%%d" w h
    _display_ngs $w
}

The user can replace or add to these bindings by adding bind commands in their configuration file. For example, here is a binding to pop up an xterm and run rn:

bind .list <Shift-1> {
    exec xterm -e rn &
}

Here is a binding that tells rn to look only at the newsgroup that was under the mouse when it was pressed.

bind .list <Shift-1> {
    exec xterm -e rn [lindex $display_list [.list nearest %y]] &
}

The tknewsbiff display can be further customized at this point by additional Tk commands. For example, the following command sets the colors of the newsgroup window:

.list config -bg honeydew1 -fg orchid2

After each loop, tknewsbiff sleeps. While it is not sleeping, it changes the shape of the cursor to a wristwatch to indicate that it is busy. The _sleep procedure itself is a little unusual. Instead of simply calling sleep, it waits for several characters from a spawned cat process. In the usual case, none arrive and _sleep returns after the expect times out.

However, if the user calls update-now (earlier this was bound to the middle button), a carriage-return is sent to the cat process. cat echoes this as four characters ( ) which is just what the expect in _sleep is waiting for. Thus, tknewsbiff wakes up if update-now is run. The cat process is spawned once at the beginning of the process.

spawn cat -u; set _cat_spawn_id $spawn_id
set _update_flag 0

proc _sleep {timeout} {
    global _cat_spawn_id _update_flag

    set _update_flag 0

    # restore to idle cursor
    .list config -cursor ""; update

    # sleep for a little while, subject to click from
    # "update" button
    expect -i $_cat_spawn_id -re "...."    ;# two crlfs

    # change to busy cursor
    .list config -cursor watch; update
}

proc update-now {} {
    global _update_flag _cat_spawn_id

    if $_update_flag return   ;# already set, do nothing
    set _update_flag 1

    exp_send −i $_cat_spawn_id "
"
}

The last things to be done in the script are some miscellaneous initializations. _init_ngs is called at the beginning of every loop, so that tknewsbiff starts with a clean slate.

set previous_seen_list {}
set seen_list {}

proc _init_ngs {} {
    global display_list db
    global seen_list previous_seen_list

    set previous_seen_list $seen_list

    set display_list {}
    set seen_list {}

    catch {unset db}
}

Exercises

  1. Use the timeout technique from the tknewsbiff script to cause an interact to return by pressing a Tk button. Compare this to using a signal.

  2. The file transfer script from page 461 assumes very little of the remote host. Modify the script so that it checks for the existence of rz/sz (or other tools) on the remote machine and uses them if possible. Similarly, use gzip if possible.

  3. Most compression programs can read from or write to a pipeline. Use this to reduce the number of temporary files used by the file transfer script.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.161.222