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.
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 " "
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 }
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:
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 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} }
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.
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.
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.
3.137.161.222