parse_args(3tcl)

πŸ“¦ version 0.6.3, in ubuntu stonking

✨ parse_args(3tcl) β€” bestie, it's 0.6, it's the Tcl manual page, and it's about to fix your whole argument-parsing life ✨


πŸŽ€ NAME πŸŽ€

parse_args β€” Core-style argument parsing for scripts. Yes. The thing you've been hand-rolling badly in every single script. Bestie, put the duct tape down.

🌸 SYNOPSIS 🌸

package require parse_args ?0.6?

parse_args::parse_args args argspec ?varname?

πŸ’œ DESCRIPTION πŸ’œ

okay so. Let's talk about how the Tcl core does things, because it's lowkey iconic. The commands that the Tcl core hands you follow a whole pattern: there are optional, named arguments (you know the ones β€” they start with a - so you can tell they're options), and then there are the positional arguments just vibing along without a dash. And here's the tea β€” the core strongly prefers named arguments once the argument count starts growing, especially once you've got more than about 3 of them and a bunch of them are optional.

But Tcl scripts? Poor things. They have no access to those standard argument parsing facilities. Zero. Nada. So if you want that same pattern in your own code, you have to build the entire named-argument-parsing machinery from scratch, every time, by hand. 😭

And that is slow to do in script. It's error prone (fr fr). And it creates so much noise in your code that it actively distracts from the actual core task of the command you were trying to implement β€” the thing you actually wanted to write about. πŸ™ˆ

The result? Script-defined commands generally end up presenting half-baked argument handling APIs, and they tend to over-use positional arguments because who has the energy.

So this package shows up like the main character it is, and aims to address this weakness by handing script code a robust argument parsing mechanism that:

  • can implement the full set of patterns established by the core Tcl commands 🌈
  • is terse and readable to specify the arguments ✍️
  • is fast enough to use in any situation πŸ”₯

πŸ‘‘ COMMANDS πŸ‘‘

πŸ’… parse_args::parse_args args argspec

Parse the arguments supplied in args against the specification given in argspec, setting variables in the current scope as defined in the argspec. That's the default vibe β€” it just drops variables into wherever you called it from.

If the optional varname argument is supplied, though, plot twist: varname names a variable that will have a dictionary written to it, containing all the parsed arguments. And when you do that, no variables will be created for the arguments. It's one or the other, bestie, pick your lane.

See ARGUMENT SPECIFICATION below for the format of argspec β€” that's where the real magic happens, keep reading. πŸ¦‹

🌟 ARGUMENT SPECIFICATION 🌟

So the syntax of the argument specification (that's the argspec argument you hand to the parse_args command) is a dictionary. And the keys of that dictionary are the names by which your arguments will be available to the caller β€” either as variables created by the parse_args command, or as keys in the dictionary that gets written to varname if you specified one. Same names, different delivery system. 🎁

Here's the vibe check on keys:

  • If the first character of the key is a -, that key defines a named option. Fancy.
  • Otherwise, it's a positional parameter. Classic.

Also, just like in proc argument definitions, the name args is special ✨ β€” it will consume all remaining arguments. No cap.

The values of the dictionary hold the definition of the corresponding argument β€” stuff like -required, which marks that argument as being required (and yes, that works whether it's a named or a positional argument). πŸ’–

The options that are available to define the argument are:

  • -default val β€” Provides a default value for the argument if it wasn't supplied. Without this, an argument that wasn't supplied won't have its corresponding variable (or dictionary key) set at all β€” it just won't exist, ghosted. πŸ‘» And here's the cute part: val is permitted to be a value that is outside of the range of possible values that would be accepted from the caller. By design. Iconic.
  • -required β€” If this boolean option is given, the argument gets flagged as required. Fail to supply the arg and parse_args throws an exception β€” similar to what would happen if a required positional argument to a proc was not provided. So like, the exact same energy, just from us instead of the core. πŸ’€
  • -validate cmdprefix β€” Append the value supplied for this argument to the cmdprefix and run it. The value is rejected if an exception is thrown, or if the value returned isn't a Tcl boolean true value. Two ways to fail, one flag. 🎯
  • -name newname β€” Change the name of the variable / dictionary key that will receive the value for this argument, and make it newname. By default the name is just the argspec key (with the leading - dropped if it's a named argument). Glow-up, but for identifiers. πŸ’…
  • -boolean β€” Flag this argument as a boolean, similar to -nocase to regexp. The variable will always be defined, and it'll be a boolean: true if the argument was supplied, false if it wasn't. Perfect for "was this even there" energy. βœ…
  • -args count β€” By default, named arguments consume a single following argument as the value for that argument. Simple. Cute. But this setting changes that to count, which causes the following count arguments to be gathered into the variable holding this argument. And if count is the special value all, then all remaining arguments get gathered into a list as the value of this argument. 🧺
  • -enum possible_values β€” Restrict the values accepted for this argument to only those in the possible_values list. It's similar to using a validator that checks for inclusion in that list β€” same outcome β€” but with additional internal performance optimizations. So it's the turbo version of the same idea. πŸ”₯
  • -# free_form_text β€” Embed a comment for this argument. The supplied free_form_text is ignored by parse_args and serves only to document the argument to programmers reading the code. In some future version, this text may be incorporated into a usage error message that's generated when argument parsing fails. So for now it's a little love note to future-you. πŸ’Œ
  • -multi β€” Group a set of defined arguments together so they act like mutually exclusive flags. This is how you implement patterns like the -ascii, -dictionary, -integer and -real arguments to the core lsort command. 🫢 So, how it works:
    • name will be the name of the variable that holds the value of the flag that was supplied.
    • 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. πŸ’«
  • -alias β€” Treat the value supplied for this argument as a variable name, and bind the resulting variable for this argument to the variable of that name in the call frame above this one (upvar 1). Sharing is caring. πŸ”—
  • -all β€” Collect all the instances of this argument as a list, rather than the default behaviour of using the last instance as the value. Yep, the default is "last one wins", and this is how you change the rules. πŸ“
  • -end β€” Treat the remaining arguments as if -- directly followed this option. That is: all words in args after those consumed by the current option are treated as positional arguments. Everything after is just... arguments now. πŸ›‘

more parts are coming, bestie β€” don't go anywhere πŸ’–

✨ EXAMPLES ✨

okay bestie this is the fun part, these are all real Tcl core commands reimplemented on top of parse_args::parse_args, so you can literally see how the pros flex πŸ’ͺ


🌈 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]} {
            ...
        }
    }
    ...
}

