Tcl Part I · Module 12 · Files, Directories, and Paths
Channels: How Tcl Talks to a File
This is the first module where your script reaches outside the interpreter, and Tcl makes that easier than you might expect by having exactly one idea for it. A channel is a thing you can read from or write to — a file, the terminal, a pipe to another program. You have been using channels since Chapter 1 without being told: every puts you have written sent text to one. This chapter makes that explicit, shows how to get a channel of your own by opening a file, and establishes the obligation that comes with it, which is that a channel you open is a resource you have to give back.
Foundation11 min readTclPart IFilesChannelsI/O
Part I · Module 12 · Chapter 12.1 · Channels: How Tcl Talks to a File
1. You already have three
Tcl starts with three channels open:
puts [lsort [file channels]]stderr stdin stdoutstdin is where input arrives, stdout is normal output, and stderr is for errors. Every puts you have written since Chapter 1.7 has been writing to stdout — the command simply defaults to it when you do not say otherwise:
puts "this goes to stdout"
puts stdout "so does this, explicitly"
puts stderr "this goes to stderr"this goes to stdout
so does this, explicitly
this goes to stderrAll three lines appeared here because a terminal shows both streams, but they are separate: redirect one in a shell and the other still reaches you. That is the whole reason stderr exists.
2. Opening a file gives you another
set fp [open sample.txt w]
puts $fp "line one"
puts $fp "line two"
close $fp
puts "the file now holds [file size sample.txt] bytes"the file now holds 18 bytesopen takes a filename and a mode and returns a channel handle — a short token naming the channel it just created. Everything afterwards refers to the channel by that handle, not by the filename.
3. The same commands work on any channel
This is the payoff of having one idea. puts writes to a channel, whether that channel is a file or your terminal:
set fp [open sample.txt w]
puts stdout "to the terminal"
puts $fp "to the file"
close $fpto the terminalOnly one line appeared, because the other went into the file. Nothing about puts changed — only which channel it was given.
Reading is the same story. gets takes a channel and reads one line from it, and that channel can be stdin as easily as a file.
4. Modes
open takes a second argument saying what you intend to do:
| Mode | Meaning |
|---|---|
r | read; the file must exist. This is the default if you omit the mode. |
w | write; truncates an existing file, creates it if absent |
a | append; writes go to the end, creates it if absent |
r+ | read and write; the file must exist |
The mode is enforced. Writing to a channel opened for reading fails:
set fp [open sample.txt r]
puts $fp "nope"channel "file5" wasn't opened for writingThat file5 is the handle, and the number will differ for you — which is section 2's point arriving in an error message.
Chapter 12.5 deals with w versus a properly, because the truncation is more dangerous than it looks.
5. close is an obligation
Every channel you open must be closed. Three reasons, in increasing order of how likely they are to bite you:
Your output may not be on disk until you close. Writes are buffered — Chapter 12.6 demonstrates a file sitting at zero bytes while its data waits in memory.
A process can only have so many channels open at once. A loop that opens a file per iteration and never closes will eventually run out.
The file stays locked or incomplete for anything else trying to read it.
set fp [open sample.txt w]
puts $fp "important"
close $fpThree lines, and the third is not optional.
6. Making close reliable
If something between open and close raises an error, the close never runs. The shape that survives that uses catch — which you met in Chapter 7.3 §4:
proc first_line {path} {
set fp [open $path r]
set code [catch {
gets $fp line
set line
} result]
close $fp
if {$code} { return -code error $result }
return $result
}
set fp [open sample.txt w] ; puts $fp "the first line" ; close $fp
puts [first_line sample.txt]the first lineThe close runs whether the body succeeded or failed, and a failure is still reported. That is the pattern to reach for whenever a procedure opens something.
7. What a channel is not
Two clarifications worth having early.
A channel is not the file. It is your connection to it. Two channels can be open on the same file at once, each with its own position.
A handle is not a path. You cannot pass $fp to file size or file exists — those take filenames:
set path sample.txt
set fp [open $path r]
puts "size via the path : [file size $path]"
close $fpsize via the path : 15Keep the path in its own variable when you will need both.
8. A worked example
Copying a file, one line at a time, with both channels managed:
proc copy_file {from to} {
set in [open $from r]
set out [open $to w]
set n 0
while {[gets $in line] >= 0} {
puts $out $line
incr n
}
close $in
close $out
return $n
}
set fp [open source.txt w]
puts $fp "alpha"
puts $fp "beta"
puts $fp "gamma"
close $fp
puts "copied [copy_file source.txt dest.txt] lines"
puts "dest.txt is [file size dest.txt] bytes"copied 3 lines
dest.txt is 17 bytesTwo channels, two handles, two closes. The loop is the canonical reading form and Chapter 12.2 takes it apart properly.
9. Exercises
Exercise 1. Which three channels exist before your script does anything?
Exercise 2. What does open return, and why should you not hard-code the value?
Exercise 3. What is the difference between puts "x" and puts $fp "x"?
Exercise 4. What error do you get writing to a channel opened with mode r?
Exercise 5. Give three reasons to close a channel you opened.
Exercise 6. Why can you not pass a channel handle to file size?
Answers
1. stdin, stdout and stderr.
2. A channel handle — a token such as file5. The number depends on how many channels have been opened in that interpreter, so it varies between runs and machines. Keep the value open returned in a variable and use that.
3. puts "x" writes to stdout, which is the default channel. puts $fp "x" writes to the channel whose handle is in fp. The command is the same; only the destination differs.
4. channel "fileN" wasn't opened for writing, where N is whatever number that handle has.
5. Buffered output may not have reached the disk until you close; a process can only hold a limited number of open channels, so a loop that leaks them will eventually fail; and the file may be incomplete or locked for anything else reading it.
6. Because a handle names your connection to a file, not the file itself. file size and file exists take a path. Keep the path in its own variable if you need both.
10. Remember
- A channel is anything you can read from or write to — a file, the terminal, later a pipe.
stdin,stdoutandstderralready exist. Everyputsso far has written tostdout.open path modereturns a handle such asfile5; the number varies, so always keep it in a variable.- The same commands work on any channel —
puts stdoutandputs $fpdiffer only in destination. - Modes:
rread (must exist, the default),wwrite and truncate,aappend,r+read and write. - The mode is enforced —
channel "fileN" wasn't opened for writing. - Close what you opened, and do not close the three standard channels.
- Wrap
open…closeincatchso an error in between does not leak the channel. - A handle is not a path.
file sizetakes the filename, not the handle.
Where this fits
Part of the Tcl curriculum.
