withOpenFile

Larry Smith withOpenFile looks like something tk-related. I would use something like "with filename {};" "using filename {};" or "accessing filename {}" "With" is popular with dicts, so usings or accessing might be better. Ideally, an ensemble, "file with <name> do {}'".

2014-01-11: with open file is an old programming pattern or idiom that abstracts the following:

  1. open a channel to a file
  2. do something with the channel
  3. close the channel

(When I started this page, I first searched this wiki for similar pages, but still missed a couple of pages (keyword search will only get you so far): Playing with with and using, both of which address the same subject.)

In Tcl code, e.g. on this wiki, this procedure is usually written step-by-step, usually without error handling (and sometimes without actually closing the file):

set f [open $myfile]
# ... $f ...
close $f

Abstracting away such sequences will make the code less cluttered. A couple of examples:

proc slurp {filename} {
    set f [open $filename]
    set res [chan read $f]
    chan close $f
    set res
}

proc fileputs {filename str} {
    set f [open $filename a]
    chan puts $f $str
    chan close $f
}

Doing it this way, one command is needed for every file action. Instead, a generic command can be used for all cases where some kind of operation is to be done on a temporarily open file:

proc withOpenFile args {
    if {[llength $args] < 3} {
        error {wrong # args: should be "withOpenFile channelIdName filename ?access? ?permissions? script"}
    }

    upvar 1 [lindex $args 0] channelid

    try {
        open {*}[lrange $args 1 end-1]
    } on ok channelid {
        uplevel 1 [lindex $args end]
    } finally {
        catch {chan close $channelid}
    }
}

The point of having a try structure without error handling is that it ensures that the channel will be closed even if the script terminates with an exception.

So, instead of

set filename foo.bar
set mytext {foo bar}
# ...
set f [open $filename a]
chan puts $f $mytext
chan close $f
# ...
set f [open $filename]
set result [chan read $f]
chan close $f

you could write

set filename foo.bar
set mytext {foo bar}
# ...
fileputs $filename $mytext
# ...
set result [slurp $filename]

or

set filename foo.bar
set mytext {foo bar}
# ...
withOpenFile f $filename a {chan puts $f $mytext}
# ...
set result [withOpenFile f $filename {chan read $f}]

andrewsh 2015-03-11: Here's something similar I hacked together about three years ago. As dbohdan pointed out, I could probably do without redefining proc, but anyway. I must admit I never used this code in production :)

#!/usr/bin/env tclsh

namespace eval ::autofile {
}

rename ::proc ::autofile::proc
rename ::open ::autofile::open
set ::autofile::files(::) {}

::autofile::proc proc {name args body} {
    set ns [uplevel {
        namespace current
    }]
    if {$ns eq "::"} {
        set ns {}
    }
    set name $ns\::$name
    ::autofile::proc $name $args $body
    trace add execution $name leave ::autofile::autoclose
}

::autofile::proc open args {
    set cmd [uplevel {
        if {[dict exists [info frame -2] proc]} {
            namespace which [dict get [info frame -2] proc]
        }
    }]
    set file [::autofile::open {*}$args]
    lappend ::autofile::files($cmd) $file
    puts "don't forget to close $::autofile::files($cmd) for $cmd"
    return $file
}

::autofile::proc ::autofile::autoclose {cmd args} {
    foreach file $::autofile::files($cmd) {
        puts "closing $file for $cmd"
        ::close $file
    }
}

namespace eval ::test {
    proc hi args {
        set f [open /dev/urandom]
        puts [binary encode base64 [read $f 4]]
    }
}

::test::hi

puts bye

See also