Tcl · Module 15
Parsing Options From argv
Options are just words in a list until you separate them. Twelve lines of Tcl handle flags, options that take a value, several inputs, and the mistakes users will actually make.
Intermediate18 min readtclargvoptionsswitchcommand-linevalidation
Part I · Module 15 · Chapter 15.3 · Parsing Options From argv
Chapter 15.2 left argv as a flat list of words. A user typing
tclsh report.tcl -verbose -limit 30 data.txtmeans one flag, one option with a value, and one input file. Tcl hands you four words and no opinion about which is which. This chapter turns those words into decisions.
1. Why a loop, and not a search
The obvious first attempt is to look for the flag wherever it is, and assume the last word is the file:
# naive.tcl
set verbose 0
if {[lsearch -exact $argv -verbose] >= 0} { set verbose 1 }
set input [lindex $argv end]
puts "verbose = $verbose"
puts "input = $input"It looks fine on the case you tested:
$ tclsh naive.tcl -verbose data.txt
verbose = 1
input = data.txtNow the cases you did not:
$ tclsh naive.tcl -out report.txt data.txt
verbose = 0
input = data.txt
$ tclsh naive.tcl -verbose
verbose = 1
input = -verbose
$ tclsh naive.tcl -vrebose data.txt
verbose = 0
input = data.txtThree failures, and the last two are the dangerous kind:
-verboseon its own givesinput = -verbose. The flag became the filename. The program will now try to open a file called-verboseand report a confusing error about a file the user never mentioned.-vreboseis silently ignored. The user asked for verbose output, made a typo, and got a successful run with no verbosity and no complaint. They will assume the program ran verbosely and that there was nothing to report.
Both come from the same root cause: nothing in that code knows where options stop and inputs begin. A search cannot know that. A loop that walks the list front to back can.
2. The loop
set verbose 0
set out ""
set limit 100.0
while {[llength $argv]} {
set arg [lindex $argv 0]
switch -glob -- $arg {
-verbose { set verbose 1 ; set argv [lrange $argv 1 end] }
-out {
if {[llength $argv] < 2} { puts stderr "-out needs a value" ; exit 2 }
set out [lindex $argv 1] ; set argv [lrange $argv 2 end]
}
-limit {
if {[llength $argv] < 2} { puts stderr "-limit needs a value" ; exit 2 }
set limit [lindex $argv 1] ; set argv [lrange $argv 2 end]
}
-- { set argv [lrange $argv 1 end] ; break }
-* { puts stderr "unknown option: $arg" ; exit 2 }
default { break }
}
}
puts "verbose = $verbose"
puts "out = '$out'"
puts "limit = $limit"
puts "inputs = $argv ([llength $argv])"The shape is: look at the first word, decide what it is, remove as many words as it used, repeat. When you reach a word that is not an option, stop — everything left is input.
That is why each branch ends with set argv [lrange $argv ...]. A flag consumes one word; an option with a value consumes two. Getting that number wrong is the only real bug in this pattern, and it shows up immediately as a value appearing in the input list.
3. Every case it handles
$ tclsh parse.tcl data.txt
verbose = 0
out = ''
limit = 100.0
inputs = data.txt (1)
exit status: 0
$ tclsh parse.tcl -verbose data.txt
verbose = 1
out = ''
limit = 100.0
inputs = data.txt (1)
exit status: 0
$ tclsh parse.tcl -out report.txt -limit 30 data.txt
verbose = 0
out = 'report.txt'
limit = 30
inputs = data.txt (1)
exit status: 0
$ tclsh parse.tcl -verbose a.txt b.txt
verbose = 1
out = ''
limit = 100.0
inputs = a.txt b.txt (2)
exit status: 0Options in any order, values collected correctly, and any number of inputs left over. Now the mistakes:
$ tclsh parse.tcl -vrebose data.txt
unknown option: -vrebose
exit status: 2
$ tclsh parse.tcl -out
-out needs a value
exit status: 2One case the parser handles correctly but the program still must check:
$ tclsh parse.tcl -verbose
verbose = 1
out = ''
limit = 100.0
inputs = (0)
exit status: 0The parsing is right — there genuinely were no inputs. But a report tool with nothing to report is a usage error, so the program needs the [llength $inputs] == 0 guard from 15.2 §5 after parsing. Option parsing and input validation are two separate jobs.
4. What -- is for
$ tclsh parse.tcl -- -weird-name.txt
verbose = 0
out = ''
limit = 100.0
inputs = -weird-name.txt (1)
exit status: 0-- means stop looking for options; everything after this is input. Without it, a file genuinely named -weird-name.txt would hit the -* branch and be rejected as an unknown option, and the user would have no way to name their file.
It is three characters and it removes a whole class of "I cannot process this file" complaints. The branch is:
-- { set argv [lrange $argv 1 end] ; break }Consume the -- itself, then break out so nothing after it is examined.
5. default must come last
This one is a genuine silent bug, so it is worth seeing directly.
With default as the final pattern, everything behaves:
default LAST:
data.txt -> 'input'Move it above the -* branch and the same input matches nothing:
default NOT last (followed by -*):
data.txt -> ''
the literal word 'default' -> 'input'The ordering also matters for a second reason. -* is a glob pattern that matches any word starting with a dash, including -verbose. Because switch takes the first matching branch, the specific options must come before -*, or every option would be rejected as unknown.
6. Putting it in a procedure
At this size the loop belongs in a procedure that returns the leftover inputs:
namespace eval ::report {
variable verbose 0
variable limit 100.0
variable out ""
}
proc ::report::usage {} {
return "usage: [file tail $::argv0] \[-verbose\] \[-limit N\] \[-out FILE\] INPUT..."
}
proc ::report::parse_args {argv} {
variable verbose ; variable limit ; variable out
while {[llength $argv]} {
set arg [lindex $argv 0]
switch -glob -- $arg {
-verbose { set verbose 1 ; set argv [lrange $argv 1 end] }
-limit {
if {[llength $argv] < 2} { error "-limit needs a value" {} {REPORT USAGE} }
set v [lindex $argv 1]
if {![string is double -strict $v]} {
error "-limit needs a number, got '$v'" {} {REPORT USAGE}
}
set limit $v ; set argv [lrange $argv 2 end]
}
-out {
if {[llength $argv] < 2} { error "-out needs a value" {} {REPORT USAGE} }
set out [lindex $argv 1] ; set argv [lrange $argv 2 end]
}
-- { set argv [lrange $argv 1 end] ; break }
-* { error "unknown option: $arg" {} {REPORT USAGE} }
default { break }
}
}
return $argv
}Three changes from the inline version, each deliberate:
- It raises instead of calling
exit. A procedure that exits cannot be tested and cannot be reused — the caller never gets a chance to decide. Raising with aREPORT USAGEerror code letsmainprint the usage line and choose the status, which is Chapter 14.5 §7's dispatch idea applied to arguments. - It validates the value, not just its presence.
-limit abcis caught here rather than producing a nonsense comparison later — exactly the boundary check from Chapter 14.11. Remember that Tcl would happily string-compare againstabcand report nothing wrong. - Settings live in a namespace, not in globals, so the procedures that use them do not depend on a particular variable existing at the top level.
Running the capstone with each kind of bad input:
$ report-tool.tcl -limit
error: -limit needs a value
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]
$ report-tool.tcl -limit abc rt/data/readings.txt
error: -limit needs a number, got 'abc'
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]
$ report-tool.tcl -bogus rt/data/readings.txt
error: unknown option: -bogus
usage: report-tool.tcl [-verbose] [-limit N] [-out FILE] INPUT...
[exit status 2]Each message names the specific problem, then shows the shape of a correct invocation.
7. How far to take this
Stop here. A dozen lines of switch handle flags, values, multiple inputs, unknown options and --, and every line is readable by someone who has finished Part I.
Do not reach for an argument-parsing package to handle three options, and do not build a generic option-table framework with callbacks and type coercion. Those exist, and they are worth it at thirty options with subcommands. At three, the framework is more code than the program and one more thing for the next engineer to learn before they can change anything.
The signals that you have outgrown the loop are real ones: subcommands (tool build, tool clean), combinable short flags (-xvf), or generated help text. None of those apply to a script that takes a file and a limit.
8. Exercises
Exercise 1. Why can a parser built on lsearch not tell options from inputs?
Exercise 2. In the loop, why does -verbose consume one word and -out consume two?
Exercise 3. What does the -* branch do, and why is silently ignoring an unknown option worse than stopping?
Exercise 4. Why must -* appear before default, and what happens if default is not last?
Exercise 5. A user has a file genuinely named -data.txt. How do they pass it?
Exercise 6. The parser finishes with inputs empty and exit status 0. Is that a bug in the parser?
Exercise 7. Why should parse_args raise rather than call exit?
Where this fits
Part of the Tcl curriculum.
