Tcl Part I · Module 9 · Loops and Iteration
foreach: Iterating Over a List
This module opens with foreach rather than for, and the ordering is deliberate. Most people arriving at Tcl from C or Java reach first for a counter, a limit and an index, and then spend the rest of the loop writing lindex to get at the thing they actually wanted. Tcl's foreach hands you the element directly. That removes the index variable, the bound, and the entire category of off-by-one mistakes that comes with them — and it has one more property, not obvious from the syntax, that guarantees the loop finishes even if the list is growing underneath it.
Foundation11 min readTclPart ILoopsforeachIteration
Part I · Module 9 · Chapter 9.1 · foreach: Iterating Over a List
1. The shape
set files {report.log data.csv notes.txt}
foreach file $files {
puts "processing $file"
}processing report.log
processing data.csv
processing notes.txtThree arguments: a variable name, a list, and a body. On each pass Tcl sets the variable to the next element and runs the body.
Note which arguments take a $. The variable name is bare — foreach file — because foreach is going to assign to it, the same rule as set. The list is a value, so it takes $files. That is the same distinction Chapter 7.2 §7 drew for dict set and dict get.
2. It is a command, with braces
Like if in Chapter 8.1, foreach is an ordinary command taking a script as an argument. So the body is braced for exactly the reasons Chapter 8.2 gave:
foreach x {1 2 3} {
puts $x
}The braces hand the body over unevaluated, so $x is looked up on each pass rather than once while the line is being parsed. A quoted body would be substituted once, before the loop ever started, and every pass would print the same thing.
3. No index, and why that matters
Here is the same job written the way a C habit suggests:
set files {report.log data.csv notes.txt}
for {set i 0} {$i < [llength $files]} {incr i} {
puts "processing [lindex $files $i]"
}processing report.log
processing data.csv
processing notes.txtIdentical output, and four more things that can go wrong: the initial value, the comparison (< versus <=), the increment, and the lindex call. Chapter 6.4 showed that lindex returns an empty string rather than erroring when the index is out of range — so an off-by-one here does not crash, it quietly processes one empty filename.
set files {a b c}
for {set i 0} {$i <= [llength $files]} {incr i} {
puts "'[lindex $files $i]'"
}'a'
'b'
'c'
''That trailing empty line is the bug, and nothing reported it. foreach cannot produce it.
If you want the element and its position, you do not have to give up foreach:
set files {report.log data.csv notes.txt}
set i 0
foreach file $files {
puts [format "%2d %s" $i $file]
incr i
} 0 report.log
1 data.csv
2 notes.txtOne counter you control, no bound to get wrong.
4. An empty list is fine
set n 0
foreach x {} {
incr n
}
puts "ran $n times"ran 0 timesZero passes, no error. This matters more than it sounds: a loop over a list that happened to come back empty simply does nothing, which is usually the correct behaviour and means you rarely need to guard the loop with a length check.
5. The loop variable outlives the loop
foreach q {1 2 3} {}
puts "q = $q"q = 3It holds the last element. This is the same as dict for in Chapter 7.5 §7 and it is occasionally handy — but relying on it is fragile, because an empty list leaves the variable untouched or unset, and the next read silently picks up a stale value from an earlier loop.
6. foreach walks a snapshot
The property that guarantees termination:
set l {a b}
foreach x $l {
lappend l zzz
puts "saw $x (list is now [llength $l] long)"
}
puts "final: $l"saw a (list is now 3 long)
saw b (list is now 4 long)
final: a b zzz zzzThe loop ran exactly twice. The appends really happened — the last line proves it — and the loop never saw them.
This is a direct consequence of foreach taking $l, a value, rather than the variable name. The value was handed over once, when the loop started. It is the same rule as dict for in Chapter 7.5 §6, and it is what you want: a loop that grew as you fed it would never finish.
7. foreach returns nothing
set r [foreach x {1 2 3} {expr {$x * 2}}]
puts "'[set r]'"''Not the list of results, not the last body's value — the empty string. foreach is run for its effect. If you want the results collected, that is Chapter 9.8 or the accumulator pattern in Chapter 9.9.
8. A worked example
Reporting on a list of records, using only what you have seen so far:
set ports {
{clk in 1}
{rst_n in 1}
{data in 32}
{valid out 1}
}
set total 0
set wide {}
foreach p $ports {
lassign $p name dir width
puts [format " %-6s %-4s %3d" $name $dir $width]
incr total $width
if {$width > 1} {
lappend wide $name
}
}
puts "total bits : $total"
puts "wide ports : $wide" clk in 1
rst_n in 1
data in 32
valid out 1
total bits : 35
wide ports : datalassign from Chapter 6.14 unpacks each record, and the loop body reads as a description of one row rather than as index arithmetic.
9. Exercises
Exercise 1. Why is the variable bare in foreach file $files while the list has a $?
Exercise 2. Rewrite this with foreach and say what can no longer go wrong:
for {set i 0} {$i < [llength $items]} {incr i} {
puts [lindex $items $i]
}Exercise 3. Predict the output:
set l {a b}
foreach x $l { lappend l q }
puts "[llength $l] elements"Exercise 4. What does set r [foreach x {1 2} {expr {$x}}] put in r?
Exercise 5. Write a loop printing each element of {alpha beta gamma} numbered from 1.
Exercise 6. A colleague's loop over an empty list crashes. Given that foreach over {} runs zero times without error, where is the bug more likely to be?
Answers
1. Because foreach assigns to the variable, so it needs the name — the same reason set x 1 takes a bare x. The list is a value it only reads, so it takes $files.
2.
foreach item $items {
puts $item
}Gone: a wrong starting index, a < that should be <= or vice versa, a missing or wrong increment, and an out-of-range lindex that silently yields an empty string instead of failing.
3. 4 elements. The loop ran twice over the snapshot {a b}, appending once each time, so the list ends with four elements while the loop saw only two.
4. The empty string. foreach always returns nothing.
5.
set i 1
foreach w {alpha beta gamma} {
puts "$i. $w"
incr i
}1. alpha
2. beta
3. gamma6. Not in the foreach itself — iterating an empty list is a no-op. The bug is almost certainly after the loop, in code that assumes the loop ran at least once: reading the loop variable, dividing by a count that is zero, or using lindex ... 0 on an empty accumulator.
10. Remember
foreach var $list {body}— bare variable name, list as a value, braced body.- It removes the index entirely, and with it the starting value, the bound, the increment and the
lindex. - An out-of-range
lindexreturns an empty string rather than erroring, so off-by-one in aforloop produces a silent blank rather than a crash. - An empty list runs the body zero times, with no error and no need to guard.
- The loop variable survives the loop, holding the last element — do not build on that.
foreachiterates a snapshot. Appending to the list inside the body does not extend the loop, so it always terminates.- Build changed lists in a second variable rather than mutating the one you are walking.
foreachreturns the empty string, never the results.
Where this fits
Part of the Tcl curriculum.
