Tcl Part I · Module 12 · Files, Directories, and Paths
Append Mode, and Why the Mode Matters
There are two ways to open a file for writing and the difference is not a detail. One of them starts from empty and one of them starts from whatever is already there, which is the obvious part. The part worth a chapter is when the emptying happens: not when you write, but at the instant you open. A script that opens a log with the wrong mode and then takes an early return, or hits an error, or simply finds nothing to report, has already destroyed the previous contents — without writing a byte and without reporting anything.
Foundation10 min readTclPart IFilesAppendModes
Part I · Module 12 · Chapter 12.5 · Append Mode, and Why the Mode Matters
1. The two modes
# w — start from empty
set fp [open notes.txt w]
puts $fp "first run"
close $fp
# w again — starts from empty again
set fp [open notes.txt w]
puts $fp "second run"
close $fp
set fp [open notes.txt r] ; puts -nonewline [read $fp] ; close $fpsecond runThe first run's line is gone. Now the same sequence with a:
set fp [open log.txt w]
puts $fp "first run"
close $fp
set fp [open log.txt a]
puts $fp "second run"
close $fp
set fp [open log.txt r] ; puts -nonewline [read $fp] ; close $fpfirst run
second runa positions every write at the end of whatever is already there.
2. w truncates at open, not at write
This is the fact to take away. Watch the file's size across the two commands:
set fp [open data.txt w]
puts $fp "twenty-two bytes here"
close $fp
puts "before : [file size data.txt] bytes"
set fp [open data.txt w]
puts "just opened : [file size data.txt] bytes"
close $fp
puts "after close : [file size data.txt] bytes"before : 22 bytes
just opened : 0 bytes
after close : 0 bytesNothing was written between the open and the close. The file was emptied anyway, the instant it was opened.
3. a creates the file too
Append mode does not require the file to exist:
puts "exists before : [file exists fresh.log]"
set fp [open fresh.log a]
puts $fp "first entry"
close $fp
puts "exists after : [file exists fresh.log], [file size fresh.log] bytes"exists before : 0
exists after : 1, 12 bytesSo a is safe as a first write as well as a subsequent one. A logging procedure never needs to check whether the log exists yet.
4. Choosing
| You want | Mode |
|---|---|
| A file that reflects only this run — a report, a generated output | w |
| A record that accumulates across runs — a log, an audit trail | a |
| To add to a file you did not create | a |
| To be sure stale content cannot survive | w |
The question to ask is whether the previous contents are yours to destroy. A report you generate every run is; a log someone may be tailing is not.
5. A logging procedure
The canonical shape, which opens and closes per call:
proc log_line {path text} {
set fp [open $path a]
puts $fp $text
close $fp
}
file delete -force run.log
log_line run.log "started"
log_line run.log "step one complete"
log_line run.log "finished"
set fp [open run.log r] ; puts -nonewline [read $fp] ; close $fpstarted
step one complete
finishedOpening and closing for every line is more work than holding the channel open, and it has two advantages that usually outweigh that: the data is on disk after every call (Chapter 12.6 explains why that is not automatic), and the script can be interrupted at any point without losing the log.
For a hot loop writing thousands of lines, hold the channel open instead and flush periodically.
6. r+ and the modes you will rarely need
open accepts a few more:
| Mode | Meaning |
|---|---|
r | read only; must exist (the default) |
w | write; truncate or create |
a | append; create if needed |
r+ | read and write; must exist; does not truncate |
w+ | read and write; truncates |
a+ | read and append; creates if needed |
r+ is the one worth knowing about, because it is the only way to open an existing file for writing without emptying it:
set fp [open existing.txt w] ; puts $fp "original content" ; close $fp
puts "before : [file size existing.txt] bytes"
set fp [open existing.txt r+]
puts "with r+: [file size existing.txt] bytes (not truncated)"
close $fpbefore : 17 bytes
with r+: 17 bytes (not truncated)In practice, modifying a file in place is rarely what you want — reading it, changing the data, and writing a new file is easier to get right and easier to recover from. Chapter 12.4 §7's write-to-temporary-then-rename is the shape to reach for.
7. A worked example
A script that keeps a log and regenerates a report, using the right mode for each:
file delete -force build.log summary.txt
proc note {text} {
set fp [open build.log a] ;# accumulates
puts $fp $text
close $fp
}
proc write_summary {rows} {
if {[llength $rows] == 0} { return 0 }
set fp [open summary.txt w] ;# replaced each run
foreach {name value} $rows {
puts $fp [format "%-10s %s" $name $value]
}
close $fp
return [expr {[llength $rows] / 2}]
}
note "run started"
set n [write_summary {cycles 12000 errors 0}]
note "summary written: $n rows"
note "run finished"
puts "--- build.log ---"
set fp [open build.log r] ; puts -nonewline [read $fp] ; close $fp
puts "--- summary.txt ---"
set fp [open summary.txt r] ; puts -nonewline [read $fp] ; close $fp--- build.log ---
run started
summary written: 2 rows
run finished
--- summary.txt ---
cycles 12000
errors 0The log grows; the summary is replaced. And write_summary checks before opening, so a run with nothing to report leaves the previous summary intact.
8. Exercises
Exercise 1. What is the difference between modes w and a?
Exercise 2. At what moment does w empty an existing file?
Exercise 3. What is wrong with this, and what is the fix?
proc write_report {path rows} {
set fp [open $path w]
if {[llength $rows] == 0} { close $fp ; return "nothing to do" }
...
}Exercise 4. Does mode a require the file to exist?
Exercise 5. Write a log_line procedure that appends one line to a path.
Exercise 6. Which mode opens an existing file for writing without truncating it, and why is it usually not the right answer anyway?
Answers
1. w starts from an empty file, discarding any previous contents. a positions writes at the end of whatever is already there, so the file grows. Both create the file if it does not exist.
2. At open, before any puts runs. A script that opens with w and then writes nothing still leaves the file empty.
3. The open happens before the emptiness check, so a call with no rows truncates the file and then returns "nothing to do" — destroying the previous report while claiming to have done nothing. The fix is to check first and open only once there is something to write.
4. No. a creates the file if it is absent, which is why a logging procedure never has to check.
5.
proc log_line {path text} {
set fp [open $path a]
puts $fp $text
close $fp
}6. r+. It is usually not the right answer because modifying a file in place is harder to get right than reading it, changing the data in memory, and writing a fresh file — and a failure part-way through an in-place edit leaves the file in a state that is neither the old one nor the new one.
9. Remember
wstarts from empty;aadds to the end. Both create the file if absent.wtruncates atopen, not at the first write. A path that opens withwand writes nothing still empties the file.- So every early return and every error after an
open ... wdestroys the previous contents — open only once you know you have something to write. acreates the file too, so a logging procedure never needs an existence check.- Ask whether the previous contents are yours to destroy. A generated report, yes; a shared log, no.
- The per-call open/append/close shape is the standard logger — slower, and the data is on disk after every line.
r+opens an existing file for writing without truncating, and in-place editing is usually worse than writing a new file and renaming it.
Where this fits
Part of the Tcl curriculum.
