@ -8,7 +8,7 @@
# (C) Julian Noble 2024
#
# @@ Meta Begin
# Application argparsingtest 1.0 .0
# Application argparsingtest 1.2 .0
# Meta platform tcl
# Meta license MIT
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_argparsingtest 0 1.0 .0]
#[manpage_begin punkshell_module_argparsingtest 0 1.2 .0]
#[copyright "2024"]
#[titledesc {Module API}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -123,16 +123,25 @@ namespace eval argparsingtest {
# title - table section heading used by compare
# argvec - default timing argument vector for the style
# roster - parser proc name -> marker, where marker is one of:
# always - proc unconditionally defined in argparsingtest::<style>
# argp - proc defined unconditionally but callable only when the
# optional argp package loaded at module require time
# unsupported - the backing library cannot express this calling style
# (no proc exists); reported as unsupported and shown as
# an N/A row by compare rather than being omitted
# always - proc unconditionally defined in argparsingtest::<style>
# {optional P} - proc defined unconditionally but callable only when the
# optional package P loaded at module require time
# (see the optional-package requires below); reported as
# loaded when P is present, not_loaded otherwise
# unsupported - the backing library cannot express this calling style
# (no proc exists); reported as unsupported and shown as
# an N/A row by compare rather than being omitted
# Styles:
# opts - keyed options only
# tkstyle - 2 mandatory positionals THEN options ('tk style', as in: .c create rect 1 2 3 4 -fill red)
# tclstyle - options THEN 2 mandatory positionals ('tcl style', as in: lsearch -exact $list $val)
# sandwich - 2 mandatory positionals, THEN options, THEN 3 more mandatory positionals.
# The asymmetric 2/3 split is deliberate: it catches a parser that
# merely splits the positional block evenly or matches it by count
# from one end. This is the STRICT sandwich - a parser is only
# credited with the style if it binds the leaders and the trailing
# values in that arrangement; see the sandwich namespace comment
# for what that does and does not say about permissive parsers.
# The manual_* roster membership differs deliberately: all manual option-loop
# variants are compared in the opts style; the positional styles carry a single
# manual_switch baseline since the option-loop technique comparison is orthogonal
@ -157,7 +166,9 @@ namespace eval argparsingtest {
opt always
cmdline_untyped always
cmdline_typed always
argp argp
argp {optional argp}
parse_args {optional parse_args}
argparse {optional argparse}
tepam always
}
}
@ -171,9 +182,11 @@ namespace eval argparsingtest {
punkargs_parsecache always
opt always
tepam always
argparse {optional argparse}
cmdline_untyped unsupported
cmdline_typed unsupported
argp unsupported
parse_args unsupported
}
}
tclstyle {
@ -188,18 +201,51 @@ namespace eval argparsingtest {
cmdline_untyped always
cmdline_typed always
tepam always
parse_args {optional parse_args}
argparse {optional argparse}
argp unsupported
}
}
sandwich {
title "sandwich style (2 leaders, options, 3 trailing)"
argvec {alpha beta -return object -join -x hello -y world -1 1 -2 2 -3 3 gamma delta epsilon}
roster {
manual_switch always
punkargs always
punkargs_by_id always
punkargs_parsecache always
opt always
argparse {optional argparse}
cmdline_untyped unsupported
cmdline_typed unsupported
tepam unsupported
argp unsupported
parse_args unsupported
}
}
}]
# Optional external parser packages that DO have timing wrappers (see the
# {optional P} roster markers above). Required once here, guarded by catch,
# so discover_parsers can probe them with side-effect-free 'package present'
# - matching how the opts::argp registration site requires argp.
# Both are binary extensions and neither ships with a stock tclsh; which of
# them is present varies per runtime. Surveyed 2026-07-26:
# both bin/runtime/win32-ix86/tclsh8.6.10-luck-test2.exe
# (argparse 0.61, parse_args 0.5.1)
# argparse only the native Tcl 9.0.3 install (argparse0.61 in its lib dir)
# parse_args only punk902z and punk91 kits (lib_tcl9/parse_args0.5.1)
# neither punk905
# On runtimes without them the wrapper procs stay defined but discover_parsers
# reports not_loaded, so the harness never calls them.
catch {package require parse_args}
catch {package require argparse}
# Optional external parser packages probed by discover_parsers (parser name -> package name).
# No timing wrappers are shipped for these: present -> not_wrappable, absent -> not_loaded.
variable optional_externals
set optional_externals {
getopt getopt
parse_args parse_args
argparse argparse
cmdr cmdr
}
@ -208,7 +254,7 @@ namespace eval argparsingtest {
@cmd -name argparsingtest::discover_parsers\
-summary "Discover available argument parsers per parsing style"\
-help\
"Returns a dict keyed by style (opts tkstyle tclstyle), each value
"Returns a dict keyed by style (opts tkstyle tclstyle sandwich ), each value
a dict of parser proc names to their status for that style.
Statuses:
@ -218,7 +264,7 @@ namespace eval argparsingtest {
unsupported - backing library cannot express this parsing style
(shown as an N/A row in compare tables)"
@opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\
-style -default all -type string -choices {all opts tkstyle tclstyle sandwich } -help\
"Style set(s) to report. Default all reports every style.
The result is keyed by style in all cases."
}
@ -237,15 +283,16 @@ namespace eval argparsingtest {
foreach s $stylelist {
set sresult {}
foreach {pname marker} [dict get $style_info $s roster] {
switch -- $marker {
switch -- [lindex $marker 0] {
always {
dict set sresult $pname loaded
}
argp {
# argp: proc defined unconditionally but registration guarded by catch at
# module load; use package present to check whether the load-time require
# succeeded (no side effects)
if {[catch {package present argp}]} {
optional {
# {optional P}: proc defined unconditionally but its backing package
# require (argp's registration, or the module-level requires for
# parse_args/argparse) is guarded by catch at module load; use package
# present to check whether that require succeeded (no side effects)
if {[catch {package present [lindex $marker 1]}]} {
dict set sresult $pname not_loaded
} else {
dict set sresult $pname loaded
@ -284,7 +331,7 @@ namespace eval argparsingtest {
warmup - warmup count used
Parsers that error during timing return {error <message>} instead."
@opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\
-style -default all -type string -choices {all opts tkstyle tclstyle sandwich } -help\
"Style set(s) to time. The result is keyed by style in all cases."
-iterations -default 1000 -type integer -help\
"Number of timed invocations per parser."
@ -314,7 +361,7 @@ namespace eval argparsingtest {
set stylelist [list $style]
}
if {[llength $argvec] && [llength $stylelist] > 1} {
error "timeit: -args requires a single -style (opts, tkstyle or tclstyle)"
error "timeit: -args requires a single -style (one of: o pts tkstyle tclstyle sandwich )"
}
set result {}
@ -490,6 +537,33 @@ namespace eval argparsingtest {
argparsingtest::libwarmup_argp -w 1
}
# parse_args and argparse warm-ups exercise option, flag and positional
# handling through a sacrificial proc, so the library internals are compiled
# before the timed call. Both are single warm-ups covering every style: the
# library_warmups dict is keyed by parser name only, and the parse_args /
# argparse wrappers share their name across the styles they support.
dict set library_warmups parse_args {
proc ::argparsingtest::libwarmup_parse_args {args} {
::parse_args::parse_args $args {
-w {-default 0}
-wflag {-boolean}
wpos {-required}
} res
return $res
}
::argparsingtest::libwarmup_parse_args -w 1 -wflag pval
}
dict set library_warmups argparse {
proc ::argparsingtest::libwarmup_argparse {args} {
return [::argparse -inline -mixed {
{-w -argument -default 0}
{-wflag}
{wpos}
} $args]
}
::argparsingtest::libwarmup_argparse -w 1 -wflag pval
}
punk::args::define {
@id -id ::argparsingtest::first_call
@cmd -name argparsingtest::first_call\
@ -522,7 +596,7 @@ namespace eval argparsingtest {
first-call microseconds (float), or {error <message>} if the run
fails in the child interp."
@opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\
-style -default all -type string -choices {all opts tkstyle tclstyle sandwich } -help\
"Style set(s) to time. The result is keyed by style in all cases."
-parsers -default {} -type list -help\
"List of parser proc names to time. Empty (default) times
@ -642,14 +716,18 @@ namespace eval argparsingtest {
backing library cannot express a style are still listed for that
style, as N/A rows below the timed rows.
- table: per-style human-readable matrix
(first_cold/first_warm columns are first_call timings)
(first_cold/first_warm columns are first_call timings).
Parsers whose backing package is absent are listed on a
'not loaded:' line; optional packages that are present but
have no timing wrapper get a separate 'no timing wrapper:' line
- dict: {results {style {...}} first_call {style {...}}
not_loaded {style {...}} unsupported {style {...}}}
not_loaded {style {...}} not_wrappable {style {...}}
unsupported {style {...}}}
first_call values are {cold <us> warm <us>} sub-dicts (see first_call)
- json: JSON object with results, first_call, not_loaded and
unsupported keys, each keyed by style"
- json: JSON object with results, first_call, not_loaded,
not_wrappable and unsupported keys, each keyed by style"
@opts -anyopts 0
-style -default all -type string -choices {all opts tkstyle tclstyle} -help\
-style -default all -type string -choices {all opts tkstyle tclstyle sandwich } -help\
"Style set(s) to compare: all (default) or a single style."
-format -default table -type string -choices {table dict json} -help\
"Output format: table (default), dict, or json."
@ -682,24 +760,28 @@ namespace eval argparsingtest {
set stylelist [list $style]
}
if {[llength $argvec] && [llength $stylelist] > 1} {
error "compare: -args requires a single -style (opts, tkstyle or tclstyle)"
error "compare: -args requires a single -style (one of: o pts tkstyle tclstyle sandwich )"
}
set discovered [discover_parsers]
# Per style: partition the roster (restricted to -parsers when given) into
# timing candidates (loaded), N/A rows (unsupported) and the not-loaded
# footer list. Requested names unknown to the roster are passed through to
# timeit so they surface as error entries rather than vanishing silently.
set timeit_dict {} ;# {style {pname timing}}
set first_call_dict {} ;# {style {pname {cold <us> warm <us>}}}
set na_dict {} ;# {style {pname ...}} - unsupported for the style
set not_loaded_dict {} ;# {style {pname ...}} - not_loaded or not_wrappable
# timing candidates (loaded), N/A rows (unsupported) and the two footer
# lists - packages absent from this runtime (not_loaded) and packages
# present but shipped without a timing wrapper (not_wrappable). Requested
# names unknown to the roster are passed through to timeit so they surface
# as error entries rather than vanishing silently.
set timeit_dict {} ;# {style {pname timing}}
set first_call_dict {} ;# {style {pname {cold <us> warm <us>}}}
set na_dict {} ;# {style {pname ...}} - unsupported for the style
set not_loaded_dict {} ;# {style {pname ...}} - backing package absent
set not_wrappable_dict {} ;# {style {pname ...}} - package present, no wrapper shipped
foreach s $stylelist {
set sdisco [dict get $discovered $s]
set candidates {}
set na {}
set notloaded {}
set notwrappable {}
if {[llength $parsers]} {
set requested $parsers
} else {
@ -711,13 +793,15 @@ namespace eval argparsingtest {
continue
}
switch -- [dict get $sdisco $pname] {
loaded { lappend candidates $pname }
unsupported { lappend na $pname }
default { lappend notloaded $pname }
loaded { lappend candidates $pname }
unsupported { lappend na $pname }
not_wrappable { lappend notwrappable $pname }
default { lappend notloaded $pname }
}
}
dict set na_dict $s $na
dict set not_loaded_dict $s $notloaded
dict set not_wrappable_dict $s $notwrappable
if {[llength $candidates]} {
set timeit_args [list -style $s -iterations $iterations -warmup $warmup -parsers $candidates]
@ -740,6 +824,7 @@ namespace eval argparsingtest {
set sfc [dict get $first_call_dict $s]
set sna [dict get $na_dict $s]
set snotloaded [dict get $not_loaded_dict $s]
set snotwrap [dict get $not_wrappable_dict $s]
set colwidth 6
foreach nm [concat [dict keys $stimeit] $sna] {
@ -787,6 +872,9 @@ namespace eval argparsingtest {
} else {
lappend lines "not loaded: (none)"
}
if {[llength $snotwrap] > 0} {
lappend lines "no timing wrapper: [join $snotwrap {, }]"
}
if {[llength $error_names] > 0} {
lappend lines "errors: [join $error_names {, }]"
}
@ -795,13 +883,14 @@ namespace eval argparsingtest {
return [join $style_blocks \n\n]
}
dict {
return [dict create results $timeit_dict first_call $first_call_dict not_loaded $not_loaded_dict unsupported $na_dict]
return [dict create results $timeit_dict first_call $first_call_dict not_loaded $not_loaded_dict not_wrappable $not_wrappable_dict unsupported $na_dict]
}
json {
package require json::write
set results_pairs {}
set fc_pairs {}
set nl_pairs {}
set nw_pairs {}
set na_pairs {}
foreach s $stylelist {
set timing_jsons [dict map {k v} [dict get $timeit_dict $s] {
@ -835,9 +924,10 @@ namespace eval argparsingtest {
lappend fc_pairs $s [json::write object {*}$fc_style_pairs]
lappend nl_pairs $s [json::write array-strings {*}[dict get $not_loaded_dict $s]]
lappend nw_pairs $s [json::write array-strings {*}[dict get $not_wrappable_dict $s]]
lappend na_pairs $s [json::write array-strings {*}[dict get $na_dict $s]]
}
return [json::write object results [json::write object {*}$results_pairs] first_call [json::write object {*}$fc_pairs] not_loaded [json::write object {*}$nl_pairs] unsupported [json::write object {*}$na_pairs]]
return [json::write object results [json::write object {*}$results_pairs] first_call [json::write object {*}$fc_pairs] not_loaded [json::write object {*}$nl_pairs] not_wrappable [json::write object {*}$nw_pairs] unsupported [json::write object {*}$na_pairs]]
}
}
}
@ -1545,6 +1635,89 @@ namespace eval argparsingtest::opts {
return [array get opts]
}
# parse_args and argparse (both binary extensions - see the optional-package
# requires in the argparsingtest base namespace) are called by their fully
# qualified names throughout: the wrapper procs deliberately share the
# package/command name, so an unqualified call would resolve to the wrapper
# itself (infinite recursion for argparse) rather than to the library.
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::parse_args
@cmd -name argparsingtest::opts::parse_args -summary "parse_args package arg parsing" -help\
"Accepts 10 keyed options plus -join. Uses parse_args::parse_args
with a result-dict variable rather than its set-locals default, so
the return shape matches the other opts-style wrappers. Result keys
are the option names with the leading dash stripped."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc parse_args {args} {
::parse_args::parse_args $args {
-return {-default string}
-frametype {-default \uFFEF}
-show_edge {-default \uFFEF}
-show_seps {-default \uFFEF}
-join {-boolean}
-x {-default ""}
-y {-default b}
-z {-default c}
-1 {-default 1}
-2 {-default 2}
-3 {-default 3}
} opts
return $opts
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::argparse
@cmd -name argparsingtest::opts::argparse -summary "argparse package arg parsing" -help\
"Accepts 10 keyed options plus -join. Uses argparse -inline, which
returns a dict rather than setting caller locals, so the return
shape matches the other opts-style wrappers. Switches taking a
value need an explicit -argument in the argparse element spec;
without it they are flags and the values become excess positionals."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc argparse {args} {
return [::argparse -inline {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
} $args]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::opts::tepam
@ -1597,6 +1770,10 @@ namespace eval argparsingtest::opts {
# argp has no wrapper in any positional style: argp::parseArgs walks the entire
# argument list as {key value} pairs, so any positional word errors as an
# unknown option (unsupported -> N/A row).
# parse_args has no wrapper here either: undashed spec entries are only matched
# after the options, so a leading positional aborts the parse with its generic
# 'Invalid args, should be ?-option ...? ?arg ...?' usage error (unsupported ->
# N/A row). argparse IS supported, but only in -mixed mode - see its wrapper.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::tkstyle {
namespace export {[a-z]*} ;# Convention: export all lowercase
@ -1859,6 +2036,51 @@ namespace eval argparsingtest::tkstyle {
return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tkstyle::argparse
@cmd -name argparsingtest::tkstyle::argparse -summary "argparse package arg parsing, tk style" -help\
"Accepts 2 leading positional arguments then 10 keyed options plus -join.
Uses argparse -inline -mixed: argparse's default mode requires switches
to precede positionals and rejects leading positionals as excess
arguments, so -mixed (switches permitted anywhere) is what makes this
style expressible. Returns a list {p1 p2 optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
}]
}
proc argparse {args} {
set parsed [::argparse -inline -mixed {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
} $args]
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::tkstyle ---}]
}
@ -1872,6 +2094,8 @@ namespace eval argparsingtest::tkstyle {
# options in the parser's natural form.
# cmdline IS supported here: cmdline::getoptions consumes leading options and
# leaves the trailing positionals in the caller's args variable.
# parse_args and argparse are both supported in this style - options-then-
# positionals is the arrangement each of them handles natively.
# argp has no wrapper (see the tkstyle namespace comment; unsupported -> N/A row).
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::tclstyle {
@ -2235,12 +2459,485 @@ namespace eval argparsingtest::tclstyle {
return [list $p1 $p2 [dict create return $return frametype $frametype show_edge $show_edge show_seps $show_seps x $x y $y z $z 1 $1 2 $2 3 $3 join $join]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tclstyle::parse_args
@cmd -name argparsingtest::tclstyle::parse_args -summary "parse_args package arg parsing, tcl style" -help\
"Accepts 10 keyed options plus -join, then 2 trailing positional arguments.
Positional arguments are declared by undashed name in the parse_args
spec and are only accepted after the options; -required makes both
mandatory. Returns a list {p1 p2 optsdict}."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
}]
}
proc parse_args {args} {
::parse_args::parse_args $args {
-return {-default string}
-frametype {-default \uFFEF}
-show_edge {-default \uFFEF}
-show_seps {-default \uFFEF}
-join {-boolean}
-x {-default ""}
-y {-default b}
-z {-default c}
-1 {-default 1}
-2 {-default 2}
-3 {-default 3}
p1 {-required}
p2 {-required}
} parsed
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::tclstyle::argparse
@cmd -name argparsingtest::tclstyle::argparse -summary "argparse package arg parsing, tcl style" -help\
"Accepts 10 keyed options plus -join, then 2 trailing positional arguments.
This is argparse's default arrangement - switches first, then
positionals - so no -mixed is needed here (contrast the tkstyle
wrapper). Returns a list {p1 p2 optsdict}."
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
}]
}
proc argparse {args} {
set parsed [::argparse -inline {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
} $args]
return [list [dict get $parsed p1] [dict get $parsed p2] [dict remove $parsed p1 p2]]
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::tclstyle ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Parser implementations - sandwich style: 2 mandatory positionals, THEN the 10
# keyed options (the argvec exercises the valueless -join flag as well as
# -flag value pairs), THEN 3 more mandatory positionals.
# All procs return a 3-element list {leaders values opts} where leaders is
# {p1 p2}, values is {p3 p4 p5} and opts is the parsed options in the parser's
# natural form. The other positional styles return a flat {p1 p2 opts}; with
# positionals on BOTH sides of the options, grouping them keeps which-end-is-which
# unambiguous for the parser_contract test.
#
# Only three libraries have wrappers here. Verified 2026-07-26 against the
# packages available on bin/runtime/win32-ix86/tclsh8.6.10-luck-test2.exe:
# - punk::args models the arrangement directly - @leaders, @opts and @values are
# three separate sections - and rejects any other ordering.
# - opt fills positional slots in declaration order, with the flags declared
# between the leader slots and the trailing slots; it likewise rejects the
# options appearing ahead of the leaders.
# - argparse needs -mixed, which permits switches ANYWHERE. It parses this
# argvec correctly, but unlike the two above it does NOT enforce the
# arrangement: the identical spec also accepts all five positionals trailing,
# or positionals interleaved with options. It is timed here for parsing the
# style, which is not a claim that it can express the constraint.
# Its flag convention also differs, as opts is reported in each parser's
# natural form: a supplied bare flag yields an empty-string value and an
# omitted one is absent from the dict entirely, where the punk::args, opt and
# manual wrappers report -join as 1 or 0.
# Unsupported (N/A rows), each verified to fail on this argvec:
# - cmdline: cmdline::getoptions stops at the first non-option word, so it
# returns WITHOUT error having parsed nothing at all - every option left at its
# default and the whole argument list untouched. A wrapper would have to
# hand-roll the option scan itself, so this is unsupported rather than wrapped;
# the silent no-op is the reason it cannot be credited by a smoke test alone.
# - tepam: unnamed arguments form one contiguous block, either before or after
# the named ones (-named_arguments_first 0/1); it cannot split them across the
# options.
# - argp: argp::parseArgs walks the entire argument list as {key value} pairs.
# - parse_args: undashed spec entries only match after the options.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval argparsingtest::sandwich {
namespace export {[a-z]*} ;# Convention: export all lowercase
#*** !doctools
#[subsection {Namespace argparsingtest::sandwich}]
#[para] parser implementations - sandwich style: 2 leading positional arguments, then options, then 3 trailing positional arguments
#[list_begin definitions]
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::manual_switch
@cmd -name argparsingtest::sandwich::manual_switch -summary "manual parsing - sandwich style baseline" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
The leading positionals are plain proc formal parameters; the option
loop consumes one word for -join and two for every other option, then
whatever remains must be exactly the 3 trailing positionals.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc manual_switch {p1 p2 args} {
set opts [dict create {*}{
-return string
-frametype \uFFEF
-show_edge \uFFEF
-show_seps \uFFEF
-join 0
-x ""
-y b
-z c
-1 1
-2 2
-3 3
}]
set i 0
set argc [llength $args]
while {$i < $argc} {
set k [lindex $args $i]
switch -- $k {
-join {
#valueless flag - consumes one word, not two
dict set opts -join 1
incr i
}
-return - -show_edge - -show_seps - -frametype - -x - -y - -z - -1 - -2 - -3 {
if {$i + 1 >= $argc} {
error "option '$k' requires a value"
}
dict set opts $k [lindex $args [expr {$i + 1}]]
incr i 2
}
default {
#first non-option word ends the option run - the trailing positionals
break
}
}
}
set values [lrange $args $i end]
if {[llength $values] != 3} {
error "expected exactly 3 trailing positional arguments, got [llength $values]"
}
return [list [list $p1 $p2] $values $opts]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::punkargs
@cmd -name argparsingtest::sandwich::punkargs -summary "punk::args withdef, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Uses punk::args::parse withdef with @leaders, @opts and @values sections -
the arrangement is declared, not inferred.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc punkargs {args} {
set argd [punk::args::parse $args withdef {
@id -id ::argparsingtest::sandwich::punkargs
@cmd -name argparsingtest::sandwich::punkargs -help "test of punk::args::parse comparative performance"
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-join -type none -multiple 1
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
#Sole documentation source is the eager punk::args::define (load-time
#registration is part of what this variant measures) - see the
#opts::punkargs_by_id comment.
punk::args::define {
@id -id ::argparsingtest::sandwich::punkargs_by_id
@cmd -name argparsingtest::sandwich::punkargs_by_id\
-summary "punk::args withid pre-registered, sandwich style"\
-help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Parses via a definition pre-registered at module load, using withid.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-join -type none -multiple 1
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}
proc punkargs_by_id {args} {
set argd [punk::args::parse $args withid ::argparsingtest::sandwich::punkargs_by_id]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::punkargs_parsecache
@cmd -name argparsingtest::sandwich::punkargs_parsecache -summary "punk::args withid with parse cache, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus -join,
then 3 trailing positional arguments.
Uses withid and -cache 1 for repeated parse reuse.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc punkargs_parsecache {args} {
set argd [punk::args::parse $args -cache 1 withid ::argparsingtest::sandwich::punkargs_by_id]
set leaders [tcl::dict::get $argd leaders]
set values [tcl::dict::get $argd values]
return [list\
[list [tcl::dict::get $leaders p1] [tcl::dict::get $leaders p2]]\
[list [tcl::dict::get $values p3] [tcl::dict::get $values p4] [tcl::dict::get $values p5]]\
[tcl::dict::get $argd opts]]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::opt
@cmd -name argparsingtest::sandwich::opt -summary "tcllib opt-based arg parsing, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
Uses tcl::OptProc with the leading positionals declared first, the flags
next and the trailing positionals last - tcl::OptProc fills positional
slots in declaration order, so the arrangement is expressed by the spec.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
tcl::OptProc opt {
{p1 -string "first leading positional argument"}
{p2 -string "second leading positional argument"}
{-return string "return type"}
{-frametype \uFFEF "type of frame"}
{-show_edge \uFFEF "show table outer borders"}
{-show_seps \uFFEF "show separators"}
{-join "solo option"}
{-x "" "x val"}
{-y b "y val"}
{-z c "z val"}
{-1 1 "1val"}
{-2 -int 2 "2val"}
{-3 -int 3 "3val"}
{p3 -string "first trailing positional argument"}
{p4 -string "second trailing positional argument"}
{p5 -string "third trailing positional argument"}
} {
set opts [dict create]
foreach v [info locals] {
if {$v ni {p1 p2 p3 p4 p5 opts args Args}} {
dict set opts $v [set $v]
}
}
return [list [list $p1 $p2] [list $p3 $p4 $p5] $opts]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::argparsingtest::sandwich::argparse
@cmd -name argparsingtest::sandwich::argparse -summary "argparse package arg parsing, sandwich style" -help\
"Accepts 2 leading positional arguments, then 10 keyed options plus the
valueless -join flag, then 3 trailing positional arguments.
Uses argparse -inline -mixed. -mixed permits switches anywhere, which is
what lets the leading positionals through; the cost is that this spec
does not CONSTRAIN the arrangement the way the punk::args and opt
wrappers do - it would equally accept all five positionals trailing.
Returns a list {{p1 p2} {p3 p4 p5} optsdict}."
@leaders -min 2 -max 2
p1 -type string -optional 0
p2 -type string -optional 0
@opts -anyopts 0
-return -default string -type string
-frametype -default \uFFEF -type string
-show_edge -default \uFFEF -type string
-show_seps -default \uFFEF -type string
-x -default "" -type string
-y -default b -type string
-z -default c -type string
-1 -default 1 -type boolean
-2 -default 2 -type integer
-3 -default 3 -type integer
-join -type none -multiple 1
@values -min 3 -max 3
p3 -type string -optional 0
p4 -type string -optional 0
p5 -type string -optional 0
}]
}
proc argparse {args} {
set parsed [::argparse -inline -mixed {
{-return -argument -default string}
{-frametype -argument -default \uFFEF}
{-show_edge -argument -default \uFFEF}
{-show_seps -argument -default \uFFEF}
{-join}
{-x -argument -default ""}
{-y -argument -default b}
{-z -argument -default c}
{-1 -argument -default 1}
{-2 -argument -default 2}
{-3 -argument -default 3}
{p1}
{p2}
{p3}
{p4}
{p5}
} $args]
return [list\
[list [dict get $parsed p1] [dict get $parsed p2]]\
[list [dict get $parsed p3] [dict get $parsed p4] [dict get $parsed p5]]\
[dict remove $parsed p1 p2 p3 p4 p5]]
}
#*** !doctools
#[list_end] [comment {--- end definitions namespace argparsingtest::sandwich ---}]
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# Secondary API namespace
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
@ -2288,7 +2985,7 @@ namespace eval ::punk::args::register {
package provide argparsingtest [namespace eval argparsingtest {
variable pkg argparsingtest
variable version
set version 1.0 .0
set version 1.2 .0
}]
return