Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set files {report.log data.csv notes.txt}

foreach file $files {
    puts "processing $file"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
processing report.log
processing data.csv
processing notes.txt

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set files {report.log data.csv notes.txt}

for {set i 0} {$i < [llength $files]} {incr i} {
    puts "processing [lindex $files $i]"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
processing report.log
processing data.csv
processing notes.txt

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set files {a b c}
for {set i 0} {$i <= [llength $files]} {incr i} {
    puts "'[lindex $files $i]'"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set files {report.log data.csv notes.txt}
set i 0
foreach file $files {
    puts [format "%2d  %s" $i $file]
    incr i
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 0  report.log
 1  data.csv
 2  notes.txt

One counter you control, no bound to get wrong.

4. An empty list is fine

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set n 0
foreach x {} {
    incr n
}
puts "ran $n times"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ran 0 times

Zero 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach q {1 2 3} {}
puts "q = $q"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
q = 3

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set l {a b}

foreach x $l {
    lappend l zzz
    puts "saw $x (list is now [llength $l] long)"
}

puts "final: $l"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
saw a (list is now 3 long)
saw b (list is now 4 long)
final: a b zzz zzz

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set r [foreach x {1 2 3} {expr {$x * 2}}]
puts "'[set r]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
''

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  clk    in     1
  rst_n  in     1
  data   in    32
  valid  out    1
total bits : 35
wide ports : data

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
for {set i 0} {$i < [llength $items]} {incr i} {
    puts [lindex $items $i]
}

Exercise 3. Predict the output:

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set i 1
foreach w {alpha beta gamma} {
    puts "$i. $w"
    incr i
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. alpha
2. beta
3. gamma

6. 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 lindex returns an empty string rather than erroring, so off-by-one in a for loop 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.
  • foreach iterates 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.
  • foreach returns the empty string, never the results.

Where this fits

Part of the Tcl curriculum.