Tcl Part I · Module 12 · Files, Directories, and Paths
Writing a File
Writing is the mirror of reading and the commands are ones you have used since the first module, so the mechanics take about a minute. What takes longer, and is worth the time, is one specific mistake: puts takes an optional channel as its first argument, so leaving it out is not a syntax error — it is a valid command that writes somewhere else. Your script runs, prints what looks like progress to the terminal, exits cleanly, and leaves behind a file that is empty or missing half its content. This chapter covers writing properly and then makes that failure happen on purpose, because recognising it in the wild is worth more than avoiding it in theory.
Foundation11 min readTclPart IFilesputsWriting
Part I · Module 12 · Chapter 12.4 · Writing a File
1. The three steps
set fp [open summary.txt w]
puts $fp "cycles 12000"
puts $fp "errors 0"
close $fp
puts "wrote [file size summary.txt] bytes"wrote 24 bytesMode w creates the file if it does not exist. puts with a channel as its first argument writes a line to that channel, adding the newline. close finishes.
Reading it back confirms it:
set fp [open summary.txt r]
puts -nonewline [read $fp]
close $fpcycles 12000
errors 02. The missing channel argument
Here is the failure the chapter exists for. puts takes the channel optionally, so omitting it is a perfectly valid command:
set fp [open report.txt w]
puts $fp "this line goes to the file"
puts "this line does not"
puts $fp "and this one does"
close $fp
puts "--- report.txt now contains: ---"
set fp [open report.txt r]
puts -nonewline [read $fp]
close $fpthis line does not
--- report.txt now contains: ---
this line goes to the file
and this one doesRead the output carefully. The middle line appeared on the terminal, mixed in with the script's other output, and the file has two lines where it should have three. Nothing failed. Nothing warned.
3. puts -nonewline
By default puts appends a line terminator. -nonewline suppresses it:
set fp [open joined.txt w]
puts -nonewline $fp "no newline here"
puts -nonewline $fp " -- so these run together"
puts $fp ""
close $fp
set fp [open joined.txt r]
puts [read -nonewline $fp]
close $fpno newline here -- so these run togetherNote the option comes before the channel. puts -nonewline $fp "text" is the order; the other way round is an error.
This is how you build a line in pieces — a table row assembled from several values, or a progress indicator that stays on one line.
4. Writing values that are not plain strings
Lists and dictionaries are strings (Chapter 6.1, Chapter 7.1), so writing one writes its text form:
set ports [list clk rst "data bus"]
set cfg [dict create mode fast runs 3]
set fp [open values.txt w]
puts $fp $ports
puts $fp $cfg
close $fp
set fp [open values.txt r]
puts -nonewline [read $fp]
close $fpclk rst {data bus}
mode fast runs 3The braces around data bus are the automatic quoting from Chapter 6.2, and they are what make the round trip work — read that line back and llength still says 3.
set fp [open values.txt r]
set firstline [gets $fp]
close $fp
puts "read back: [llength $firstline] elements, second is '[lindex $firstline 1]'"read back: 3 elements, second is 'rst'5. Copying is read plus write
gets strips the terminator and puts adds one back, so the pair is lossless for ordinary text:
set fp [open source.txt w]
foreach l {alpha beta gamma} { puts $fp $l }
close $fp
set in [open source.txt r]
set out [open copy.txt w]
while {[gets $in line] >= 0} {
puts $out $line
}
close $in
close $out
puts "source [file size source.txt] bytes, copy [file size copy.txt] bytes"source 17 bytes, copy 17 bytesIdentical sizes. The one case where it is not lossless is a file whose last line has no terminator — the copy will gain one.
6. Writing a report
Putting the formatting commands from Chapter 5.11 to work:
set rows {cycles 12000 errors 0 warnings 4}
set fp [open report.txt w]
puts $fp [string repeat "=" 32]
puts $fp "Run summary"
puts $fp [string repeat "-" 32]
set total 0
foreach {name value} $rows {
puts $fp [format " %-12s %8d" $name $value]
incr total $value
}
puts $fp [string repeat "-" 32]
puts $fp [format " %-12s %8d" "TOTAL" $total]
close $fp
set fp [open report.txt r]
puts -nonewline [read $fp]
close $fp================================
Run summary
--------------------------------
cycles 12000
errors 0
warnings 4
--------------------------------
TOTAL 12004Every puts in that block has $fp. Scan them and confirm it — that is the habit section 2 asked for.
7. Making the write safe
The same catch shape as Chapter 12.1 §6, now on the writing side, where it matters more: an error between open and close leaves a file that is partly written and a channel that is never released.
proc write_checked {path lines} {
set fp [open $path w]
set code [catch {
set n 0
foreach l $lines {
if {$l eq "BAD"} { error "refusing to write '$l'" }
puts $fp $l
incr n
}
set _ $n
} result]
close $fp
if {$code} { return -code error $result }
return $result
}
puts "good run : wrote [write_checked out.txt {one two three}] lines, file is [file size out.txt] bytes"
if {[catch {write_checked out.txt {one BAD three}} msg]} {
puts "bad run : $msg"
puts " file is now [file size out.txt] bytes"
}good run : wrote 3 lines, file is 14 bytes
bad run : refusing to write 'BAD'
file is now 4 bytesTwo things to read off that. The error was reported rather than swallowed, and close still ran — so the channel was not leaked. But the file is 4 bytes: mode w truncated it the moment it was opened, and only one had been written before the error. The previous good content is gone.
8. Exercises
Exercise 1. What are the three steps to write a file, and which mode creates it if absent?
Exercise 2. What happens if you write puts "text" instead of puts $fp "text" inside a writing block?
Exercise 3. Why does Tcl not report that as an error?
Exercise 4. Write a procedure that writes a list of lines to a path and returns how many it wrote.
Exercise 5. What does puts -nonewline $fp "a" followed by puts -nonewline $fp "b" put in the file?
Exercise 6. A script prints its progress to the terminal and leaves an empty output file. What is the first thing to check?
Answers
1. open with a write mode, puts $fp ... for each line, then close. Mode w creates the file if it does not exist (and truncates it if it does — Chapter 12.5).
2. The line goes to stdout — your terminal — instead of the file. The file simply does not receive it, and ends up short by one line.
3. Because the channel argument is optional. puts "text" is a complete, valid command meaning "write this to the default channel", which is stdout. Tcl cannot know you meant the file.
4.
proc write_lines {path lines} {
set fp [open $path w]
foreach l $lines { puts $fp $l }
close $fp
return [llength $lines]
}
puts [write_lines out.txt {alpha beta}]25. ab, with no newline between them and none at the end — both terminators were suppressed.
6. Whether every puts in the writing block has its $fp. Output appearing on the terminal that should have gone to the file is the signature of a missing channel argument.
9. Remember
open path wcreates the file,puts $fp textwrites a line,close $fpfinishes.- The channel argument to
putsis optional, so omitting it is valid and sends the line tostdout. - That failure is silent — the file comes out short and the missing lines appear on your terminal.
- Scan every
putsin a writing block for its$fp. The language cannot check your intent. puts -nonewline $fp textsuppresses the terminator; the option comes before the channel.- Lists and dictionaries write as their text form, with automatic quoting, so one record per line round-trips cleanly.
getsplusputsis lossless for ordinary text, because one strips the terminator and the other adds it back.- Wrap the writing block in
catchsoclosealways runs — an error part-way leaves a partial file.
Where this fits
Part of the Tcl curriculum.
