Tcl · Module 15
The env Array: Reading the Environment
The environment is how a program gets configuration it should not have to be told every time. Tcl hands it to you as an ordinary-looking array with one unusual property.
Foundation16 min readtclenvenvironmentconfigurationscriptsdefaults
Part I · Module 15 · Chapter 15.5 · The env Array: Reading the Environment
Some settings do not belong on the command line. An output directory that is the same for every run on this machine, a verbosity level a team has standardised on, a scratch location that differs between a laptop and a build server — typing those into every invocation is noise.
The environment is where they live, and Tcl gives you the whole of it as one array.
1. It is just an array
puts "array exists env : [array exists env]"
puts "REPORT_DIR set? : [info exists env(REPORT_DIR)]"
if {[info exists env(REPORT_DIR)]} {
puts "REPORT_DIR : $env(REPORT_DIR)"
} else {
puts "using default : ./reports"
}$ tclsh basics.tcl
array exists env : 1
REPORT_DIR set? : 0
using default : ./reports
$ REPORT_DIR=/var/reports tclsh basics.tcl
array exists env : 1
REPORT_DIR set? : 1
REPORT_DIR : /var/reportsenv is a global array whose keys are environment variable names. Everything you know about arrays from Chapter 7.9 applies: $env(NAME) reads one, array names env lists them, info exists env(NAME) tests one.
Inside a procedure you need $::env(NAME) or a global env declaration, because env is global like any other global (Chapter 11.2).
2. Always check before you read
The registry purpose for this chapter puts it plainly: test for presence so a missing variable is a clear message rather than an empty string.
# brittle
set reportDir $env(REPORT_DIR)
# fine — a default
set reportDir "./reports"
if {[info exists env(REPORT_DIR)]} {
set reportDir $env(REPORT_DIR)
}
# fine — a requirement
if {![info exists env(REPORT_DIR)]} {
error "REPORT_DIR is not set"
}
set reportDir $env(REPORT_DIR)The second and third are both correct; they encode different decisions. Use a default when the program can sensibly carry on, and a requirement when it cannot. What you must not do is the first — reading a variable that may not exist.
3. A missing one says something slightly different
Worth knowing so it does not surprise you:
array set ordinary {a 1}
puts "ordinary array, missing key:"
catch {set x $ordinary(zzz)} e ; puts " $e"
puts "env, missing variable:"
catch {set x $env(NO_SUCH_VAR_XYZ)} e ; puts " $e"
puts ""
puts "info exists works the same on both:"
puts " info exists ordinary(zzz) = [info exists ordinary(zzz)]"
puts " info exists env(NO_SUCH_VAR_XYZ) = [info exists env(NO_SUCH_VAR_XYZ)]"ordinary array, missing key:
can't read "ordinary(zzz)": no such element in array
env, missing variable:
can't read "env(NO_SUCH_VAR_XYZ)": no such variable
info exists works the same on both:
info exists ordinary(zzz) = 0
info exists env(NO_SUCH_VAR_XYZ) = 0Chapter 14.7 §1 taught that a missing array element says no such element in array. For env the message is no such variable instead, even though both are arrays.
Do not try to tell them apart by message text. info exists gives the same answer for both, which is the whole reason to use it.
4. env is a live link, not a copy
This is what makes env different from a normal array, and it is easiest to see by launching a child process.
# child.tcl
puts " child sees REPORT_MODE = [expr {[info exists env(REPORT_MODE)] ? $env(REPORT_MODE) : "<unset>"}]"# parent.tcl
puts "parent: REPORT_MODE starts as [expr {[info exists env(REPORT_MODE)] ? $env(REPORT_MODE) : "<unset>"}]"
puts "parent: launching a child BEFORE setting it"
puts [exec tclsh child.tcl]
set env(REPORT_MODE) "summary"
puts "parent: set env(REPORT_MODE) to 'summary'"
puts "parent: launching a child AFTER setting it"
puts [exec tclsh child.tcl]
unset env(REPORT_MODE)
puts "parent: unset it again"
puts [exec tclsh child.tcl]parent: REPORT_MODE starts as <unset>
parent: launching a child BEFORE setting it
child sees REPORT_MODE = <unset>
parent: set env(REPORT_MODE) to 'summary'
parent: launching a child AFTER setting it
child sees REPORT_MODE = summary
parent: unset it again
child sees REPORT_MODE = <unset>Writing to env changed the real process environment, and the next child inherited it. Unsetting removed it again.
An ordinary Tcl variable of the same name does nothing of the sort:
set REPORT_MODE "summary"
puts "parent: a plain variable REPORT_MODE = $REPORT_MODE"
puts [exec tclsh child.tcl]parent: a plain variable REPORT_MODE = summary
child sees REPORT_MODE = <unset>5. Choosing between an option and an environment variable
Both configure the program. The useful split:
| Use a command-line option when | Use the environment when |
|---|---|
| it changes per run | it is the same all day |
| the reader should see it in the command | it is machine- or site-specific |
| you want it recorded in a build log | it would be noise in every invocation |
| it is required | it is optional with a sensible default |
Then make the relationship between them explicit, because a user who sets both must be able to predict which wins.
The standard order, least to most specific:
built-in default → environment → command-line optionThe command line wins because it is the most deliberate: someone typed it, for this run, on purpose.
6. Precedence in the capstone
The report tool applies exactly that order. The default is in the namespace, main lets the environment override it, and parse_args runs afterwards so an option overrides both:
namespace eval ::report {
variable limit 100.0
}
proc ::report::main {argv} {
variable limit
if {[info exists ::env(REPORT_LIMIT)]} {
if {[string is double -strict $::env(REPORT_LIMIT)]} {
set limit $::env(REPORT_LIMIT)
} else {
puts stderr "warning: ignoring REPORT_LIMIT='$::env(REPORT_LIMIT)' (not a number)"
}
}
...
}All three levels, visible in one line of output each:
$ report-tool.tcl rt/data/readings.txt
over limit: pressure-1
$ REPORT_LIMIT=30 report-tool.tcl rt/data/readings.txt
over limit: temp-outlet, pressure-1, temp-case
$ REPORT_LIMIT=30 report-tool.tcl -limit 90 rt/data/readings.txt
over limit: pressure-1The built-in 100 flags one reading. REPORT_LIMIT=30 flags three. Adding -limit 90 with the environment still set flags one again — the option won.
And a bad value does not silently become a wrong limit:
$ REPORT_LIMIT=abc report-tool.tcl rt/data/readings.txt
warning: ignoring REPORT_LIMIT='abc' (not a number)
report for rt/data/readings.txt7. What not to put in the environment
Keep configuration harmless. Output directories, verbosity, a mode, a scratch path: fine.
Passwords, API tokens and licence keys are a different matter. The environment of a running process is visible to other processes owned by the same user on most systems, it is inherited by every child — including any external program you exec — and it routinely ends up in log files and crash dumps, because programs print their environment when they fail.
None of that is a Tcl issue; it is true of environment variables everywhere. It is simply a reason to read secrets from a protected file at the moment you need them rather than leaving them in the environment of every process you start.
8. Exercises
Exercise 1. What kind of Tcl value is env?
Exercise 2. Write the three lines that set reportDir from REPORT_DIR if present and to ./reports otherwise.
Exercise 3. Why read $::env(NAME) rather than $env(NAME) inside a procedure?
Exercise 4. REPORT_LIMIT is set to thirty. Your code does if {$value > $env(REPORT_LIMIT)}. What happens?
Exercise 5. A script does set env(TOOL_MODE) batch then exec a program. Does the program see it? Does the shell that launched your script see it afterwards?
Exercise 6. A user sets REPORT_LIMIT=30 and also passes -limit 90. Which should win, and why?
Exercise 7. Why warn and fall back on a bad environment value, but exit on a bad option value?
Where this fits
Part of the Tcl curriculum.