see how args {-name patterns} just sucks up everything left over into a variable called patterns?? that's the whole magic trick, no cap 🎩✨


🌸 Mimic regexp:

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

not the -required tag doing its little job on exp and string so you literally cannot forget them πŸ’…


πŸ¦‹ 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::-
            }
        }
    }
}

the -multi squad + -name compare_as means ALL of -ascii, -dictionary, -integer, -real pour their value into one single variable and last one wins bestie 🫢 and yesss -default ascii means if you say nothing you get ascii vibes by default


πŸ“ 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"
    }
}

the way it just hand-rolls its own mutual-exclusion check with a plain old error at the end?? iconic. parse_args does the parsing, YOU do your own weird custom validation. separation of concerns, main character energy πŸ‘‘


πŸŽ€ 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       {}
    }
}

look at those -enum blocks spanning multiple lines, so you can list every legal value and parse_args will reject anything else β€” no more silent typo bugs bestie, she ate πŸ’–


πŸ‘€ SEE ALSO πŸ‘€

if you want the lore, the deep dive, the director's commentary ✨ the paper presented at the 2016 Tcl Conference talks about 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


πŸ’€ BUGS πŸ’€

found something broken?? 😭 please open an issue on the github tracker for the project if you encounter any problems:

https://github.com/RubyLane/parse_args/issues

do NOT suffer in silence bestie, the tracker is right there 🫢

πŸ“œ LICENSE πŸ“œ

This package is Copyright 2015-2026 Cyan Ogilvie, and is made available under the same license terms as the Tcl Core.


πŸ‘‘ AUTHORS πŸ‘‘

Cyan Ogilvie.


and that's the whole vibe check, we made it to the end of the page πŸŽ‰ this was version 0.6, page 3, filed under parse_args(3tcl) β€” go forth and <rainbow>parse some args bestie, you've got this πŸ’…βœ¨πŸ¦‹

πŸ“¦ where this page comes from