Version 5 of A Coroutine-Enabled Interactive Command Line

Updated 2009-07-31 16:44:45 by CMcC

NEM 2009-07-31: Here's a simple interactive tclsh-style application, with the difference that it runs inside a coroutine. This allows you to have complex event-driven interactions appear as if they are happening in a blocking, non-event-driven manner. For example, you can have Tk GUI applications responding to events while you are apparently in a blocking HTTP request.

# corotcl --
#
#       A coroutine-enabled tclsh
#
package require Tk
proc lambda {params body args} {
    list ::apply [list $params $body] {*}$args
}
proc prompt p {
    puts -nonewline "$p "
    flush stdout
    fileevent stdin readable [lambda return {
        $return [gets stdin]
    } [info coroutine]]
    yield
}
proc get-command {} {
    set cmd [prompt %]
    while {![info complete $cmd]} {
        append cmd \n [prompt >]
    }
    return $cmd
}
proc repl {} {
    while 1 {
        set cmd [get-command]
        set code [catch { uplevel #0 $cmd } result opts]
        if {$code == 1} {
            puts [dict get $opts -errorinfo]
        } else { puts $result }
    }
}
coroutine main repl
#vwait forever ;# if no Tk

An example of use. First, we bring up a simple Tk interface to check that everything keeps running in the background:

% pack [button .b -text Test -command {puts Test}]
.b

Testing this will show that it does print "Test" in the console when clicked. So far, so normal. Now, we create a coroutine-enabled version of http::geturl:

% package require http
2.7.2
% proc fetch url {
>   http::geturl $url -command [info coroutine]
>   yield
> }

We can now use this to fetch data from the web, just like the usual geturl. The difference is that this works in the background: you can still click on the Tk button and it will still print to the console even while the fetch is still in progress:

% set t [fetch http://wiki.tcl.tk/4]
Test
Test
Test
::http::1

There we go: an interactive Tcl interpreter that is fully coroutine enabled.

Next step? Futures?


CMcC - 2009-07-31 11:36:54

Very cool.

Here's a variant which uses a TclOO object called Shell so one can have multiple of these constructed with different chans as i/o. If you construct it with a 'port' argument, it'll listen on localhost.

# Shell -- a coroutine enabled tcl evaluator # # From Neil Madden's corotcl http://wiki.tcl.tk/24060 # # Usage: # # Shell new - create a shell listening on stdio # Shell new in $chan out $chan - shell connected to chan # Shell new port $port - shell server on localhost port $port

package require TclOO namespace import oo::*

package provide Shell

class create Shell {

    variable interp
    constructor {args} {
        # prompt for input, collect it and return
        proc prompt {in out p} {
            puts -nonewline $out "$p "
            chan flush $out
            chan event $in readable [list ::apply {{return in} {
                $return [gets $in]
            }} [info coroutine] $in]
            return [yield]
        }

        # read-eval-print loop - prompt, gets input, evaluate it, print result
        proc repl {self in out} {
            variable interp
            while {1} {
                set cmd [prompt $in $out %]
                while {![info complete $cmd]} {
                    append cmd \n [prompt $in $out >]
                }

                try {
                    {*}$interp $cmd
                } on error {result eo} {
                    puts $out [dict get $eo -errorinfo]
                } on return {result} {
                    break
                } on ok {result} {
                    puts $out $result
                }
            }

            # close the i/o unless it's stdio
            if {$in ne "stdin"} {
                chan close $in read
            } else {
                chan event $in readable {}        ;# stop listening to stdin
            }
            if {![string match std* $out]} {
                chan close $in write
            }

            return $result
        }

        set interp {uplevel #0}
        set in stdin; set out "";# default - use stdio
        set host localhost        ;# default - listen only to localhost

        dict with args {
            if {[info exists port]} {
                # what is wanted is a listener
                socket -server [list ::apply {{sock addr port} {
                    set shell [Shell new in $sock]
                }}]  -myaddr $host $port
            } else {
                # we have a chan (or a couple of chans)
                if {$out eq ""} {
                    if {$in eq "stdin"} {
                        set out stdout
                    } else {
                        set out $in
                    }
                }
                chan configure $out -buffering line
                coroutine [self]_CORO repl [self] $in $out
            }
        }
    }

}

if {info exists argv0 && ($argv0 eq info script)} {

    puts "Shell on stdio"
    Shell new
    puts "Shell on localhost port 8082"
    Shell new port 8082 interp {uplevel #1}
    vwait forever

}