Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open summary.txt w]
puts $fp "cycles  12000"
puts $fp "errors  0"
close $fp

puts "wrote [file size summary.txt] bytes"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wrote 24 bytes

Mode 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open summary.txt r]
puts -nonewline [read $fp]
close $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cycles  12000
errors  0

2. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
this line does not
--- report.txt now contains: ---
this line goes to the file
and this one does

Read 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no newline here -- so these run together

Note 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clk rst {data bus}
mode fast runs 3

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open values.txt r]
set firstline [gets $fp]
close $fp
puts "read back: [llength $firstline] elements, second is '[lindex $firstline 1]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
source 17 bytes, copy 17 bytes

Identical 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
================================
Run summary
--------------------------------
  cycles          12000
  errors              0
  warnings            4
--------------------------------
  TOTAL           12004

Every 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
good run : wrote 3 lines, file is 14 bytes
bad run  : refusing to write 'BAD'
           file is now 4 bytes

Two 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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}]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
2

5. 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 w creates the file, puts $fp text writes a line, close $fp finishes.
  • The channel argument to puts is optional, so omitting it is valid and sends the line to stdout.
  • That failure is silent — the file comes out short and the missing lines appear on your terminal.
  • Scan every puts in a writing block for its $fp. The language cannot check your intent.
  • puts -nonewline $fp text suppresses 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.
  • gets plus puts is lossless for ordinary text, because one strips the terminator and the other adds it back.
  • Wrap the writing block in catch so close always runs — an error part-way leaves a partial file.

Where this fits

Part of the Tcl curriculum.