cfile

SS 30Dec2004 - cfile is a object based interface for file I/O in Tcl; an alternative to the usual commands operating on channels. I wrote this code because I use chan a lot, and it makes more simple to write programs that have to deal with files, and is in general a very tclish way to do it (very similar to Tk).

The library exports two commands in the global namespace that are used to create file objects. The first command is called cfile, and it takes exactly the same arguments of the Tcl open command, but instead of returning a file descriptor, it returns the name of a command that can be used to perform operations on that file. Example:

% set f [cfile /etc/passwd]
::cfile::file1
% $f gets
root:x:0:0:root:/root:/bin/zsh

The first line of code opens the /etc/passwd file and creates the file object. The name of the file object is returned, and is used as a command in the second line, where the gets method is called.

An alternative way to create file objects is to call the cfileFromChannel command, with a Tcl channel as argument. This will create a file object from an already open channel (useful to work with stdin, stdout, stderr, and other kind of channels).

This is the list of available methods:

close - close the file and release the object associated
channel - return the Tcl channel of the file object
content - return the whole file content
gets ?varName? - like the gets command.
read ?numChars? - like the read command.
puts string - like the puts command.
write string - like the puts command with the -nonewline option.
foreach var body - execute body for every line of the file, assigning the value of the line to var at every iteration.
map var body - like the foreach method, but returns a list composed of the return values of body at every iteration.
lines - returns a Tcl list where every element is a line of the file
putlist list - write every element of list to the file using puts.
seek offset ?origin? - like the seek Tcl command
rewind - the same as seek 0
tell - like the tell Tcl command
blocking boolean - set/unset the file blocking mode
encoding value - set the file encoding to the specified value
translation value - set the file translation to the specified value
buffering value - set the file buffering to the specified value
flush - like the flush Tcl command
readable script - register a file readable event for the file, using the given script
writable script - register a file writable event for the file, using the given script
autoclean - automatically close the file when the current procedure exists

There is very little need to comment the methods, most things are the same of Tcl commands. The autoclean method makes the file scope similar to a local variable, when the procedure exists the file is closed and the relative object released.

Examples

source cfile.tcl

# Example 1: counts the number of lines inside /etc/passwd
set f [cfile /etc/passwd]
set lines [llength [$f lines]]
puts "/etc/passwd is composed of $lines lines"
$f close

# Example 2: put the whole file content into the $content variable
set f [cfile /etc/passwd]
set content [$f content]
puts "/etc/passwd contains [string length $content] characters"
$f close

# Example 3: counts the number of lines inside /etc/passwd matching */bin/sh*
set f [cfile /etc/passwd]
set matches 0
$f foreach line {
    if {[string match {*/bin/sh*} $line]} {
        incr matches
    }
}
$f close
puts "It contains $matches lines matching */bin/sh*"

# Example 4: Alternative of Example 3 using the 'map' method.
set f [cfile /etc/passwd]
set matches [expr [join [$f map line {string match {*/bin/sh*} $line}] +]]
puts "It contains $matches lines matching */bin/sh* (2)"
$f close

# Example 5: Sorts the lines of /etc/passwd and write the sorted
# version of the file in /tmp/sorted.txt
set f [cfile /etc/passwd]
set lines [lsort [$f lines]]
$f close
set f [cfile /tmp/sorted.txt w]
$f putlist $lines
$f close

# Example 6: Auto close a file when a procedure exits
proc foobar {} {
    set f [cfile /etc/passwd]
    $f autoclean
    # Do some work, possibly complex with handling of special coditions...
    # The $f file will be automatically closed when the procedure returns.
}
 
foobar

Implementation

# Cfile - Object based interface for file handling in Tcl.
# Copyright (C) 2004 Salvatore Sanfilippo <[email protected]>
# This software is released under the BSD license.
 
package provide cfile 0.1
 
namespace eval cfile {}
set ::cfile::id 0
array set ::cfile::fd {}
 
proc cfile {filename args} {
     switch -- [llength $args] {
        0 {set fd [open $filename]}
        1 {set fd [open $filename [lindex $args 0]]}
        2 {set fd [open $filename [lindex $args 0] [lindex $args 1]]}
        default {
            return -code error "wrong # of args for command"
        }
     }
     set id [incr ::cfile::id]
     set ::cfile::fd($id) $fd
     interp alias {} ::cfile::file$id {} ::cfile::__dispatch__ $id
}
 
proc cfileFromChannel channel {
     set id [incr ::cfile::id]
     set ::cfile::fd($id) $channel
     interp alias {} ::cfile::file$id {} ::cfile::__dispatch__ $id
}
 
proc ::cfile::__dispatch__ {id method args} {
     if {[info command ::cfile::__method__$method] eq {}} {
        return -code error "cfile: no such subcommand: '$method'"
     }
     set fd $::cfile::fd($id)
     uplevel 1 [list ::cfile::__method__$method $id $fd] $args
}
 
# Close the file
proc ::cfile::__method__close {id fd} {
     catch {close $fd}
     catch {unset ::cfile::fd($id)}
     catch {interp alias {} $::cfile::file$id {}}
}
 
