**For the impatient** The code below defines two commands '''hubs::interp::local_spawn''' and '''hubs::interp::ssh_spawn''' which are similar to [interp create] in that they create a new [interp] and return the name of a "slave command" that can be used to control it, e.g. tell it to [eval] whatever script one likes. What is new about these commands is however that the interpreter they create resides in a separate process. In the case of '''ssh_spawn''', that process may furthermore have been started on a completely different computer! For example, you can do ======none % hubs::interp::ssh_spawn my lars@example.tcl.tk ; # Bogus login info Password: Hello, I'm example.tcl.tk 52024 /usr/bin/tclsh 8.5.7 ::my::slave % my::slave eval {array get tcl_platform} osVersion 10.5.0 pointerSize 8 byteOrder littleEndian threaded 1 machine i386 platform unix os Darwin user lars wordSize 8 % my::slave exit ====== **Some context** For various reasons, it was a requirement for [hubs] that it should handle the following situation: 1. There is a process A running on a server somewhere. A is not allowed to open IP sockets. 2. Every once in a while, the user starts the process B locally on his computer. B needs to communicate with A. 3. To accomplish this, B logs in (using [ssh]) on the computer where A is running, and spawns a third process C there. Being on the same machine as A, it is possible (though not necessarily trivial) for C to communicate directly with A. The idea was to have C act as a router for the stream of messages between B and A. [hubs] has a lot of code for managing small networks of Tcl processes that send messages to each other, and for abstracting the details of the actual means employed in each communication link. This is all rather complicated (e.g., there is a dependence on [snit]), and a process like C can get started in a fairly barren environment, so some kind of bootstrapping would probably be needed: First set up an environment that is just stable enough that you can send arbitrary commands, then worry about loading whatever packages that'll be needed. The code below is mostly just the first step of that bootstrapping — a slave [interp] seems a very natural model to emulate — but it does have some bells and whistles built in that are rather meant for the full system. If you don't use them, then they shouldn't be a problem, but they can make it harder to understand the code. The [hubs] page has links to more documentation of the many protocols and interfaces that are in use. **The code** A ''child'' interpreter here is a separate Tcl interpreter process whose `stdin` and `stdout` are available to us as a channel—as would e.g. be the result of `open "|tclsh" r+`. This technique can be used to achieve multitasking without threads. A Tcl interpreter started at the other end of an SSH connection also falls into this category. ***Channel link endpoint as namespace*** When booting [hubs] in a child process, it is useful to avoid depending on [snit] (at least initially), so the following is a clone of the `hubs::link::channel` class as a namespace. namespace eval hubs::link::nschan {} The procedure to set up an "`nschan` object" is as follows: 1. Copy all procedures in the `hubs::link::nschan` namespace to a new namespace for the new object. 2. Call the `create` procedure in this new namespace. The latter initialises all the variables and creates an "object command" (a [namespace ensemble] or [interp alias] depending on Tcl version) for the new object, whose name is the same as that of the namespace. Conversely, the `destroy` "method" deletes the namespace and "object command". ****hubs::link::nschan::create**** The `create` procedure has the call syntax : ''namespace''::'''create''' ''output-channel'' ''input-channel'' where ''output-channel'' must be a channel open for writing and ''input-channel'' must be a channel open for reading. The return value is the fully qualified name of the new link object. As a special case, the ''input-channel'' may also be an empty string, in which case the link is set up with input already closed. This may be used to tie a link to a log file. The first step is to create the instance variables. See other [hubs] documentation for details on what they do. proc hubs::link::nschan::create {outputF inputF} { variable inF $inputF variable outF $outputF variable Callback array set Callback { got {first_of_whatever 0} undeliverable list garbled list error list } variable queue {} variable openB 1 variable buffer {} variable skimmingB 1 The second step is to create the object command. The pre-8.5 implementation of this command is an alias to `namespace inscope`, which is a slight abuse of data, but works here since all method names are invariant under `list`-quoting. if {[catch { namespace export {[a-z]*} namespace ensemble create }]} then { interp alias {} [namespace current] {}\ ::namespace inscope [namespace current] } The input channel is set up to start receiving immediately, so it is important that the `got` callback is set before entering the event loop. The `-blocking` setting makes it possible to read everything transmitted from the other end of the link without knowing beforehand how much that is. if {$inF == ""} then { unset inF } else { fconfigure $inF -blocking 0 -translation binary fileevent $inF readable [list [namespace current]::Receive 0] } The `-buffering` setting here means there's no need to `flush`, but it also makes the need for the `queue` more pronounced. The `\xC0\x8D` sequence is written to make sure that any partial message in the channel gets properly terminated. fconfigure $outF -blocking 0 -translation binary -buffering none fileevent $outF writable {} puts $outF \xC0\x8D return [namespace current] } ****hubs::link::nschan::first_of_whatever**** Again this little helper procedure that accepts any number of arguments and always returns the first. It is used as default for '''got''' callback, since the return value for this must be a boolean. Duplicating it in every link object is probably not necessary, but it's the easiest way to play it safe when booting remote interpreters. proc hubs::link::nschan::first_of_whatever {args} {lindex $args 0} ****hubs::link::nschan::close**** The presence of a '''close''' method is awkward in that it shadows the core command [close]. Therefore the latter must be written as `::close` in this namespace. The call syntax of the '''close''' method is : '''close''' ''close-messages'' ('''-listen''' ''boolean'' | '''-immediate''' ''urgency'' | '''-destroy''' ''boolean'' )* proc hubs::link::nschan::close {msgL args} { array set O {-listen 0 -immediate 0 -destroy 1} array set O $args variable openB 0 variable inF variable outF if {!$O(-listen) && [info exists inF]} then { fileevent $inF readable {} if {![info exists outF] || $inF!=$outF} then {::close $inF} unset inF } variable Callback variable queue if {$O(-immediate) > 0} then { foreach msg $queue { eval [linsert $Callback(undeliverable) end $msg] } set queue {} } if {$O(-immediate) > 1} then { set L {} foreach var {outF inF} { if {[info exists $var]} then { lappend L [set $var] unset var } } foreach F [lsort -unique $L] {::close $F} if {$O(-destroy)} then {destroy} return } if {[info exists inF]} then { fileevent $inF readable\ [list [namespace current]::Receive $O(-destroy)] } if {[info exists outF]} then { fileevent $outF writable\ [list [namespace current]::Transmit $O(-destroy)] foreach msg $msgL {lappend queue $msg} } else { foreach msg $msgL { eval [linsert $Callback(undeliverable) end $msg] } } if {$O(-destroy) && ![info exists inF] && ![info exists outF]}\ then {destroy} } ****hubs::link::nschan::destroy**** In principle the same name collision exists for the '''destroy''' method (which here has to be implemented explicitly), but that's not an issue as we don't need the Tk [destroy] command. This command takes no arguments, and is supposed to release all resources held by the object. This means: * The file handles. * The "object command". * The object namespace itself (and hence everything in it). proc hubs::link::nschan::destroy {} { variable inF if {[info exists inF]} then {catch {::close $inF}} variable outF if {[info exists outF]} then {catch {::close $outF}} catch {rename [namespace current] ""} namespace delete [namespace current] } ****hubs::link::nschan::callback**** The implementation of the '''callback''' method is now obvious. An error is thrown if the event is not among the array elements. proc hubs::link::nschan::callback {event {prefix ""}} { variable Callback if {![info exists Callback($event)]} then { error "Unknown event \"$event\": must be [ join [lsort -dictionary [array names Callback]] {, } ]" } if {[llength $prefix]} then { set Callback($event) $prefix } else { return $Callback($event) } } ****hubs::link::nschan::put**** The '''put''' method arranges for a message to be sent. The actual transmission is handled by the '''Transmit''' method, which is called as a file event handler. proc hubs::link::nschan::put {msg} { variable queue variable openB variable outF variable Callback if {$openB} then { lappend queue $msg fileevent $outF writable [list [namespace current]::Transmit 0] } else { eval [linsert $Callback(undeliverable) end $msg] } } ****hubs::link::nschan::Transmit**** This method is meant to be called from a [fileevent writable] handler, since it writes data to a non-buffering channel. The call syntax is : ''object'' '''Transmit''' ''destroy'' where ''destroy'' is true if the object is no longer open and it should be '''destroy'''ed as soon as all traffic has completed. It's usually false, however. proc hubs::link::nschan::Transmit {destroy} { variable inF variable outF variable queue variable openB variable Callback if {![llength $queue]} then { if {$openB} then { fileevent $outF writable {} } else { if {![info exists inF] || $inF!=$outF} then {::close $outF} unset outF if {$destroy && ![info exists inF]} then {destroy} } return } if {[catch { puts $outF "[ encoding convertto utf-8 [lrange $queue 0 0] ]\xC0\x8D" } res] == 0} then { set queue [lreplace $queue 0 0] return } set infoD [list -errorcode $::errorCode] eval [linsert $Callback(error) end $res $infoD] fileevent $outF writable {} } ****hubs::link::nschan::Receive**** This method is the [fileevent readable] handler for incoming material. The call syntax is : ''object'' '''Receive''' ''destroy'' where ''destroy'' is true if the object is no longer open and it should be `destroy`ed as soon as all traffic has completed. It is usually false, however. Errors when reading are reported straight off. proc hubs::link::nschan::Receive {destroy} { variable inF variable buffer variable Callback if {[catch { append buffer [read $inF] } res]} then { set infoD [list -errorcode $::errorCode] eval [linsert $Callback(error) end $res $infoD] } An end of file situation usually occurs when the link is closing down, but if it occurs unexpectedly then it is comparable to an error and reported as such. However, since no technical error occurred, there is no `-errorcode` entry in the dictionary. The `error` callback ''should'' close the link, but in case it doesn't, the '''Receive''' method will make the necessary call while telling the object to live on as a `zombie`. An extra end-of-message is appended to the `buffer` to make sure that everything read gets processed. variable openB variable outF if {[info exists inF] && [eof $inF]} then { if {$openB} then { eval [linsert $Callback(error) end "Unexpected EOF" {}] } if {$openB} then {close {} -destroy 0} The calls above may have closed the input channel while we weren't looking, so it's necessary to check `inF` existence again. if {[info exists inF]} then { if {![info exists outF] || $outF!=$inF} then { ::close $inF } else { fileevent $inF readable {} } unset inF } append buffer \xC0\x8D } After all that error and shutdown handling, we're now at the main part of this method: split off, decode, and handle the message(s) received. If ignoring is on, then turn it off instead of treating what is split off from the buffer as a message. variable skimmingB while {[ set pos [string first \xC0\x8D $buffer] ] >= 0} { set chunk [string range $buffer 0 [expr {$pos-1}]] set buffer [string range $buffer [expr {$pos+2}] end] if {$skimmingB} then { set skimmingB 0 } elseif {[catch { encoding convertfrom utf-8 $chunk } msg] || [catch {llength $msg} len] || $len>1} then { eval [linsert $Callback(garbled) end $chunk $msg] } elseif {$len == 1} then { if {[ eval [linsert $Callback(got) end [lindex $msg 0]] ]} then { Another special case, which makes it possible to react immediately to a `close` message. It is important if the object has been deregistered by the hub but was still delivering incoming traffic, since the hub then cannot call the link object anymore. if {[info exists inF]} then { if {![info exists outF] || $outF!=$inF} then { ::close $inF } else { fileevent $inF readable {} } unset inF } break } } } Finally one more point of shutdown: if the object is set to self-destruct and there's nothing more to do, then carry out that deed. if {$destroy && ![info exists inF] && ![info exists outF]} then { destroy } } ****hubs::link::nschan::state**** This final method reports the current state of the link. proc hubs::link::nschan::state {} { set res {} variable outF if {[info exists outF] && [llength [fileevent $outF writable]]}\ then {lappend res busy} variable buffer if {[regexp {\S} $buffer]} then {lappend res incoming} variable skimmingB if {$skimmingB} then {lappend res skimming} variable openB variable inF if {!$openB} then { if {[info exists outF] || [info exists inF]} then { lappend res closing } else { lappend res zombie } } if {![llength $res]} then {lappend res idle} return $res } ****hubs::link::nschan::on_zombie**** This procedure is meant to be called from [unset] traces on the `inF` and `outF` variables; it evaluates a script when both are unset. One application is to arrange for a child interpreter to [exit] when it loses contact with the parent. The call syntax is : '''on_zombie''' ''script'' ''trace-arguments...'' It is the namespace context of this procedure that is used to identify the link as a whole. proc hubs::link::nschan::on_zombie {script args} { variable inF variable outF if {![info exists inF] && ![info exists outF]} then $script } ***Remote evaluation*** Since remote evaluation would be important for a remote booting system, it is implemented as a namespace with state (replies being waited upon and senders that have identified themselves). namespace eval hubs::eval {} ****hubs::eval::do-8.5**** This procedure takes a script, evaluates it, and returns the triplet : ''status'' ''result'' ''info'' that one gets from `catch`ing this evaluation. It relies on the Tcl 8.5 extension (hence the name) of the [catch] command. The call syntax is : '''hubs::eval::do-8.5''' ''prefix'' ''script'' where ''prefix'' is a command prefix to which the ''script'' will be appended, to produce the command to be caught. Some ''prefix''es of interest are: * `uplevel \#0` * `namespace eval` ''namespace'' * `interp eval` ''path'' proc hubs::eval::do-8.5 {prefix script} { list [eval {catch [lappend prefix $script] res info}] $res $info } Regarding the [eval], see Tcl bug #2038069. ****hubs::eval::do-8.4**** This procedure takes a script, evaluates it using a custom ''prefix'', and returns the triple : ''status'' ''result'' ''info'' that one gets from [catch]ing this evaluation. It is meant to work under Tcl 8.4 and earlier, so it does not use the ''info'' extension of the [catch] command, but instead takes `-errorinfo` and `-errorcode` from the global variables. The call syntax is : '''hubs::eval::do-8.4''' ''prefix'' ''script'' proc hubs::eval::do-8.4 {prefix script} { set status [eval {catch [lappend prefix $script] res}] if {$status == 1} then { list $status $res [ list -errorinfo $::errorInfo -errorcode $::errorCode ] } else { list $status $res {} } } The result from this command is different from that of '''do-8.5''' in that it doesn't provide `-level` and `-code` entries in the ''info'', but there is little point in faking these—'''renormalise''' the result if you need these entries. ****hubs::eval::do-not**** This procedure is an extended catch command that never evaluates anything and always responds with an error. proc hubs::eval::do-not {script} { list 1 "Eval not supported" {-errorcode {hubs ENOTSUP}} } ****hubs::eval::renormalise**** This procedure renormalises the : ''status'' ''result'' ''info'' triplet returned by an extended catch command; it has the call syntax : '''renormalise''' ''triplet'' and returns the renormalisation of the ''triplet''. Renormalisation means: 1. The ''status'' must be an integer. The standard aliases `ok`, `error`, `return`, `break`, and `continue` are converted to their corresponding codes, but all other noninteger ''status''es are turned into errors. 2. The ''info'' dictionary contains `-level` and `-code` entries. The `-level` value is nonzero if and only if the ''status'' is `2`. The `-code` value is an integer not equal to `2`, and it is equal to the ''status'' unless the latter is `2`. proc hubs::eval::renormalise {triplet} { foreach {status res info} $triplet break if {![string is integer -strict $status]} then { switch -- $status { ok {set status 0} error {set status 1} return {set status 2} break {set status 3} continue {set status 4} default { return [list 1 "Bad status code \"$status\""\ [list -oldtriplet $triplet -level 0 -code 1\ -errorinfo "(renormalisation rewrite)"]] } } } array set A $info if {$status != 2} then { set A(-code) $status set A(-level) 0 } else { if {![info exists A(-level)]} then {set A(-level) 1} if {![info exists A(-code)]} then { set A(-code) 0 } elseif {![string is integer -strict $A(-code)]} then { switch -- $A(-code) { ok {set A(-code) 0} error {set A(-code) 1} return {set A(-code) 2} break {set A(-code) 3} continue {set A(-code) 4} default { return [list 1 "Bad -code \"$A(-code)\""\ [list -oldtriplet $triplet -level 0 -code 1\ -errorinfo "(renormalisation rewrite)"]] } } } if {$A(-code)==2} then { incr A(-level) set A(-code) 0 } if {$A(-level) <= 0} then { set status $A(-code) set A(-level) 0 } } return [list $status $res [array get A]] } ****hubs::eval::filter_result**** This is a wrapper around an extended catch command, which however itself has the syntax of an extended catch command. The idea is that it filters the result returned, preventing results that would have dangerous consequences if rethrown in the caller. The call syntax is : '''filter_result''' ''expression'' ''xcatch-prefix'' ''script'' where the ''xcatch-prefix'' is called (from the calling context) with the ''script'' as extra argument. The return value is a renormalised "''status'' ''result'' ''info-dict''" triplet. The ''expression'' is an expression evaluated in the '''filter_result''' procedure. The result is only let through if this expression returns true. The status code is available in the `status` variable, the result is available in the `res` variable, and the info dictionary has been unrolled into the `A` array. proc hubs::eval::filter_result {expr prefix script} { foreach {status res info} [renormalise\ [uplevel 1 [lappend prefix $script]]] break array set A $info if $expr then { return [list $status $res $info] } else { return [list 1 "Forbidden return status: $status"\ [list -code 1 -level 0 -oldtriplet [list $status $res $info]\ -errorinfo "(result filtering)"]] } } ****hubs::eval::extra_errorinfo**** This is a wrapper around an extended catch command, which however itself has the syntax of an extended catch command. This wrapper appends text to the `-errorinfo` (if there is any, otherwise it does nothing), which can be used to include extra information, e.g.~that this error has been transported from one process to another. The call syntax is : '''extra_errorinfo''' ''msg'' ''xcatch-prefix'' ''script'' proc hubs::eval::extra_errorinfo {msg prefix script} { set triplet [uplevel 1 [lappend prefix $script]] array set A [lindex $triplet 2] if {![info exists A(-errorinfo)]} then {return $triplet} append A(-errorinfo) \n $msg return [lreplace $triplet 2 2 [array get A]] } ****hubs::eval::reply($id)**** When a reply with identifier ''id'' is expected, the corresponding entry in this array is set to an empty list. When such a reply arrives, the entry is set to a three-element list : ''status'' ''result'' ''info-dict'' Typically, the entries in this array are being [vwait]ed upon. ****hubs::eval::write_off_reply**** This procedure provides a dummy error reply to the largest entry in the `reply` array. It can (hopefully) be used to end a [vwait] for a reply that should have come back long ago. The call syntax is : '''write_off_reply''' ?''message''? ?''errorinfo''? ?''errorcode''? where the arguments specify the error to leave as the result. The return value is the index into the `reply` array for the entry that was set, or an error if no entry to set existed. proc hubs::eval::write_off_reply {args} { if {[llength $args]<1} then {lappend args "Manual abort"} if {[llength $args]<2} then { lappend args "[lindex $args 0]\n by\n\"[info level 0]\"\n" } if {[llength $args]<3} then {lappend args {hubs ECANCELED}} variable reply foreach id [lsort -dictionary -decreasing [array names reply]] { if {[llength $reply($id)]} then {continue} set reply($id) [list 1 [lindex $args 0]\ [list -level 0 -code 1 -errorinfo [lindex $args 1]\ -errorcode [lindex $args 2]]] return $id } error "No reply pending" } ****hubs::eval::master($fromAddr)**** This array holds `-presentation`s of masters. `eval` messages from masters that haven't presented themselves are typically rejected. ****hubs::eval::handlemsg**** This procedure parses messages and handles them. The call syntax is : '''hubs::eval::handlesmg''' ''do-cmd'' ''from'' ''id'' ''message'' and the return value is a list of messages to send in return (usually zero or one `reply` messages). ''do-cmd'' is the extended catch command used to actually evaluate scripts provided in `eval` messages; it is typically `do-8.4 {uplevel #0}` or ditto `do-8.5`. (However, if one uses an `interp eval` instead of the `uplevel \#0` then it is possible to get better emulation of ordinary interpreter behaviour—concretely avoid visibly dropping out to level #0 just because of being in an event handler, because that happened in the interpreter handling messages, not the interpreter where the code is being evaluated.) ''from'' is a return address and ''id'' is a message identifier provided by a higher level mechanism; neither is parsed, and may be generated locally, but they are used in some cases. proc hubs::eval::handlemsg {doprefix from id msg} { switch -- [lindex $msg 0] "eval" { set O(-id) $id set O(-reply) 1 array set O [lreplace $msg 0 1] variable master if {[info exists O(-presentation)]} then { set master($from) $O(-presentation) } elseif {![info exists master($from)]} then { return [list [list reply $O(-id) 1\ {Who is you to tell me what to do?}\ {-errorcode {hubs ENEEDAUTH}}]] } set res [eval [linsert $doprefix end [lindex $msg 1]]] if {$O(-reply)} then { return [list [linsert $res 0 reply $O(-id)]] } else { return "" } } "reply" { variable reply if {[info exists reply([lindex $msg 1])]} then { set reply([lindex $msg 1]) [lrange $msg 2 end] } return "" } } ****hubs::eval::linkglue**** This procedure can be used to "glue" '''handlemsg''' directly to a link endpoint. It is meant to be called as the '''got''' handler of the link, and will '''put''' any replies back on the link. The call syntax is : '''linkglue''' ''link'' ''docmd'' ''from'' ''msg'' where ''link'' is the link object (for replying), ''docmd'' is the extended catch command used to evaluate scripts in `eval` messages, ''from'' is where the message should count as coming from (index into the `master` array), and ''msg'' is the message itself. proc hubs::eval::linkglue {link docmd from msg} { foreach reply [ handlemsg $docmd $from {} $msg ] { $link put $reply } return 0 } ****hubs::eval::linkcatch**** This procedure is a kind of "`interp catch`" for child interpreters interfaced directly with a link endpoint. The call syntax is : '''hubs::eval::linkcatch''' ''link'' ''script'' and the return value is a triple status–result–info that is result of evaluating the ''script'' on the remote side. The procedure calls [vwait] to wait for a reply, so it enters the event loop. It does not supply any `-presentation` of itself; you should do that separately if needed (see the end of '''hubs::interp::initchild''' for an example of how). [info cmdcount] is used to generate an identifier for the reply. proc hubs::eval::linkcatch {link script} { set id [info cmdcount] variable reply set reply($id) {} $link put [list eval $script -id $id] vwait [namespace current]::reply($id) set res $reply($id) unset reply($id) return $res } ****hubs::eval::hubglue**** This procedure can be used as a hub handler for `eval` messages. It has the call syntax : '''hubs::eval::hubglue''' ''async'' ''do-cmd'' ''hub'' ''from'' ''id'' ''message'' and no particular return value. ''async'' is a boolean; if it is `1` then the procedure returns immediately and arranges for the message to be processed by an `after 0` script, otherwise it is processed before the procedure returns. ''do-cmd'' is as for '''handlemsg'''. ''hub'' is the hub to use when sending replies (which ought to be the same as we're handling the event for, since that is where the ''from'' address is valid). proc hubs::eval::hubglue {async docmd hub from id msg} { if {$async} then { after 0 [namespace code [list\ hubglue 0 $docmd $hub $from $id $msg]] return } foreach reply [ handlemsg $docmd $from $id $msg ] { eval [linsert $reply 0 $hub send $from ""] } } ****hubs::eval::twistglue**** This procedure can be used to "glue" '''handlemsg''' to the link side of a '''hubs::link::twist''' link endpoint. When doing so, one gets the partial illusion of having a hub at the other end, but the only thing this "hub" ever does is process `eval` and `reply` messages directed specifically to it; everything else gets a `delivery nack`. The call syntax is : hubs::eval::twistglue ''mode'' ''do-cmd'' ''prefix'' ''message'' where the ''message'' is a `connect`, `close`, or `envelope`; only the last of these is actually processed. The ''mode'' is `1` if evaluation is to be handled asynchronously, `0` if it is to be handled synchronously, and `-1` if it is to be handled synchronously and it has been verified that this is an `envelope` that we want to process. The ''prefix'' is the command prefix used for sending reply messages back over the link (typically an `incoming` method call). proc hubs::eval::twistglue {mode docmd prefix msg} { foreach {type to from id payload} $msg break if {$mode>=0} then { if {$type != "envelope"} then {return} set done 1 if {$to != ""} then { set ret [list delivery nack $id $to {No such connection}] } elseif {[lindex $payload 0] != "eval" &&\ [lindex $payload 0] != "reply"} then { set ret [list delivery nack $id {} {No handler for subject}] } else { set ret [list delivery ack $id [clock seconds]] set done 0 } if {$id != ""} then { eval [linsert $prefix end [list envelope $from {} "" $ret]] } if {$done} then {return} } if {$mode==1} then { after 0 [namespace code [list twistglue -1 $docmd $prefix $msg]] return } foreach reply [ handlemsg $docmd $from $id $payload ] { eval [linsert $prefix end [ list envelope $from {} "" $reply ]] } } ****hubs::eval::throw-8.4**** This procedure calls an extended catch command and throws its result back at the caller. The call syntax is : '''hubs::eval::throw-8.4''' ''catch-cmd-prefix'' ''mode'' ''script'' ... The idea is that a "''slave'' '''eval'''" type command should be possible to implement as an alias to '''throw'''. The ''catch-cmd-prefix'' is the extended catch command prefix used. The ''mode'' specifies how the subsequent arguments should be turned into a script. The supported modes are: +++ concat The words of the ''script'' are [concat]enated into a script. This results in [eval] semantics. just There must be exactly one additional argument, and it is the script. This somewhat matches the [catch] behaviour. lappend The first word of the ''script'' is a list of words, and any additional words are appended to it. This results in [namespace inscope] semantics. list The words of the ''script'' constitute the list of words of the command. This is rather [interp alias] semantics. +++ proc hubs::eval::throw-8.4 {catchprefix mode args} { switch -- $mode "concat" { set script [eval [list concat] $args] } "just" { if {[llength $args] != 1} then { return -code error "wrong # args, must be:\ throw-8.4 just