Beginner

Control Flow in Tcl

Learn conditionals, loops, and branching in Tcl - if/elseif/else, switch, for, while, and foreach with Docker-ready examples

Control flow determines the order in which your program executes. Tcl approaches control flow in a way that surprises newcomers: there are no special control-flow keywords. Instead, if, while, for, foreach, and switch are ordinary commands—just like puts or set. They happen to take blocks of code (held in braces) as arguments.

This is a direct consequence of Tcl’s design philosophy: everything is a command. When you write if {$x > 5} { ... }, you are calling the if command with two arguments: an expression string and a script string. Understanding this makes Tcl’s quoting rules feel natural rather than arbitrary—the braces aren’t syntax, they are just a way to pass an unevaluated block to a command.

In this tutorial you’ll learn how Tcl handles conditionals (if/elseif/else), multi-way branching (switch), counted and conditional loops (for, while, foreach), and loop control (break, continue). A key idiom you’ll see throughout: conditions belong in braces so that expr can compile and evaluate them efficiently.

Conditionals with if/elseif/else

The if command evaluates an expression and runs a script block if it’s true. The condition is wrapped in braces so Tcl passes it to the expression evaluator unmolested.

Create a file named conditionals.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
# Conditionals in Tcl
set score 82

if {$score >= 90} {
    puts "Grade: A"
} elseif {$score >= 80} {
    puts "Grade: B"
} elseif {$score >= 70} {
    puts "Grade: C"
} else {
    puts "Grade: F"
}

# String comparison uses eq/ne (or the string compare command)
set lang "tcl"
if {$lang eq "tcl"} {
    puts "Everything is a string!"
}

# Logical operators: && || !
set age 25
set has_ticket 1
if {$age >= 18 && $has_ticket} {
    puts "Entry allowed"
}

A few things worth noting. The braces around {$score >= 90} are important: they let Tcl’s expr engine compile the expression once and substitute $score itself, which is both faster and safer than letting the interpreter substitute first. For string equality, Tcl provides the eq and ne operators, which compare values as strings regardless of how they look—use these instead of == when you mean “are these the same text.”

Multi-way branching with switch

When you need to compare one value against many possibilities, switch is cleaner than a chain of elseif branches. By default switch matches exact strings.

Create a file named switch_demo.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
# Multi-way branching with switch
set day "Wed"

switch $day {
    "Mon" -
    "Tue" -
    "Wed" -
    "Thu" -
    "Fri" {
        puts "$day is a weekday"
    }
    "Sat" -
    "Sun" {
        puts "$day is the weekend"
    }
    default {
        puts "Unknown day: $day"
    }
}

# Glob-style matching with -glob
set filename "report.txt"
switch -glob $filename {
    "*.txt" { puts "Text file" }
    "*.tcl" { puts "Tcl script" }
    default { puts "Unknown type" }
}

The dash (-) after a pattern is the fall-through marker: it tells switch to use the next pattern’s body. This is how Tcl groups several cases under one action—here all five weekday names share the same block. The default pattern catches anything that didn’t match. With the -glob option, patterns become wildcard matches (* matches any characters), and -regexp enables full regular expressions.

Counted loops with for

The for command takes four arguments: an initialization script, a test expression, a “next” script, and the loop body. It mirrors C’s for loop but, again, it’s just a command.

Create a file named for_loop.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Counted loop
for {set i 1} {$i <= 5} {incr i} {
    puts "Iteration $i"
}

# Counting down with a step
puts "Countdown:"
for {set n 10} {$n > 0} {incr n -2} {
    puts "  $n"
}

The incr command increments a variable in place; incr n -2 decrements by 2. Because each of the four parts is a separate script argument, you have full freedom—the initialization and step parts can contain any commands, not just simple assignments.

Conditional loops with while

A while loop repeats as long as its condition expression stays true. Keep the condition in braces so it’s re-evaluated each pass.

Create a file named while_loop.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# While loop: sum numbers until we exceed 20
set total 0
set i 1
while {$total <= 20} {
    incr total $i
    incr i
}
puts "Stopped at total = $total"

# break and continue
puts "Odd numbers under 10:"
set k 0
while {1} {
    incr k
    if {$k >= 10} {
        break
    }
    if {$k % 2 == 0} {
        continue
    }
    puts "  $k"
}

This shows the two loop-control commands. break exits the innermost loop immediately, and continue skips to the next iteration. The while {1} idiom creates an infinite loop that you exit explicitly with break—a common pattern when the exit condition is easier to express in the middle of the body.

Iterating with foreach

Tcl’s foreach is its most idiomatic loop. Since lists are a native Tcl data type (any whitespace-separated string is a list), foreach walks through their elements directly—and it can even iterate over multiple variables at once.

Create a file named foreach_loop.tcl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Iterate over a list
set fruits {apple banana cherry}
foreach fruit $fruits {
    puts "Fruit: $fruit"
}

# Iterate over two values per step
puts "Pairs:"
foreach {name age} {Alice 30 Bob 25 Carol 41} {
    puts "  $name is $age"
}

# Parallel iteration over two lists
set ids {101 102 103}
set users {root admin guest}
foreach id $ids user $users {
    puts "ID $id -> $user"
}

The first loop is the everyday case: one variable, one list. The second form takes two loop variables in braces, so foreach pulls two elements off the list per iteration—handy for flat key/value lists. The third form pairs up multiple lists, advancing through all of them together. This flexibility is why experienced Tcl programmers reach for foreach far more often than for.

Running with Docker

Run any of the examples above using the official Tcl image—no local installation required.

1
2
3
4
5
6
7
8
# Pull the official image
docker pull efrecon/tcl:latest

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

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

Swap in any filename from this tutorial as the final argument to run that script.

Expected Output

Running conditionals.tcl:

Grade: B
Everything is a string!
Entry allowed

Running switch_demo.tcl:

Wed is a weekday
Text file

Running for_loop.tcl:

Iteration 1
Iteration 2
Iteration 3
Iteration 4
Iteration 5
Countdown:
  10
  8
  6
  4
  2

Running while_loop.tcl:

Stopped at total = 21
Odd numbers under 10:
  1
  3
  5
  7
  9

Running foreach_loop.tcl:

Fruit: apple
Fruit: banana
Fruit: cherry
Pairs:
  Alice is 30
  Bob is 25
  Carol is 41
ID 101 -> root
ID 102 -> admin
ID 103 -> guest

Key Concepts

  • Control structures are commandsif, while, for, foreach, and switch are ordinary commands that take script blocks as arguments, not special syntax.
  • Brace your conditions — Always wrap test expressions in { } (e.g. while {$x < 10}). This lets Tcl’s expr engine compile and evaluate them efficiently and avoids substitution surprises.
  • Use eq/ne for strings — Compare text with the eq and ne operators (or string compare); reserve == and != for numeric comparisons.
  • switch fall-through uses - — A dash after a pattern shares the next pattern’s body, grouping multiple cases under one action. Add -glob or -regexp for wildcard or regex matching.
  • foreach is the idiomatic loop — It iterates lists directly, supports multiple loop variables per step, and can walk several lists in parallel—usually clearer than an index-based for.
  • break and continue control loopsbreak exits the innermost loop; continue jumps to the next iteration. Pair them with while {1} for mid-body exits.
  • incr mutates in placeincr i adds 1; incr i -2 subtracts 2; incr total $i adds an arbitrary amount.

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