Tcl Part I · Module 9 · Loops and Iteration
break and continue
Two commands that change how a loop runs: one abandons it, the other abandons just the current pass. They are simple, and the interesting part is not the mechanism but what you do with it. A loop body that filters out several uninteresting cases before doing its real work can be written as nested conditions wrapping the work, or as a short run of guards that skip onward — the same choice Chapter 8.3 made for conditions, and the same answer. This chapter also pins down one behaviour that differs between loop types and has hung more scripts than any other single mistake.
Foundation10 min readTclPart ILoopsbreakcontinue
Part I · Module 9 · Chapter 9.6 · break and continue
1. The two commands
break abandons the loop entirely:
foreach x {1 2 3 4 5} {
if {$x == 3} break
puts $x
}
puts "done"1
2
donecontinue abandons only the current pass:
foreach x {1 2 3 4 5} {
if {$x % 2 == 0} continue
puts $x
}1
3
5Both work in foreach, for and while:
for {set i 0} {$i < 10} {incr i} {
if {$i == 3} break
}
puts "for stopped at i=$i"
set n 0
while {1} {
incr n
if {$n >= 4} break
}
puts "while stopped at n=$n"for stopped at i=3
while stopped at n=42. continue behaves differently in for and while
This is the one to internalise. In a for, the next script runs even when the body hits continue:
set seen {}
for {set i 0} {$i < 5} {incr i} {
if {$i == 2} continue
lappend seen $i
}
puts "seen = $seen, i = $i"seen = 0 1 3 4, i = 5The loop skipped 2 and still advanced and still finished, because incr i lives in for's own argument rather than in the body.
Write the same logic as a while with the increment at the bottom and it hangs:
set i 0
while {$i < 5} {
if {$i == 2} continue ;# never reaches the incr
lappend seen $i
incr i
}continue jumps straight back to the test, incr i is never reached, and i stays at 2 forever.
3. continue as a guard
Here is the readability argument. A loop that must ignore several kinds of line can be written with the work nested inside the tests:
set lines {"# a comment" "" "width = 32" " " "depth = 16" "# end"}
foreach line $lines {
if {[string trim $line] ne ""} {
if {![string match "#*" [string trim $line]]} {
if {[string match "*=*" $line]} {
puts "setting: [string trim $line]"
}
}
}
}setting: width = 32
setting: depth = 16Or with guards that skip onward, leaving the work at the bottom and unindented:
foreach line $lines {
set t [string trim $line]
if {$t eq ""} continue
if {[string match "#*" $t]} continue
if {![string match "*=*" $t]} continue
puts "setting: $t"
}setting: width = 32
setting: depth = 16Same output. The second version puts each rejection next to its reason, computes string trim once instead of three times, and ends with the real work on one unindented line.
4. break for a search
break is at its clearest when you are looking for something and the rest of the list stops mattering once you find it:
set ports {clk rst data valid ready}
set want data
set found ""
foreach p $ports {
if {$p eq $want} {
set found $p
break
}
}
puts [expr {$found eq "" ? "not found" : "found $found"}]found dataInside a procedure, return is usually better still, because it says "we are finished" rather than "stop looping and then fall through to whatever is next":
proc find_port {ports want} {
foreach p $ports {
if {$p eq $want} { return $p }
}
return ""
}
puts "[find_port {clk rst data} data] / [find_port {clk rst} data]<- empty"data / <- empty5. They affect only the innermost loop
set table {{1 2} {3 4} {5 6}}
foreach row $table {
foreach cell $row {
if {$cell == 3} break
puts -nonewline "$cell "
}
}
puts ""1 2 5 6The break ended the inner loop for the row containing 3, and the outer loop carried straight on to the next row. That is why 5 and 6 still appear. Chapter 9.10 deals with leaving an outer loop.
6. break inside a switch breaks the loop
Worth checking if you come from C, where break is how you end a switch case:
foreach x {a b c} {
switch -- $x {
a { puts "saw a" }
b { break }
c { puts "saw c" }
}
}
puts "after the loop"saw a
after the loopThe break ended the loop, not the switch. Tcl's switch never falls through (Chapter 8.4 §2), so it has no use for break and does not absorb it. c was never reached.
7. A worked example
Scanning a log until a terminator, skipping noise, and reporting:
set log {
"INFO starting"
"# comment"
"WARN clock skew"
""
"ERROR missing cell"
"INFO continuing"
"END"
"INFO never reached"
}
set problems 0
set scanned 0
foreach line $log {
set t [string trim $line]
if {$t eq ""} continue
if {[string match "#*" $t]} continue
if {$t eq "END"} break
incr scanned
if {[string match "WARN*" $t] || [string match "ERROR*" $t]} {
incr problems
puts " $t"
}
}
puts "scanned $scanned lines, $problems problems" WARN clock skew
ERROR missing cell
scanned 4 lines, 2 problemsTwo guards, one terminator, and the body's real work is three lines at the bottom.
8. Exercises
Exercise 1. What does each print?
foreach x {1 2 3} { if {$x == 2} break ; puts $x }
foreach x {1 2 3} { if {$x == 2} continue ; puts $x }Exercise 2. Rewrite with guards:
foreach v $values {
if {$v ne ""} {
if {[string is integer -strict $v]} {
puts $v
}
}
}Exercise 3. Why does continue hang a while that increments at the bottom but not a for?
Exercise 4. Predict the output and explain:
foreach x {a b c} {
switch -- $x { a {puts A} b {break} c {puts C} }
}Exercise 5. Write a loop finding the first element of {4 9 16 25} greater than 10, using break. Then rewrite it as a procedure using return.
Exercise 6. In a nested pair of loops, a break in the inner loop does not end the outer one. Give one way to end both.
Answers
1. The first prints 1. The second prints 1 then 3.
2.
foreach v $values {
if {$v eq ""} continue
if {![string is integer -strict $v]} continue
puts $v
}3. In a for, the advance lives in the loop's own next-script argument, which runs even when the body hits continue. In a while, the advance is just another command in the body, so continue jumps back to the test before reaching it and the variable never changes.
4. It prints A and nothing else. The break in the b case ends the enclosing foreach, not the switch — Tcl's switch has no fallthrough and so no use for break. The c pass never happens.
5.
set found ""
foreach n {4 9 16 25} {
if {$n > 10} { set found $n ; break }
}
puts $found
proc first_over {list limit} {
foreach n $list {
if {$n > $limit} { return $n }
}
return ""
}
puts [first_over {4 9 16 25} 10]16
166. Set a flag in the inner loop and test it at the top of the outer one, or — usually better — move the nested loops into a procedure and return from the inner one, which leaves both at once.
9. Remember
breakabandons the loop;continueabandons the current pass. Both work inforeach,forandwhile.continuein aforstill runs the next script, so the loop advances.continuein awhiledoes not run anything at the bottom of the body — advance before thecontinue, or usefor. This is the commonest cause of a hung script.- Use
continueas a guard to keep loop bodies flat: reject the uninteresting cases first, leave the real work unindented at the bottom. - That is the loop version of Chapter 8.3's guard clause.
- Inside a procedure, prefer
returntobreakwhen finding something ends the work. - They affect only the innermost loop.
breakinside aswitchends the enclosing loop, not the switch — Tcl'sswitchnever falls through, so it has no use for it.
Where this fits
Part of the Tcl curriculum.
