Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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/reports

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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) = 0

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

This is what makes env different from a normal array, and it is easiest to see by launching a child process.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# child.tcl
puts "  child sees REPORT_MODE = [expr {[info exists env(REPORT_MODE)] ? $env(REPORT_MODE) : "<unset>"}]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set REPORT_MODE "summary"
puts "parent: a plain variable REPORT_MODE = $REPORT_MODE"
puts [exec tclsh child.tcl]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 whenUse the environment when
it changes per runit is the same all day
the reader should see it in the commandit is machine- or site-specific
you want it recorded in a build logit would be noise in every invocation
it is requiredit 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
built-in default   →   environment   →   command-line option

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

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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ REPORT_LIMIT=abc report-tool.tcl rt/data/readings.txt
warning: ignoring REPORT_LIMIT='abc' (not a number)
report for rt/data/readings.txt

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