# Return the file channel
proc ::cfile::__method__channel {id fd} {
     return $fd
}
 
# Return the whole content of the file.
proc ::cfile::__method__content {id fd} {
     catch {seek $fd 0}
     set buf [read $fd]
     return $buf
}
 
# method similar to the [gets] command.
proc ::cfile::__method__gets {id fd args} {
     if {![llength $args]} {
        gets $fd
     } else {
        upvar 1 [lindex $args 0] var
        gets $fd var
     }
}
 
# method similar to the [read] command.
proc ::cfile::__method__read {id fd {count {}}} {
     if {$count eq {}} {
        return [read $fd]
     } else {
        return [read $fd $count]
     }
}
 
# method similar to the [puts] command.
proc ::cfile::__method__puts {id fd buf} {
     puts $fd $buf
}
 
# method similar to the [puts] command with the -nonewline option
proc ::cfile::__method__write {id fd buf} {
     puts -nonewline $fd $buf
}
 
# method foreach:
# Execute a Tcl script for each line of file
proc ::cfile::__method__foreach {id fd var script} {
     catch {seek $fd 0}
     upvar 1 $var line
     while {[gets $fd line] != -1} {
        uplevel 1 $script
     }
}
 
# method map:
# Execute a Tcl script for each line of file, accumulate the result
# of every iteration into a list, returned as result.
proc ::cfile::__method__map {id fd var script} {
     catch {seek $fd 0}
     set result {}
     upvar 1 $var line
     while {[gets $fd line] != -1} {
        lappend result [uplevel 1 $script]
     }
     return $result
}
 
# method lines: return all the file lines as a Tcl list.
proc ::cfile::__method__lines {id fd} {
     catch {seek $fd 0}
     set result {}
     while {[gets $fd line] != -1} {
        lappend result $line
     }
     return $result
}
 
# method putlist: write every element of $list as a file line.
proc ::cfile::__method__putlist {id fd list} {
     foreach e $list {
        puts $fd $e
     }
}
 
# method similar to the [seek] command.
proc ::cfile::__method__seek {id fd offset {origin start}} {
     seek $fd $offset $origin
}
 
# method rewind: equivalent to "seek 0"
proc ::cfile::__method__rewind {id fd} {
     seek $fd 0
}
 
# method similar to the [tell] command.
proc ::cfile::__method__tell {id fd} {
     tell $fd
}
 
# method blocking: set the channel blocking mode on/off
proc ::cfile::__method__blocking {id fd val} {
     fconfigure $fd -blocking $val
}
 
# method encoding: set the channel encoding
proc ::cfile::__method__encoding {id fd val} {
     fconfigure $fd -encoding $val
}
 
# method translation: set the channel translation mode
proc ::cfile::__method__translation {id fd val} {
     fconfigure $fd -translation $val
}
 
# method buffering: set the channel buffering mode
proc ::cfile::__method__buffering {id fd val} {
     fconfigure $fd -buffering $val
}
 
# method flush: calls [flush] against the file channel
proc ::cfile::__method__flush {id fd} {
     flush $fd
}
 
# method readable: set a callback for the 'fileevent readable' event.
proc ::cfile::__method__readable {id fd script} {
     fileevent $fd readable $script
}
 
# method writable: set a callback for the 'fileevent writable' event.
proc ::cfile::__method__readable {id fd script} {
     fileevent $fd writable $script
}
 
# method to automatically free a cfile once the current procedure returns.
proc ::cfile::__method__autoclean {id fd} {
     set var __cfile__autoclean__$id
     uplevel 1 [list set $var {}]
     uplevel 1 [list trace add variable $var unset ::cfile::autocleanCallback]
}
 
# The trace callback handler for the 'autoclean' method.
proc ::cfile::autocleanCallback {name1 name2 op} {
     set id [string range $name1 21 end]
     ::cfile::file$id close
}

Discussion

Comments are very welcomed. This is new code that I hope to use many times in the future, so to improve it is one of my goal.

jcw - Looks very nice. I have often wished channels to be more object-like at the core level (one can always wrap 'em of course). For a different angle on the file/directory side of things, see also a little project called iohan, which brings dirs-of-files and many other collections into a Tcl setting. It address another issue than cfile, but it shows how there too one can take very simple OO-style APIs further towards implementation-independence.

RS feels reminded of Peter da Silva's stream extension discussed on Tcl 2.1 :) I like the lines and map methods - that would be nice to have in the core.

SS seems like that the very bad (IMHO) chan was adopted instead.

NEM Note that all of the useful "methods" of chan take the channel as first argument, allowing you to wrap an object-like sugar around these channels:

 proc chan: {c cmd args} { uplevel 1 [linsert $args 0 ::chan $cmd $c] }
 interp alias {} log {} chan: [open myapp.log w]
 log configure -buffering line -encoding utf-8
 log puts "INFO: this is a test..."
 ...
 log close

The only exceptions are the create and names sub-commands, which do not take a channel at all (and do not make sense as instance methods), and the -nonewline options to puts and read, which cannot be specified using this technique. Of course, this doesn't address the higher-level operations that cfile supports, but these can be added in tcllib or by further TIPs. See for instance a higher-level channel API for a start at this.