The Java try ... catch ... finally construct is really nice for those cases when you want to guarantee to always release a resource (particularly a file handle) even if the code generates an exception. It is also very useful to be able to use different types of error handling depending on the error that occurred. This code implements such a facility in pure Tcl.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | namespace eval ::try {
namespace export try
variable bodyMatch { \("uplevel" body line}; # Note: must be FOUR spaces
variable usage {unknown keyword "%s" to try: should be "try body ?catch matcher body ...? ?finally body?"}
# Some code that is factorised out. It runs the given script in the context of our
# caller's caller and, if that call generates an error, removes the extra junk inserted
# into the error trace due to this. All results are passed back by setting variables
# in our caller's context...
# The part argument gives the part of the error trace to insert into the error trace
# in the case of an error occurring so as to indicate what the context of the error
# w.r.t. the try command really is.
proc helper {script part {eiv ei} {ecv ec} {codev code} {msgv msg}} {
global errorInfo errorCode
variable bodyMatch
upvar 1 $eiv ei $ecv ec $codev code $msgv msg
set code [catch [list uplevel 2 $script] msg]; # Note: unusual uplevel parameter
set ec $errorCode
set lines [split $errorInfo "\n"]
if {$code == 1} {
while {![regexp $bodyMatch [lindex $lines end]]} {
set lines [lrange $lines 0 [expr {[llength $lines]-2}]]
}
regsub {"uplevel" body} [lindex $lines end] $part fixed
set lines [lrange $lines 0 [expr {[llength $lines]-2}]]
lappend lines $fixed
}
set ei [join $lines "\n"]
}
# The main command's implementation (see example for syntax.)
proc try {body args} {
# First, parse apart the args. This is relatively straight-forward
set hasFinally 0
set catches {}
for {set i 0} {$i<[llength $args]} {incr i} {
set word [lindex $args $i]
if {![string compare $word catch]} {
if {$i+1 >= [llength $args]} {
return -code error "missing matcher to catch"
} elseif {$i+2 >= [llength $args]} {
return -code error "missing body to catch"
}
lappend catches [lindex $args [incr i]]
lappend catches [lindex $args [incr i]]
} elseif {![string compare $word finally]} {
if {$i+1 >= [llength $args]} {
return -code error "missing body to finally"
}
set finally [lindex $args [incr i]]
set hasFinally 1
} else {
variable usage
return -code error [format $usage $word]
}
}
# Now evaluate the body. This updates variables "code", "ei", "ec" and "msg"
# with the result code, the error trace, the error detail and the returned value
# respectively...
helper $body "try body"
# Handle errors, if there were any. Note that if an error does occur and gets
# handled, the details of what the error was are lost as part of the processing.
# Doing something more sophisticated is left as an exercise to the reader...
if {$code == 1} {
foreach {matcher handler} $catches {
if {[string match $matcher $ec]} {
helper $handler "catch \"$matcher\" body"
break
}
}
}
# Do the finally clause. This only wipes out the result information if the clause
# causes an error. Otherwise, the clause has no effect. In particular, you cannot
# (successfully) use return, break or continue in a finally clause.
if {$hasFinally} {
helper $finally "finally clause" a b c d
if {$c} {
set ei $a
set ec $b
set code $c
set msg $d
}
}
# Now we can return. Phew!
return -code $code -errorinfo $ei -errorcode $ec $msg
}
}
namespace import ::try::try
|
It is pretty easy to use the above script. Here's a demonstration: try { set f [open /non/existant/file] } catch {POSIX ENOENT *} { puts "That file doesn't exist!" } catch {POSIX EACCES *} { puts "You may not open that file!" } finally { puts "Wibble..." }
Usage of this sort of thing is actually more important in Tcl than in Java, because a great many Tcl resources (particularly commands and channels) are not garbage collected, and so must be deleted explicitly, a process which can be easy to forget to do when an error happens. With this, you can make cleanup in all possible error cases a much simpler chore, and provide extra special handling for some errors.
The error handling of this code is based on the comparatively little-used ::errorCode variable. This makes it ideal for handling system problems, but you should note that it is all too easy to forget to create special error types for your own code, which converts this tool into something rather blunt. Remember to create those machine-readable errors; both [return] and [error] support them!
Candidate for "control" module in Tcllib. This code is a candidate for the "control" module in Tcllib.
Funny... funny, i just started writing the very same thing last night :-)
anyways i recommend a different way to use the construct:
there is also a need to decide on a break/continue semantics in catch clauses...
any thoughts? /NL