Intermediate

I/O Operations in Tcl

Master input and output in Tcl - formatted console output, reading from stdin, and file channels with open, read, gets, puts, and close

Input and output in Tcl revolve around a single elegant abstraction: the channel. The console, files, pipes, and sockets are all channels, and the same small set of commands—puts, gets, read, and close—works on all of them. Once you learn to read and write a file, you already know how to talk to a network socket.

This uniformity is a direct consequence of Tcl’s “everything is a string” philosophy. Data flowing through a channel is just text (unless you ask for binary mode), so there’s no distinction between “serializing” data and simply writing it. Combined with Tcl’s event-driven heritage, channels can even be read asynchronously with fileevent—the same mechanism that powers Tk GUIs and network servers.

In this tutorial you’ll go beyond the puts you met in Hello World: formatting output precisely, reading user input from stdin, writing and reading files in several modes, and handling I/O errors gracefully with catch.

Formatted Console Output

You already know puts writes a line to standard output. It has more tricks: it can suppress the trailing newline, write to any channel (including stderr), and pair with the format command for printf-style precision.

Create a file named io_console.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Formatted console output in Tcl
puts "A plain line from puts"

# -nonewline suppresses the trailing newline
puts -nonewline "These two pieces... "
puts "join on one line"

# format works like C's printf and returns a string
set language "Tcl"
set year 1988
puts [format "%-8s appeared in %d" $language $year]

# Width, precision, and zero-padding
puts [format "Pi is roughly %8.4f" 3.14159265]
puts [format "Hex: %x  Octal: %o  Padded: %05d" 255 8 42]

# puts accepts a channel name; stderr is always open
puts stderr "Diagnostics go to standard error"

The first argument to format is a template; %s, %d, %f, %x, and friends are filled from the remaining arguments. Because format simply returns a string, you can use it anywhere a string is expected—not just in puts.

Reading from Standard Input

Standard input is the pre-opened channel stdin. The gets command reads one line from a channel; with two arguments it stores the line in a variable and returns the character count (or -1 at end of file).

Create a file named io_input.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Reading user input from stdin
puts -nonewline "What is your name? "
flush stdout
set name [gets stdin]

puts -nonewline "What year were you born? "
flush stdout
set born [gets stdin]

puts "Hello, $name!"
puts "You turn [expr {2026 - $born}] this year."

Two details matter here. First, flush stdout forces the prompt to appear before the program blocks waiting for input—output to stdout is line-buffered, and the prompt has no newline. Second, [gets stdin] with one argument returns the line directly, which is convenient for simple scripts; the two-argument form (gets stdin name) is preferred inside loops because its return value signals end of file.

Because everything in Tcl is a string, no conversion step is needed before doing arithmetic on $bornexpr interprets the string as a number when the context demands it.

File I/O with Channels

Files use the same commands as the console. The open command returns a channel handle, which you pass to puts, gets, or read, and finally to close. The second argument to open is the access mode: r to read, w to write (truncating), and a to append.

Create a file named io_files.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File I/O in Tcl: write, append, read, and handle errors

# Open for writing ("w" creates or truncates the file)
set out [open "journal.txt" w]
puts $out "First entry"
puts $out "Second entry"
close $out

# Open for appending ("a" adds to the end)
set out [open "journal.txt" a]
puts $out "Third entry"
close $out

# Slurp the whole file at once with read
set in [open "journal.txt" r]
set contents [read $in]
close $in
puts "--- Whole file ---"
puts -nonewline $contents

# Read line by line: gets returns -1 at end of file
puts "--- Line by line ---"
set in [open "journal.txt" r]
set lineno 0
while {[gets $in line] >= 0} {
    incr lineno
    puts "$lineno: $line"
}
close $in

# I/O errors raise exceptions; catch traps them
if {[catch {open "missing.txt" r} err]} {
    puts "Could not open file: $err"
}

# The file command inspects and manipulates the filesystem
file delete journal.txt
puts "File exists after delete: [file exists journal.txt]"

The while {[gets $in line] >= 0} idiom is the standard Tcl pattern for processing a file line by line without loading it all into memory. The catch command traps the error raised when open fails and stores the message in err, letting the script recover instead of dying. Finally, the file command family (file exists, file delete, file size, file mkdir, and many more) handles filesystem operations that don’t involve reading content.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Pull the Tcl image
docker pull efrecon/tcl:latest

# Run the console output example
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/io_console.tcl

# Run the stdin example (-i keeps stdin open for interactive input)
docker run --rm -i -v $(pwd):/app -w /app efrecon/tcl:latest /app/io_input.tcl

# Run the file I/O example (the mounted volume lets it create journal.txt)
docker run --rm -v $(pwd):/app -w /app efrecon/tcl:latest /app/io_files.tcl

Note the -i flag on the second command—it connects your terminal’s stdin to the container so gets stdin can read what you type.

Expected Output

Running io_console.tcl (stderr and stdout both appear in the terminal):

A plain line from puts
These two pieces... join on one line
Tcl      appeared in 1988
Pi is roughly   3.1416
Hex: ff  Octal: 10  Padded: 00042
Diagnostics go to standard error

Running io_input.tcl (a sample session; entering Ada and 1990):

What is your name? Ada
What year were you born? 1990
Hello, Ada!
You turn 36 this year.

Running io_files.tcl:

--- Whole file ---
First entry
Second entry
Third entry
--- Line by line ---
1: First entry
2: Second entry
3: Third entry
Could not open file: couldn't open "missing.txt": no such file or directory
File exists after delete: 0

Key Concepts

  • Everything is a channel - stdin, stdout, stderr, files, pipes, and sockets all use the same commands: puts, gets, read, and close.
  • puts writes, gets reads lines - Both take an optional channel argument; puts defaults to stdout, and gets returns -1 at end of file, which drives the standard while {[gets $ch line] >= 0} loop.
  • format handles precision - Printf-style templates (%s, %d, %8.4f, %05d) return a string you can use anywhere, not just for printing.
  • Flush prompts before reading - Stdout is buffered, so call flush stdout after a puts -nonewline prompt or the user may never see it.
  • open modes mirror C - r reads, w truncates and writes, a appends; open returns a handle that must eventually be passed to close.
  • read slurps, gets iterates - Use read $ch for small files you want whole, and a gets loop for large files or record-by-record processing.
  • Trap I/O errors with catch - Failed opens raise Tcl exceptions; catch {open ...} err captures the message so your script can recover.
  • The file command manages the filesystem - file exists, file delete, file size, and relatives cover metadata and manipulation without opening a channel.

Running Today

All examples can be run using Docker:

docker pull efrecon/tcl:latest
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining