Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
tclsh report.tcl -verbose -limit 30 data.txt

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

The obvious first attempt is to look for the flag wherever it is, and assume the last word is the file:

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh naive.tcl -verbose data.txt
verbose = 1
input   = data.txt

Now the cases you did not:

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

Three failures, and the last two are the dangerous kind:

  • -verbose on its own gives input = -verbose. The flag became the filename. The program will now try to open a file called -verbose and report a confusing error about a file the user never mentioned.
  • -vrebose is 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

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

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

Options in any order, values collected correctly, and any number of inputs left over. Now the mistakes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh parse.tcl -vrebose data.txt
unknown option: -vrebose
exit status: 2

$ tclsh parse.tcl -out
-out needs a value
exit status: 2

One case the parser handles correctly but the program still must check:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ tclsh parse.tcl -verbose
verbose = 1
out     = ''
limit   = 100.0
inputs  =   (0)
exit status: 0

The 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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  default LAST:
    data.txt -> 'input'

Move it above the -* branch and the same input matches nothing:

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 a REPORT USAGE error code lets main print 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 abc is caught here rather than producing a nonsense comparison later — exactly the boundary check from Chapter 14.11. Remember that Tcl would happily string-compare against abc and 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:

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