0.7.2
parse_args - Core-style argument parsing for scripts
package require parse_args ?0.7.2?
parse_args::parse_args args argspec ?varname?
The commands provided by the Tcl core follow a pattern of optional, named arguments (named with a leading “-” to indicate an option) in addition to positional arguments. They strongly prefer named arguments when the number of arguments and particularly optional arguments grows beyond about 3. Tcl scripts however have no access to standard argument parsing facilities to implement this pattern and have to build their named argument parsing machinery from scratch. This is slow to do in script, error prone, and creates noise in the code that distracts from the core task of the command implementation. This means that script-defined commands generally present half-baked argument handling APIs and tend to over-use positional arguments.
This package aims to address this weakness by providing a robust argument parsing mechanism to script code that can implement the full set of patterns established by the core Tcl commands, is terse and readable to specify the arguments, and is fast enough to use in any situation.
The syntax of the argument specification (given in the
argspec argument to the parse_args command) is
a dictionary, with the keys corresponding to the names by which the
arguments will be available to the caller, either as variables created
by the parse_args command or keys in the dictionary
written to varname if that was specified. If the first
character of the key is a “-” character it defines a named option,
otherwise a positional parameter. Named options and positional
parameters may be freely interleaved in the argspec: positional
parameters are bound in the order they appear in it, and the order of
the named options is irrelevant. (The arguments being parsed still need
to supply any named options before the positional arguments - see
NAMED OPTION SCAN.) Each key may only appear once: a
repeated key is an error. Named options must be supplied exactly as they
appear in the argspec: unlike
Tcl_GetIndexFromObj, unique prefixes are not accepted.
Like in proc argument definitions the name args is special
and will consume all remaining arguments (after the leading positionals
are bound and modulo any reservation for trailing
-required positionals - see below for details); a
positional may use -args all to opt into the same
behaviour under a different name. The one difference is that
args, like a proc’s args, has an implicit
-default of the empty list, so it is always set,
whereas a -args all positional that receives no
arguments is left unset unless it has a -default.
The values of the dictionary contain the definition of the
corresponding argument, such as -required, which marks that
argument as being required (whether a named or positional argument). The
options that are available to define the argument are:
regexp. The variable will always be defined and be a
boolean, true if the argument was supplied and false if it wasn’t. Only
valid for named options; using -boolean on a positional
argument raises an error at spec compile time.
-ascii, -dictionary, -integer and
-real arguments to the core lsort command.
name will be the name of the variable that holds the flag that
was supplied, without its leading “-” (so -integer gives
integer). A -default setting on any of the
linked -multi arguments (those that share a
name) will supply a default value in name if none were
passed. A -required setting on any of the linked
-multi arguments will require that the caller supply at
least one of them. If multiple different flags were given, the one last
in the argument list applies, unless -all was specified
(on any linked option), in which case all instances are grouped as a
list in name, in the order they were specified.
-multi is only valid for named options, and can’t be
combined with -enum; either raises an error at spec
compile time.
Named options are recognised by scanning the arguments from the start. The scan ends, and all the remaining arguments are bound to positional parameters, at the first of:
An argument that doesn’t start with “-” (including the empty string).
The argument --, which is consumed (it isn’t bound
to any positional). Only the first -- is special: a later
one is an ordinary value.
The arguments consumed by an option marked -end.
The arguments reserved for trailing -required
positionals. These are the last n arguments, where n
is the total number of arguments the -required
positionals (without a -default) need. They are never
treated as options, even if they look like one. For example, with the
argspec {-x {} name {-required}}, the arguments
-foo and -x 1 -foo both set name
to -foo, but -foo bar is an unknown option
error.
If the argspec defines no named options, there is no scan at
all: every argument is positional, including any that start with “-”,
and -- is an ordinary value rather than a separator. So
parse_args {-foo} {name {-required}} sets name
to -foo, and parse_args {--} {name {}} sets
name to --.
An argument within the scan that starts with “-” and isn’t
-- must be a named option from the argspec,
otherwise it’s an error (see ERRORS). To pass a
positional value that may start with “-” to a command that has named
options, callers should precede it with --.
Positional parameters are bound left-to-right with one element of forward lookahead. Each positional p is processed in order:
If p is args (or has -args
all): p consumes all remaining input arguments, except
that arguments needed to satisfy any later
-required positionals are reserved for them. If after
that reservation p would consume zero arguments and it has no
-default (args always has one), the
variable for p is left unset.
If p is -required (without a -default): p consumes its max(1, arg_count) arguments. If too few input arguments remain to satisfy this it is a usage error.
Otherwise (optional, or -required with a -default): p consumes its arg_count arguments only when at least arg_count + (count needed for later -required positionals) arguments remain. Otherwise p’s -default is applied (if any) and consumption skips to the next positional.
This means optional positionals between or before
-required positionals are skipped automatically when
the caller supplies just enough arguments to satisfy the
-required ones, rather than greedily filling the
optional and erroring on the missing required. An argspec may
contain at most one args/-args all
positional, and any positional that follows it must be
-required without a -default.
The string representation of an input argument is generated only for arguments that fall within the named-option scan (see NAMED OPTION SCAN). Arguments reserved for trailing -required positionals, and all arguments when the argspec has no named options, are bound by position only and never have their first character inspected. So shimmer-hostile values, such as large pure bytearrays whose string form is expensive to generate, pass through without being converted.
An invalid argspec is reported when it is first compiled, which is the first time it is used to parse arguments (or the first time after it has lost its internal representation). Usage errors are reported with these errorcodes:
TCL LOOKUP INDEX option nameTCL LOOKUP INDEX option valueTCL WRONGARGSPARSE_ARGS REQUIRED namePARSE_ARGS REQUIRED_ONE_OF namesPARSE_ARGS WRONGARGS namePARSE_ARGS VALIDATION nameThe same argument parsing is available to C code, through a Tcl stubs table. A separately compiled extension depends on parse_args (pkg-config or meson dependency(‘parse_args’), which supply -DUSE_PARSE_ARGS_STUBS=1 and the stub library), includes parse_args.h and calls ParseArgs_InitStubs(interp) in its Init. Code compiled by jitc with a package {parse_args 0.7.2} part calls the functions directly.
All three leave an error message (and errorcode, as for parse_args) in the interp result on failure.
parseargs_bind.def declares and binds a struct local in a C command implementation, from an X-macro that declares the argspec:
static int foo(void* cdata, Tcl_Interp* interp, int objc, Tcl_Obj*const objv[])
{
#define PARSEARGS_SPEC(OPT, ARG) \
OPT( special, "-boolean", BOOL ) \
ARG( a1, "-required", STRING ) \
ARG( a2, "-default 42", INT ) \
ARG( a3, "-required", WIDEINT ) \
/* line intentionally left blank */
#include <parseargs_bind.def>
// args.special, args.a1, args.a2, args.a3 ...
return TCL_OK;
}Each entry is OPT(name, settings, type) for an option (-_name_) or ARG(name, settings, type) for a positional parameter. name is both the C struct member and the argspec name (so options are limited to names that are valid C identifiers), settings is a string literal with the argspec settings for the entry, and type selects the member’s C type and the conversion:
| type | Member(s) | Conversion |
|---|---|---|
| OBJ | Tcl_Obj* name | The value itself (NULL if not supplied) |
| BOOL | bool name | Tcl_GetBooleanFromObj |
| INT | int name | Tcl_GetIntFromObj |
| WIDEINT | Tcl_WideInt name | Tcl_GetWideIntFromObj |
| DOUBLE | double name | Tcl_GetDoubleFromObj |
| STRING | const char* name | Tcl_GetString |
| BYTES | const unsigned char* name, Tcl_Size name_len | Tcl_GetBytesFromObj |
| LIST | Tcl_Size namec, Tcl_Obj** namev | Tcl_ListObjGetElements (for -args n and args) |
| INDEX | int name | Index of the value in the entry’s -enum choices |
| SKIP | (none) | Not stored |
Further types can be added by defining PARSEARGS_OUTTYPE__type_(name) (the member declaration) and PARSEARGS_SLOTFOR__type_(out, name) (the struct ParseArgs_Slot initializer, using PARSEARGS_SLOT_CUSTOM and a converter function). Members for entries that are not supplied and have no -default are left zeroed.
Values are held until the struct goes out of scope, so pointer members stay valid until then. The release is done with defer, so the code including parseargs_bind.def must be compiled as C23 with defer support (as jitc cdefs are). These macros may be defined before the include to override the defaults, and are all undefined afterwards:
Mimic the argument handling of the core glob
command:
proc glob args {
parse_args::parse_args $args {
-directory {}
-join {-boolean}
-nocomplain {-boolean}
-path {}
-tails {-boolean}
-types {-default {}}
args {-name patterns}
}
if {$join} {
set patterns [list [file join {*}$patterns]]
}
if {[llength $patterns] == 0 && $nocomplain} return
foreach pattern $patterns {
if {[info exists directory]} {
...
}
}
...
}Mimic regex:
proc regexp args {
parse_args::parse_args $args {
-about {-boolean}
-expanded {-boolean}
-indices {-boolean}
-line {-boolean}
-linestop {-boolean}
-lineanchor {-boolean}
-nocase {-boolean}
-all {-boolean}
-inline {-boolean}
-start {-default 0}
exp {-required}
string {-required}
matchvar {}
args {-name submatchvars}
}
...
}lsort:
proc lsort args {
parse_args::parse_args $args {
-ascii {-name compare_as -multi -default ascii}
-dictionary {-name compare_as -multi}
-integer {-name compare_as -multi}
-real {-name compare_as -multi}
-command {}
-increasing {-name order -multi -default increasing}
-decreasing {-name order -multi}
-indices {-boolean}
-index {}
-stride {-default 1}
-nocase {-boolean}
-unique {-boolean}
list {-required}
}
if {![info exists command]} {
switch -- $compare_as {
ascii {
set command {string compare}
if {$nocase} {
lappend command -nocase
}
}
dictionary {
...
}
integer - real {
set command tcl::mathop::-
}
}
}
}lsearch:
proc lsearch args {
parse_args::parse_args $args {
-exact {-name matchtype -multi}
-glob {-name matchtype -multi -default glob}
-regexp {-name matchtype -multi}
-sorted {-boolean}
-all {-boolean}
-inline {-boolean}
-not {-boolean}
-start {-default 0}
-ascii {-name compare_as -multi -default ascii}
-dictionary {-name compare_as -multi}
-integer {-name compare_as -multi}
-real {-name compare_as -multi}
-nocase {-boolean}
-decreasing {-name order -multi}
-increasing {-name order -multi -default increasing}
-bisect {-boolean}
-index {}
-subindices {-boolean}
}
if {$sorted && $matchtype in {glob regexp}} {
error "-sorted is mutually exclusive with -glob and -regexp"
}
}A Tk widget - entry:
proc entry {widget args} {
parse_args::parse_args $args {
-disabledbackground {-default {}}
-disabledforeground {-default {}}
-invalidcommand {-default {}}
-readonlybackground {-default {}}
-show {}
-state {-default normal -enum {
normal disabled readonly
}}
-validate {-default none -enum {
none focus focusin focusout key all
}}
-validatecommand {-default {}}
-width {-default 0}
-textvariable {}
}
}The paper presented at the 2016 Tcl Conference which discusses the approach and design choices made in this package in much greater depth: https://www.tcl-lang.org/community/tcl2016/assets/talk33/parse_args-paper.pdf
Please open an issue on the github tracker for the project if you encounter any problems: https://github.com/RubyLane/parse_args/issues
This package is Copyright 2015-2026 Cyan Ogilvie, and is made available under the same license terms as the Tcl Core