From 5cd43813aa24bd1767e69ae700a653b992ff257f Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 9 Jul 2026 23:32:53 +1000 Subject: [PATCH] update bootsupport + vfs + layout bootsupport: punk::args 0.4.0 make.tcl modules + vfscommonupdate + bootsupport outputs: punk::args 0.3.2 -> 0.4.0 in src/bootsupport/modules, src/vfs/_vfscommon.vfs/modules and the punk.project-0.1 / punk.shell-0.1 layout bootsupport copies (make.tcl-generated). Built 0.4.0 module smoke-tested (loads, private ns present, PUNKARGS docs render). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- .../punk/{args-0.3.2.tm => args-0.4.0.tm} | 770 ++++++++++++++---- .../punk/{args-0.3.2.tm => args-0.4.0.tm} | 770 ++++++++++++++---- .../punk/{args-0.3.2.tm => args-0.4.0.tm} | 770 ++++++++++++++---- .../punk/{args-0.3.2.tm => args-0.4.0.tm} | 770 ++++++++++++++---- 4 files changed, 2504 insertions(+), 576 deletions(-) rename src/bootsupport/modules/punk/{args-0.3.2.tm => args-0.4.0.tm} (95%) rename src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/{args-0.3.2.tm => args-0.4.0.tm} (95%) rename src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/{args-0.3.2.tm => args-0.4.0.tm} (95%) rename src/vfs/_vfscommon.vfs/modules/punk/{args-0.3.2.tm => args-0.4.0.tm} (95%) diff --git a/src/bootsupport/modules/punk/args-0.3.2.tm b/src/bootsupport/modules/punk/args-0.4.0.tm similarity index 95% rename from src/bootsupport/modules/punk/args-0.3.2.tm rename to src/bootsupport/modules/punk/args-0.4.0.tm index ad66351a..06065b32 100644 --- a/src/bootsupport/modules/punk/args-0.3.2.tm +++ b/src/bootsupport/modules/punk/args-0.4.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.3.2 +# Application punk::args 0.4.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.3.2] +#[manpage_begin punkshell_module_punk::args 0 0.4.0] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -269,13 +269,66 @@ tcl::namespace::eval punk::args::register { } tcl::namespace::eval ::punk::args {} +#lib & system namespaces must exist for the private namespace path below (populated further down) +tcl::namespace::eval ::punk::args::lib {} +tcl::namespace::eval ::punk::args::system {} + +tcl::namespace::eval ::punk::args::private { + #Internal implementation helpers - not part of the punk::args API and not exported. + #Naming: no underscore prefix - the namespace itself denotes internal status. + #The namespace path allows unqualified calls to siblings in punk::args and its lib/system namespaces + #(mirroring the path set on ::punk::args itself at package-provide time). + tcl::namespace::path [list ::punk::args ::punk::args::lib ::punk::args::system] +} tcl::namespace::eval ::punk::args::helpers { variable PUNKARGS namespace export * + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::B + @cmd -name punk::args::helpers::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::N + @cmd -name punk::args::helpers::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Ends a bold span opened with punk::args::helpers::B in tstr-processed + argdoc -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::I + @cmd -name punk::args::helpers::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::NI + @cmd -name punk::args::helpers::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Ends an italic span opened with punk::args::helpers::I in + tstr-processed argdoc -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic #proc I {} {punk::ansi::a+ italic} #proc B {} {punk::ansi::a+ bold} @@ -1147,11 +1200,26 @@ tcl::namespace::eval punk::args { } + lappend PUNKARGS [list { + @id -id ::punk::args::errorstyle + @cmd -name punk::args::errorstyle\ + -summary\ + "Set or query the default errorstyle (unimplemented)."\ + -help\ + "Set or query the running configuration's default -errorstyle, + as used by punk::args::parse when no explicit -errorstyle + option is supplied in the parse call. + + NOT YET IMPLEMENTED - currently raises an error. + (review - is this an override or a default with respect to + an -errorstyle explicitly passed to punk::args::parse?)" + @values -min 0 -max 1 + errorstyle -type string -optional 1 -choices {debug enhanced standard basic minimal} -help\ + "New default errorstyle. If omitted, the current value is returned." + }] proc errorstyle {args} { #set or query the running config -errorstyle #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal error todo } proc define {args} { @@ -1175,6 +1243,24 @@ tcl::namespace::eval punk::args { return $id } } + lappend PUNKARGS [list { + @id -id ::punk::args::undefine + @cmd -name punk::args::undefine\ + -summary\ + "Remove a definition and its cached data by id."\ + -help\ + "Clears the raw definition and any resolved/cached data for the + supplied id from the punk::args caches. + The definition will no longer be retrievable by id unless redefined + (or rediscovered via update_definitions if it lives in a registered + namespace's PUNKARGS variable). + Emits a note to stderr unless quiet is true." + @values -min 1 -max 2 + id -type string -help\ + "id of the definition to remove (as shown by punk::args::get_ids)" + quiet -type boolean -default 0 -optional 1 -help\ + "Suppress stderr notices about clearing/missing ids" + }] proc undefine {id {quiet 0}} { #review - alias? variable rawdef_cache_about @@ -1209,6 +1295,22 @@ tcl::namespace::eval punk::args { } #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated # In this case we don't see the autoid in order to delete it + lappend PUNKARGS [list { + @id -id ::punk::args::undefine_deflist + @cmd -name punk::args::undefine_deflist\ + -summary\ + "Remove a definition and its cached data by raw definition list."\ + -help\ + "Clears cached data for a definition keyed by the raw definition list + itself rather than by id. This is useful where an automatic id was + generated (e.g by 'punk::args::parse ... withdef ') and the + caller never saw the autoid in order to pass it to punk::args::undefine. + A no-op if the definition list isn't in the cache." + @values -min 1 -max 1 + deflist -type list -help\ + "Raw definition list (list of definition blocks) as originally supplied + to define/parse" + }] proc undefine_deflist {deflist} { variable rawdef_cache_about variable id_cache_rawdef @@ -1222,17 +1324,24 @@ tcl::namespace::eval punk::args { } - proc define2 {args} { - dict get [resolve {*}$args] id - } - lappend PUNKARGS [list { @id -id ::punk::args::resolve - @cmd -name punk::args::resolve -help\ - "" + @cmd -name punk::args::resolve\ + -summary\ + "Resolve raw definition blocks to the internal argument specification dictionary."\ + -help\ + "Takes one or more raw definition blocks (as would be supplied to + punk::args::define) and returns the internal specification dictionary + - the parsed/optimised structure used by the argument parser and the + usage/synopsis renderers. + Results are cached; dynamic definitions (@dynamic, or leading + '-dynamic 1' args) are re-substituted in the caller's definition + namespace as required. + Normally called indirectly (via define/parse/get_spec) - but can be + called directly for inspection of the resolved structure." @values -min 0 -max -1 arg -type any -multiple 1 -help\ - "rawdef line block" + "raw definition block" }] proc resolve {args} { variable rawdef_cache_about @@ -2552,7 +2661,7 @@ tcl::namespace::eval punk::args { set tspec $typespec set optional_clausemember false } - set type_alternatives [_split_type_expression $tspec] + set type_alternatives [private::split_type_expression $tspec] set normlist [list] foreach alt $type_alternatives { set firstword [lindex $alt 0] @@ -2602,7 +2711,7 @@ tcl::namespace::eval punk::args { packageversion {set normtype packageversion} packagerequirement {set normtype packagerequirement} literal { - #value was split out by _split_type_expression + #value was split out by private::split_type_expression set normtype literal([lindex $alt 1]) } literalprefix { @@ -3114,6 +3223,21 @@ tcl::namespace::eval punk::args { #return raw definition list as created with 'define' # - possibly with unresolved dynamic parts + lappend PUNKARGS [list { + @id -id ::punk::args::raw_def + @cmd -name punk::args::raw_def\ + -summary\ + "Return the raw (unresolved) definition list for an id."\ + -help\ + "Returns the raw definition list (list of definition blocks) as + originally supplied for the id. + The id is first resolved via punk::args::real_id, so aliases and + definitions in not-yet-scanned registered namespaces are found. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc raw_def {id} { variable id_cache_rawdef set realid [real_id $id] @@ -3481,6 +3605,22 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::resolved_def_values + @cmd -name punk::args::resolved_def_values\ + -summary\ + "Return textual definitions for the trailing values of a resolved definition."\ + -help\ + "Returns a text block of 'name ' lines for the + @values (trailing positional) arguments of the resolved definition, + optionally filtered by glob patterns on the value names. + See also punk::args::resolved_def for the full definition." + @values -min 1 -max 2 + id -type string -help\ + "id or id alias of the definition" + patternlist -type list -default * -optional 1 -help\ + "List of glob patterns to match against value argument names" + }] proc resolved_def_values {id {patternlist *}} { variable id_cache_rawdef set realid [real_id $id] @@ -3514,6 +3654,20 @@ tcl::namespace::eval punk::args { #proc resolved_def_leaders ?? #proc resolved_def_opts ?? + lappend PUNKARGS [list { + @id -id ::punk::args::get_spec + @cmd -name punk::args::get_spec\ + -summary\ + "Return the resolved internal specification dictionary for an id."\ + -help\ + "Retrieves the raw definition for the id (resolving aliases) and + returns the resolved internal specification dictionary as produced + by punk::args::resolve. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc get_spec {id} { set deflist [raw_def $id] if {$deflist eq ""} { @@ -3524,6 +3678,21 @@ tcl::namespace::eval punk::args { # return [resolve {*}[raw_def $id]] #} } + lappend PUNKARGS [list { + @id -id ::punk::args::is_dynamic + @cmd -name punk::args::is_dynamic\ + -summary\ + "Test whether the definition for an id is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the definition contains an + @dynamic directive (or legacy leading '-dynamic 1' arguments). + Dynamic definitions are re-substituted (dollar variables and + bracketed commands within the text) in the defining namespace on + resolution rather than being resolved once and cached in final form." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc is_dynamic {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3537,6 +3706,25 @@ tcl::namespace::eval punk::args { #@id must be within first 4 lines of first 3 blocks - or assign auto #review - @dynamic block where -id not explicitly set? - disallow? + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_id + @cmd -name punk::args::rawdef_id\ + -summary\ + "Determine the id for a raw definition list."\ + -help\ + "Extracts the id from the @id -id line of a raw definition list. + The @id line must appear within the first 4 lines of one of the first + 3 definition blocks. If no @id line is present (or its -id value is + 'auto') an automatic id of the form autoid_ is generated. + A malformed @id line (found but no retrievable -id value) raises an error. + The -id value undergoes tstr substitution in the caller's context at + the given level." + @values -min 1 -max 2 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + lvl -type integer -default 1 -optional 1 -help\ + "uplevel offset at which to perform tstr substitution of the @id line" + }] proc rawdef_id {rawdef {lvl 1}} { set id "" set found_id_line 0 @@ -3575,6 +3763,20 @@ tcl::namespace::eval punk::args { return $id } #test the rawdef for @dynamic directive + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_is_dynamic + @cmd -name punk::args::rawdef_is_dynamic\ + -summary\ + "Test whether a raw definition list is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the raw definition list contains + a block beginning with the @dynamic directive (or uses the legacy + leading '-dynamic 1' argument form). + See also punk::args::is_dynamic which tests by id." + @values -min 1 -max 1 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + }] proc rawdef_is_dynamic {rawdef} { #temporary - old way set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]}] @@ -3609,6 +3811,21 @@ tcl::namespace::eval punk::args { } #we don't automatically test for (autodef)$id - only direct ids and aliases + lappend PUNKARGS [list { + @id -id ::punk::args::id_exists + @cmd -name punk::args::id_exists\ + -summary\ + "Test whether an id or id alias is already known."\ + -help\ + "Returns a boolean indicating whether the supplied value is a known + definition id or id alias. + Only direct ids and aliases are tested - (autodef) entries are not + automatically checked, and no update_definitions scan is triggered + (compare punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to test" + }] proc id_exists {id} { variable aliases if {[tcl::dict::exists $aliases $id]} { @@ -3617,24 +3834,91 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + lappend PUNKARGS [list { + @id -id ::punk::args::idaliases + @cmd -name punk::args::idaliases\ + -summary\ + "Display the current id alias dictionary."\ + -help\ + "Returns a display (punk::lib::showdict) of the alias -> id mapping + for all id aliases created with punk::args::set_idalias." + @values -min 0 -max 0 + }] proc idaliases {} { variable aliases punk::lib::showdict $aliases } + lappend PUNKARGS [list { + @id -id ::punk::args::set_idalias + @cmd -name punk::args::set_idalias\ + -summary\ + "Create or update an alias for a definition id."\ + -help\ + "Creates (or overwrites) an alias that can be used in place of the + target id in functions such as punk::args::parse (withid), raw_def, + get_spec and usage. + The target id need not exist at the time the alias is created." + @values -min 2 -max 2 + alias -type string -help\ + "alias name" + id -type string -help\ + "target definition id" + }] proc set_idalias {alias id} { variable aliases dict set aliases $alias $id } + lappend PUNKARGS [list { + @id -id ::punk::args::unset_idalias + @cmd -name punk::args::unset_idalias\ + -summary\ + "Remove an id alias."\ + -help\ + "Removes an alias created with punk::args::set_idalias. + A no-op if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to remove" + }] proc unset_idalias {alias} { variable aliases dict unset aliases $alias } + lappend PUNKARGS [list { + @id -id ::punk::args::get_idalias + @cmd -name punk::args::get_idalias\ + -summary\ + "Return the target id for an alias."\ + -help\ + "Returns the id that the alias points to, + or an empty string if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to look up" + }] proc get_idalias {alias} { variable aliases if {[dict exists $aliases $alias]} { return [tcl::dict::get $aliases $alias] } } + lappend PUNKARGS [list { + @id -id ::punk::args::id_query + @cmd -name punk::args::id_query\ + -summary\ + "Developer report of cache state for a definition id."\ + -help\ + "Returns a text report showing the raw definition, id info + (dynamic flag, defining namespace) and argdata cache entries + for the id - including a warning if the id is unexpectedly + present under multiple rawdef keys. + Intended for interactive/debug use. + Returns an empty string if the id isn't directly present + (aliases are not followed - see punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "definition id to report on" + }] proc id_query {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3689,6 +3973,24 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::real_id + @cmd -name punk::args::real_id\ + -summary\ + "Resolve an id or alias to the actual id under which a definition is stored."\ + -help\ + "Resolves aliases and returns the id under which the definition is + actually stored - which may be the supplied id itself, the alias + target, or an (autodef) entry created by the introspection + facilities. + If the id isn't found, punk::args::update_definitions is called for + the id's namespace qualifiers so that definitions in newly loaded + packages are discovered. + Returns an empty string if no definition can be found." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to resolve" + }] proc real_id {id} { variable id_cache_rawdef variable aliases @@ -3721,6 +4023,20 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::status + @cmd -name punk::args::status\ + -summary\ + "Report scan/load status of registered definition namespaces."\ + -help\ + "Returns a table of namespaces registered in + punk::args::register::NAMESPACES showing, for each, how many ids were + found when scanned, how many definitions have been loaded, and the + time taken (microseconds) for the scan/load passes. + Definitions are scanned and loaded lazily - a blank row indicates a + package whose definitions haven't been needed yet." + @values -min 0 -max 0 + }] proc status {} { upvar ::punk::args::register::NAMESPACES registered upvar ::punk::args::register::loaded_packages loaded_packages @@ -3986,29 +4302,29 @@ tcl::namespace::eval punk::args { # -------------------------------------- - #test of Get_caller + #developer diagnostics - test of Get_caller (unexported - leading underscores exclude from export pattern) lappend PUNKARGS [list { - @id -id ::punk::args::test1 + @id -id ::punk::args::__test1 @values -min 0 -max 0 }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args + proc __test_get_dict {args} { + punk::args::get_dict [punk::args::raw_def ::punk::args::__test1] $args } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args + proc __test_get_by_id {args} { + punk::args::get_by_id ::punk::args::__test1 $args } #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { + proc __test_callers {args} { if {![llength $args]} { puts "these test functions accept no arguments" puts "Call with arg(s) to compare error output" } - if {[catch {test_get_dict {*}$args} errM]} { + if {[catch {__test_get_dict {*}$args} errM]} { puts $errM } puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { + if {[catch {__test_get_by_id {*}$args} errM]} { puts $errM } return done @@ -4073,8 +4389,9 @@ tcl::namespace::eval punk::args { variable arg_error_CLR_error array set arg_error_CLR_error {} - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR + proc private::argerror_load_colours {{forcereload 0}} { + #colour state arrays live in the parent punk::args namespace (shared with arg_error) + upvar #0 ::punk::args::arg_error_CLR arg_error_CLR #todo - option for reload/retry? if {!$forcereload && [array size arg_error_CLR] > 0} { return @@ -4107,7 +4424,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR(ansibase_header) [a+ bold] set arg_error_CLR(ansibase_body) [a+ white] set arg_error_CLR(parsekey_hint) [a+ term-grey46] - variable arg_error_CLR_nocolour + upvar #0 ::punk::args::arg_error_CLR_nocolour arg_error_CLR_nocolour #array set arg_error_CLR_nocolour {} set arg_error_CLR_nocolour(errormsg) [a+ bold] set arg_error_CLR_nocolour(title) [a+ bold] @@ -4123,7 +4440,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_nocolour(ansibase_header) [a+ bold] set arg_error_CLR_nocolour(ansibase_body) "" set arg_error_CLR_nocolour(parsekey_hint) "" - variable arg_error_CLR_info + upvar #0 ::punk::args::arg_error_CLR_info arg_error_CLR_info #array set arg_error_CLR_info {} set arg_error_CLR_info(errormsg) [a+ brightred bold] set arg_error_CLR_info(title) [a+ brightyellow bold] @@ -4134,7 +4451,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_info(ansiborder) [a+ term-grey23 bold] set arg_error_CLR_info(ansibase_header) [a+ cyan] set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error + upvar #0 ::punk::args::arg_error_CLR_error arg_error_CLR_error #array set arg_error_CLR_error {} set arg_error_CLR_error(errormsg) [a+ brightred bold] set arg_error_CLR_error(title) [a+ brightcyan bold] @@ -4198,7 +4515,7 @@ tcl::namespace::eval punk::args { set forcereload 1 } } - _argerror_load_colours $forcereload + private::argerror_load_colours $forcereload if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -5757,58 +6074,11 @@ tcl::namespace::eval punk::args { } return $result } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - #return number of values we can assign to cater for variable length clauses such as: # {"elseif" expr "?then?" body} #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { + proc private::get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { set ARG_INFO [dict get $formdict ARG_INFO] set all_remaining [lrange $values $idx end] set thisname [lindex $names $nameidx] @@ -5850,11 +6120,11 @@ tcl::namespace::eval punk::args { #todo - support complex type members such as -type {{literal a|b} int OR} #for now - require llength 1 - simple type such as -type {literal(ab)|int} if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + error "private::get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" } #foreach tp_alternative [split $tp |] {} - set tp_alternatives [_split_type_expression $tp] + set tp_alternatives [private::split_type_expression $tp] foreach tp_alternative $tp_alternatives { switch -exact -- [lindex $tp_alternative 0] { literal { @@ -5926,7 +6196,7 @@ tcl::namespace::eval punk::args { } } else { - #todo - use _split_type_expression + #todo - use private::split_type_expression #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) @@ -5952,7 +6222,7 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - puts "_get_dict_can_assign_value: checking tp '$tp' against value '$rv'" + puts "private::get_dict_can_assign_value: checking tp '$tp' against value '$rv'" switch -glob -- $tp { "literal(*" { set litmatch [string range $tp 8 end-1] @@ -6094,7 +6364,7 @@ tcl::namespace::eval punk::args { set ctg_stringendswith [list] set ctg_other [list] #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { + foreach tp_alternative [private::split_type_expression $tp] { #JJJJ lassign $tp_alternative t textra switch -exact -- $t { @@ -6244,17 +6514,17 @@ tcl::namespace::eval punk::args { #so that they are not subject to type validation #such elements shouldn't be subject to validation if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - #puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed 0 resultlist {} typelist $thistype] } - #puts ">>>> _get_dict_can_assign_value $d" + #puts ">>>> private::get_dict_can_assign_value $d" return $d } - #_split_type_expression + #private::split_type_expression #only handles toplevel 'or' for type_expression e.g int|char #we have no mechanism for & - (although it would be useful) #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) @@ -6273,7 +6543,7 @@ tcl::namespace::eval punk::args { #mode -type {literalprefix text1 literalprefix text2 OR} #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - proc _split_type_expression {type_expression} { + proc private::split_type_expression {type_expression} { if {[llength $type_expression] == 1} { #simple expressions of length one must be splittable on | #disallowed: things such as literal(|) or literal(x|etc)|int @@ -6289,7 +6559,7 @@ tcl::namespace::eval punk::args { } return $type_alternatives } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + error "private::split_type_expression unimplemented: type_expression length > 1 '$type_expression'" #todo #RPN reverse polish notation #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} @@ -6309,14 +6579,14 @@ tcl::namespace::eval punk::args { set expect_separator 0 continue } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + error "private::split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" } } switch -exact -- $word { literal - literalprefix - stringstartswith - stringendswith - stringcontains { if {$w+1 > [llength $type_expression]} { #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + error "private::split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" } lappend type_alternatives [list $word [lindex $type_expression $w+1]] incr w ;#consume arg @@ -6334,12 +6604,12 @@ tcl::namespace::eval punk::args { } #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ###proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { ### #set type $type_expression ;#todo - 'split' on | ### set vlist $clausevalues_raw ### set vlist_check $clausevalues_check - ### set type_alternatives [_split_type_expression $type_expression] + ### set type_alternatives [private::split_type_expression $type_expression] ### #each type_alternative is a list of varying length depending on arguments supported by first word. ### #TODO? ### #single element types: int double string etc @@ -6736,7 +7006,7 @@ tcl::namespace::eval punk::args { #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname #common basic case: list of single item being a single value clause. #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { #default_type_expression is for the chosen clausecolumn #if {$argname eq "frametype"} { #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" @@ -6747,7 +7017,7 @@ tcl::namespace::eval punk::args { set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - set default_type_alternatives [_split_type_expression $default_type_expression] + set default_type_alternatives [private::split_type_expression $default_type_expression] #--------------------- #pre-calc prefix sets based on the default. set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] @@ -6779,7 +7049,7 @@ tcl::namespace::eval punk::args { set c_idx -1 foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set col_type_alternatives [private::split_type_expression $clause_column_type_expression] set firstany [lsearch -exact $col_type_alternatives any] if {$firstany > -1} { lset clause_results $c_idx $firstany 1 @@ -7461,7 +7731,7 @@ tcl::namespace::eval punk::args { # #} # } - # set type_alternatives [_split_type_expression $type_expression] + # set type_alternatives [private::split_type_expression $type_expression] # #each type_alternative is a list of varying length depending on arguments supported by first word. # #TODO? # #single element types: int double string etc @@ -8141,18 +8411,40 @@ tcl::namespace::eval punk::args { #G-040: the single implementation of choice-word matching - shared by argument parsing #(get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so 'i ' #resolution can never diverge from what parsing accepts. - #Arguments: - # word - the supplied word (callers pass the check-form, e.g. ansistripped when applicable) - # nocase - value of -nocase for the argument - # allchoices - flat list of -choices plus all -choicegroups members (dups allowed) - # choicealiases - -choicealiases dict (alias -> canonical choice) - # choiceprefix - value of -choiceprefix - # denylist - -choiceprefixdenylist (full word required for these names) - # reservelist - -choiceprefixreservelist (phantom prefix-calculation members) - #Returns dict: - # matched - boolean: word identifies a choice (directly, by unique prefix, or via an alias) - # exact - boolean: the raw stored word already equals the resulting choice (no rewrite needed) - # canonical - the resulting choice value (empty when not matched) + lappend PUNKARGS [list { + @id -id ::punk::args::choiceword_match + @cmd -name punk::args::choiceword_match\ + -summary\ + "Match a supplied word against an argument's choices (the shared choice-matching implementation)."\ + -help\ + "The single implementation of choice-word matching - shared by argument + parsing (get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so + 'i ' resolution can never diverge from what parsing accepts. + (G-040) + + Returns a dict with keys: + matched - boolean: word identifies a choice (directly, by unique + prefix, or via an alias) + exact - boolean: the raw stored word already equals the resulting + choice (no rewrite needed) + canonical - the resulting choice value (empty when not matched)" + @values -min 7 -max 7 + word -type string -help\ + "The supplied word. + Callers pass the check-form, e.g ansistripped when applicable" + nocase -type boolean -help\ + "value of -nocase for the argument" + allchoices -type list -help\ + "flat list of -choices plus all -choicegroups members (dups allowed)" + choicealiases -type dict -help\ + "-choicealiases dict (alias -> canonical choice)" + choiceprefix -type boolean -help\ + "value of -choiceprefix" + denylist -type list -help\ + "-choiceprefixdenylist (full word required for these names)" + reservelist -type list -help\ + "-choiceprefixreservelist (phantom prefix-calculation members)" + }] proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} { set aliasnames [tcl::dict::keys $choicealiases] set has_choicealiases [expr {[llength $aliasnames] > 0}] @@ -8291,6 +8583,30 @@ tcl::namespace::eval punk::args { #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. #only supports -flag val pairs, not solo options #If an option is supplied multiple times - only the last value is used. + lappend PUNKARGS [list { + @id -id ::punk::args::get_dict + @cmd -name punk::args::get_dict\ + -summary\ + "Parse an argument list against a raw definition - the core parsing engine."\ + -help\ + "The core argument parsing/validation engine underlying + punk::args::parse (which is the preferred API for procs). + Parses rawargs against the supplied raw definition list and returns + a dict with keys: leaders, opts, values (argument name/value dicts) + and received (names of arguments actually supplied, with ordinal + clause info where applicable), plus solos. + Raises an error (rendering a usage table according to the errorstyle + in effect) if the arguments don't conform to the definition." + @leaders -min 2 -max 2 + deflist -type list -help\ + "Raw definition list (list of definition blocks)" + rawargs -type list -help\ + "list of arguments to parse against the definition" + @opts + -form -default * -help\ + "Ordinal index or name of the command form to parse against. + The default * considers all forms." + }] proc get_dict {deflist rawargs args} { #see arg_error regarding considerations around unhappy-path performance @@ -8651,7 +8967,7 @@ tcl::namespace::eval punk::args { incr tentative_idx } } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set assign_d [private::get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9579,7 +9895,7 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during private::get_dict_can_assign_value set start_position $positionalidx @@ -9592,14 +9908,14 @@ tcl::namespace::eval punk::args { set leadertypelist [tcl::dict::get $argstate $leadername -type] ;#often a single type, but can be a list of types (possibly with some optional) for a type that is a clause accepting multiple values. set leader_clause_size [llength $leadertypelist] - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] + set assign_d [private::get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] if {$consumed == 0} { if {[tcl::dict::get $argstate $leadername -optional]} { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 1?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9607,12 +9923,12 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg } else { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9803,7 +10119,7 @@ tcl::namespace::eval punk::args { set valtypelist [tcl::dict::get $argstate $valname -type] set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] + set assign_d [private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] ;#count set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9818,7 +10134,7 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { @@ -10545,8 +10861,8 @@ tcl::namespace::eval punk::args { #set typespec [lindex $typelist $clausecolumn] #todo - handle type-alternates e.g -type char|double #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #private::check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + private::check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs #------------------------------------------------------------------------------------ @@ -10658,7 +10974,7 @@ tcl::namespace::eval punk::args { } - proc _synopsis_form_arg_display {formdict argname} { + proc private::synopsis_form_arg_display {formdict argname} { #non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings set I "\x1b\[3m" ;#[punk::ansi::a+ italic] set NI "\x1b\[23m" ;# [punk::ansi::a+ noitalic] @@ -10699,7 +11015,7 @@ tcl::namespace::eval punk::args { } else { #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set tp_alternative_word1 [lindex $tp_alternative 0] set match [lindex $tp_alternative 1] @@ -10901,7 +11217,7 @@ tcl::namespace::eval punk::args { set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set arginfo [dict get $forminfo ARG_INFO $argname] @@ -10945,7 +11261,7 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set match [lindex $tp_alternative 1] switch -- [lindex $tp_alternative 0] { @@ -11129,7 +11445,7 @@ tcl::namespace::eval punk::args { foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set ARGD [dict create argname $argname class value] @@ -11522,18 +11838,6 @@ tcl::namespace::eval punk::args { return $argdef } - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - #*** !doctools #[list_end] [comment {--- end definitions namespace punk::args ---}] } @@ -11545,7 +11849,7 @@ tcl::namespace::eval punk::args { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::args::lib { variable PUNKARGS - tcl::namespace::export * + tcl::namespace::export {[a-z]*} tcl::namespace::path [list [tcl::namespace::parent]] #*** !doctools #[subsection {Namespace punk::args::lib}] @@ -11553,6 +11857,22 @@ tcl::namespace::eval punk::args::lib { #[list_begin definitions] #tcl86 compat for string is dict - but without -strict or -failindex options + lappend PUNKARGS [list { + @id -id ::punk::args::lib::string_is_dict + @cmd -name punk::args::lib::string_is_dict\ + -summary\ + "Test whether a string is a valid dict ('string is dict' compatibility wrapper)."\ + -help\ + "Returns a boolean indicating whether the string can be interpreted + as a dictionary (an even-length list). + On Tcl 9+ this is a thin wrapper over 'string is dict'. + On earlier Tcl versions a compatible fallback is used which accepts + but ignores any leading options (-strict/-failindex are NOT implemented)." + @values -min 1 -max -1 + arg -type any -multiple 1 -help\ + "optional 'string is dict' options followed by the string to test. + Only the final argument (the string) is significant on pre-9 Tcl." + }] if {[catch {string is dict {}} errM]} { proc string_is_dict {args} { #compatibility for tcl pre 9.0 @@ -11581,10 +11901,37 @@ tcl::namespace::eval punk::args::lib { # return 1 #} + lappend PUNKARGS [list { + @id -id ::punk::args::lib::flatzip + @cmd -name punk::args::lib::flatzip\ + -summary\ + "Interleave two lists into a single flat list."\ + -help\ + "Returns a flat list alternating elements from l1 and l2. + e.g flatzip {a b c} {1 2 3} -> a 1 b 2 c 3" + @values -min 2 -max 2 + l1 -type list -help\ + "first list" + l2 -type list -help\ + "second list" + }] proc flatzip {l1 l2} { concat {*}[lmap a $l1 b $l2 {list $a $b}] } + lappend PUNKARGS [list { + @id -id ::punk::args::lib::zero_based_posns + @cmd -name punk::args::lib::zero_based_posns\ + -summary\ + "Return the list of zero-based positions 0..count-1."\ + -help\ + "Returns a list of integers from 0 to count-1, + or an empty result if count < 1. + Uses lseq where available (Tcl 8.7+) for efficiency." + @values -min 1 -max 1 + count -type integer -help\ + "number of positions" + }] if {[info commands lseq] ne ""} { #tcl 8.7+ lseq significantly faster, especially for larger ranges #The internal rep can be an 'arithseries' with no string representation @@ -11601,6 +11948,22 @@ tcl::namespace::eval punk::args::lib { } #return list of single column-width marks - possibly with ansi + lappend PUNKARGS [list { + @id -id ::punk::args::lib::choiceinfo_marks + @cmd -name punk::args::lib::choiceinfo_marks\ + -summary\ + "Return display marks for a choice based on its -choiceinfo entries."\ + -help\ + "Returns a list of single column-width marks (possibly containing ANSI) + indicating documentation types (punkargs/ensemble/oo/native etc.) + recorded for the choice in a -choiceinfo dictionary. + Used when rendering choice tables in usage output." + @values -min 2 -max 2 + choice -type string -help\ + "choice name to look up" + choiceinfodict -type dict -help\ + "-choiceinfo dictionary from an argument definition" + }] proc choiceinfo_marks {choice choiceinfodict} { set marks [list] if {[dict exists $choiceinfodict $choice]} { @@ -11866,7 +12229,7 @@ tcl::namespace::eval punk::args::lib { if {[string first \$\{ $templatestring] < 0} { set parts [list $templatestring] } else { - set parts [_parse_tstr_parts $templatestring] + set parts [private::parse_tstr_parts $templatestring] } set textchunks [list] #set expressions [list] @@ -11962,7 +12325,7 @@ tcl::namespace::eval punk::args::lib { return [list $textchunks $params] } args { - #see example in tstr_test_one + #see example in __tstr_test_one return [list $textchunks {*}$params] } string { @@ -12012,13 +12375,13 @@ tcl::namespace::eval punk::args::lib { } } } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { + #developer example/test - single placeholder tstr args where single placeholder must be an int (unexported) + proc __tstr_test_one {args} { set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. + @cmd -name ::punk::args::lib::__tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. example: set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] + __tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] } @values -min 2 -max 2 @@ -12033,7 +12396,7 @@ tcl::namespace::eval punk::args::lib { set result [string cat [lindex $template 0] $where [lindex $template 1]] return $result } - proc _parse_tstr_parts {templatestring} { + proc ::punk::args::private::parse_tstr_parts {templatestring} { if {$templatestring eq ""} { return [list] } @@ -12084,6 +12447,22 @@ tcl::namespace::eval punk::args::lib { } #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. + lappend PUNKARGS [list { + @id -id ::punk::args::lib::indent + @cmd -name punk::args::lib::indent\ + -summary\ + "Indent each non-blank line of a text block."\ + -help\ + "Returns the text with the prefix prepended to each non-blank line. + Whitespace-only lines become empty, and remaining lines are right-trimmed. + Like textutil::adjust::indent - but doesn't strip trailing lines, + and doesn't implement the skip parameter." + @values -min 1 -max 2 + text -type string -help\ + "text block to indent" + prefix -type string -default " " -optional 1 -help\ + "string prepended to each non-blank line" + }] proc indent {text {prefix " "}} { set result [list] foreach line [split $text \n] { @@ -12096,6 +12475,20 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #dedent? + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undent + @cmd -name punk::args::lib::undent\ + -summary\ + "Remove the common leading whitespace from all lines of a text block."\ + -help\ + "Determines the longest common leading whitespace across the non-blank + lines and removes it from every line. + Whitespace-only lines become empty. + Returns the text unchanged if there is no common leading whitespace." + @values -min 1 -max 1 + text -type string -help\ + "text block to undent (dedent)" + }] proc undent {text} { if {$text eq ""} { return "" @@ -12129,6 +12522,24 @@ tcl::namespace::eval punk::args::lib { } #hacky + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undentleader + @cmd -name punk::args::lib::undentleader\ + -summary\ + "Remove a specific leading string (or its common prefix) from all lines of a text block."\ + -help\ + "Like punk::args::lib::undent - but instead of calculating the common + leading whitespace, removes up to the supplied leader string from each + line (the actual removal is the common prefix of the leader and the + non-blank lines, so is never longer than the leader). + The leader is usually whitespace - but doesn't have to be. + Whitespace-only lines become empty." + @values -min 2 -max 2 + text -type string -help\ + "text block to undent" + leader -type string -help\ + "leading string to remove from each line" + }] proc undentleader {text leader} { #leader usually whitespace - but doesn't have to be if {$text eq ""} { @@ -12166,6 +12577,19 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #A version of textutil::string::longestCommonPrefixList + lappend PUNKARGS [list { + @id -id ::punk::args::lib::longestCommonPrefix + @cmd -name punk::args::lib::longestCommonPrefix\ + -summary\ + "Return the longest common prefix of a list of strings."\ + -help\ + "Returns the longest string that is a prefix of every item in the list. + An empty list returns an empty string; a single-item list returns the item. + A version of textutil::string::longestCommonPrefixList." + @values -min 1 -max 1 + items -type list -help\ + "list of strings" + }] proc longestCommonPrefix {items} { if {[llength $items] <= 1} { return [lindex $items 0] @@ -12195,6 +12619,19 @@ tcl::namespace::eval punk::args::lib { #order-preserving #(same as punk::lib) + lappend PUNKARGS [list { + @id -id ::punk::args::lib::lunique + @cmd -name punk::args::lib::lunique\ + -summary\ + "Return the unique elements of a list, preserving order."\ + -help\ + "Returns the list with duplicate elements removed, + keeping the first occurrence of each element in original order. + (same behaviour as punk::lib::lunique)" + @values -min 1 -max 1 + list -type list -help\ + "list to filter" + }] proc lunique {list} { set new {} foreach item $list { @@ -12211,6 +12648,7 @@ tcl::namespace::eval punk::args::lib { } tcl::namespace::eval punk::args::argdocbase { + variable PUNKARGS namespace export * #use a? to test and create literal ansi here rather than relying on punk::ansi package presence #e.g @@ -12218,9 +12656,53 @@ tcl::namespace::eval punk::args::argdocbase { #- bold │SGR 1│sample│␛[1msample #- ──────┼─────┼──────┼────────── #- RESULT│ │sample│␛[1msample + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::B + @cmd -name punk::args::argdocbase::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::N + @cmd -name punk::args::argdocbase::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends a bold span opened with B in tstr-processed -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::I + @cmd -name punk::args::argdocbase::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::NI + @cmd -name punk::args::argdocbase::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends an italic span opened with I in tstr-processed -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic } @@ -12390,7 +12872,7 @@ tcl::namespace::eval punk::args::package { # set PUNKARGS "" #} -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers +lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers ::punk::args::argdocbase # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools @@ -12551,7 +13033,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.3.2 + set version 0.4.0 }] return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.3.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.0.tm similarity index 95% rename from src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.3.2.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.0.tm index ad66351a..06065b32 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.3.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.3.2 +# Application punk::args 0.4.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.3.2] +#[manpage_begin punkshell_module_punk::args 0 0.4.0] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -269,13 +269,66 @@ tcl::namespace::eval punk::args::register { } tcl::namespace::eval ::punk::args {} +#lib & system namespaces must exist for the private namespace path below (populated further down) +tcl::namespace::eval ::punk::args::lib {} +tcl::namespace::eval ::punk::args::system {} + +tcl::namespace::eval ::punk::args::private { + #Internal implementation helpers - not part of the punk::args API and not exported. + #Naming: no underscore prefix - the namespace itself denotes internal status. + #The namespace path allows unqualified calls to siblings in punk::args and its lib/system namespaces + #(mirroring the path set on ::punk::args itself at package-provide time). + tcl::namespace::path [list ::punk::args ::punk::args::lib ::punk::args::system] +} tcl::namespace::eval ::punk::args::helpers { variable PUNKARGS namespace export * + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::B + @cmd -name punk::args::helpers::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::N + @cmd -name punk::args::helpers::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Ends a bold span opened with punk::args::helpers::B in tstr-processed + argdoc -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::I + @cmd -name punk::args::helpers::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::NI + @cmd -name punk::args::helpers::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Ends an italic span opened with punk::args::helpers::I in + tstr-processed argdoc -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic #proc I {} {punk::ansi::a+ italic} #proc B {} {punk::ansi::a+ bold} @@ -1147,11 +1200,26 @@ tcl::namespace::eval punk::args { } + lappend PUNKARGS [list { + @id -id ::punk::args::errorstyle + @cmd -name punk::args::errorstyle\ + -summary\ + "Set or query the default errorstyle (unimplemented)."\ + -help\ + "Set or query the running configuration's default -errorstyle, + as used by punk::args::parse when no explicit -errorstyle + option is supplied in the parse call. + + NOT YET IMPLEMENTED - currently raises an error. + (review - is this an override or a default with respect to + an -errorstyle explicitly passed to punk::args::parse?)" + @values -min 0 -max 1 + errorstyle -type string -optional 1 -choices {debug enhanced standard basic minimal} -help\ + "New default errorstyle. If omitted, the current value is returned." + }] proc errorstyle {args} { #set or query the running config -errorstyle #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal error todo } proc define {args} { @@ -1175,6 +1243,24 @@ tcl::namespace::eval punk::args { return $id } } + lappend PUNKARGS [list { + @id -id ::punk::args::undefine + @cmd -name punk::args::undefine\ + -summary\ + "Remove a definition and its cached data by id."\ + -help\ + "Clears the raw definition and any resolved/cached data for the + supplied id from the punk::args caches. + The definition will no longer be retrievable by id unless redefined + (or rediscovered via update_definitions if it lives in a registered + namespace's PUNKARGS variable). + Emits a note to stderr unless quiet is true." + @values -min 1 -max 2 + id -type string -help\ + "id of the definition to remove (as shown by punk::args::get_ids)" + quiet -type boolean -default 0 -optional 1 -help\ + "Suppress stderr notices about clearing/missing ids" + }] proc undefine {id {quiet 0}} { #review - alias? variable rawdef_cache_about @@ -1209,6 +1295,22 @@ tcl::namespace::eval punk::args { } #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated # In this case we don't see the autoid in order to delete it + lappend PUNKARGS [list { + @id -id ::punk::args::undefine_deflist + @cmd -name punk::args::undefine_deflist\ + -summary\ + "Remove a definition and its cached data by raw definition list."\ + -help\ + "Clears cached data for a definition keyed by the raw definition list + itself rather than by id. This is useful where an automatic id was + generated (e.g by 'punk::args::parse ... withdef ') and the + caller never saw the autoid in order to pass it to punk::args::undefine. + A no-op if the definition list isn't in the cache." + @values -min 1 -max 1 + deflist -type list -help\ + "Raw definition list (list of definition blocks) as originally supplied + to define/parse" + }] proc undefine_deflist {deflist} { variable rawdef_cache_about variable id_cache_rawdef @@ -1222,17 +1324,24 @@ tcl::namespace::eval punk::args { } - proc define2 {args} { - dict get [resolve {*}$args] id - } - lappend PUNKARGS [list { @id -id ::punk::args::resolve - @cmd -name punk::args::resolve -help\ - "" + @cmd -name punk::args::resolve\ + -summary\ + "Resolve raw definition blocks to the internal argument specification dictionary."\ + -help\ + "Takes one or more raw definition blocks (as would be supplied to + punk::args::define) and returns the internal specification dictionary + - the parsed/optimised structure used by the argument parser and the + usage/synopsis renderers. + Results are cached; dynamic definitions (@dynamic, or leading + '-dynamic 1' args) are re-substituted in the caller's definition + namespace as required. + Normally called indirectly (via define/parse/get_spec) - but can be + called directly for inspection of the resolved structure." @values -min 0 -max -1 arg -type any -multiple 1 -help\ - "rawdef line block" + "raw definition block" }] proc resolve {args} { variable rawdef_cache_about @@ -2552,7 +2661,7 @@ tcl::namespace::eval punk::args { set tspec $typespec set optional_clausemember false } - set type_alternatives [_split_type_expression $tspec] + set type_alternatives [private::split_type_expression $tspec] set normlist [list] foreach alt $type_alternatives { set firstword [lindex $alt 0] @@ -2602,7 +2711,7 @@ tcl::namespace::eval punk::args { packageversion {set normtype packageversion} packagerequirement {set normtype packagerequirement} literal { - #value was split out by _split_type_expression + #value was split out by private::split_type_expression set normtype literal([lindex $alt 1]) } literalprefix { @@ -3114,6 +3223,21 @@ tcl::namespace::eval punk::args { #return raw definition list as created with 'define' # - possibly with unresolved dynamic parts + lappend PUNKARGS [list { + @id -id ::punk::args::raw_def + @cmd -name punk::args::raw_def\ + -summary\ + "Return the raw (unresolved) definition list for an id."\ + -help\ + "Returns the raw definition list (list of definition blocks) as + originally supplied for the id. + The id is first resolved via punk::args::real_id, so aliases and + definitions in not-yet-scanned registered namespaces are found. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc raw_def {id} { variable id_cache_rawdef set realid [real_id $id] @@ -3481,6 +3605,22 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::resolved_def_values + @cmd -name punk::args::resolved_def_values\ + -summary\ + "Return textual definitions for the trailing values of a resolved definition."\ + -help\ + "Returns a text block of 'name ' lines for the + @values (trailing positional) arguments of the resolved definition, + optionally filtered by glob patterns on the value names. + See also punk::args::resolved_def for the full definition." + @values -min 1 -max 2 + id -type string -help\ + "id or id alias of the definition" + patternlist -type list -default * -optional 1 -help\ + "List of glob patterns to match against value argument names" + }] proc resolved_def_values {id {patternlist *}} { variable id_cache_rawdef set realid [real_id $id] @@ -3514,6 +3654,20 @@ tcl::namespace::eval punk::args { #proc resolved_def_leaders ?? #proc resolved_def_opts ?? + lappend PUNKARGS [list { + @id -id ::punk::args::get_spec + @cmd -name punk::args::get_spec\ + -summary\ + "Return the resolved internal specification dictionary for an id."\ + -help\ + "Retrieves the raw definition for the id (resolving aliases) and + returns the resolved internal specification dictionary as produced + by punk::args::resolve. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc get_spec {id} { set deflist [raw_def $id] if {$deflist eq ""} { @@ -3524,6 +3678,21 @@ tcl::namespace::eval punk::args { # return [resolve {*}[raw_def $id]] #} } + lappend PUNKARGS [list { + @id -id ::punk::args::is_dynamic + @cmd -name punk::args::is_dynamic\ + -summary\ + "Test whether the definition for an id is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the definition contains an + @dynamic directive (or legacy leading '-dynamic 1' arguments). + Dynamic definitions are re-substituted (dollar variables and + bracketed commands within the text) in the defining namespace on + resolution rather than being resolved once and cached in final form." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc is_dynamic {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3537,6 +3706,25 @@ tcl::namespace::eval punk::args { #@id must be within first 4 lines of first 3 blocks - or assign auto #review - @dynamic block where -id not explicitly set? - disallow? + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_id + @cmd -name punk::args::rawdef_id\ + -summary\ + "Determine the id for a raw definition list."\ + -help\ + "Extracts the id from the @id -id line of a raw definition list. + The @id line must appear within the first 4 lines of one of the first + 3 definition blocks. If no @id line is present (or its -id value is + 'auto') an automatic id of the form autoid_ is generated. + A malformed @id line (found but no retrievable -id value) raises an error. + The -id value undergoes tstr substitution in the caller's context at + the given level." + @values -min 1 -max 2 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + lvl -type integer -default 1 -optional 1 -help\ + "uplevel offset at which to perform tstr substitution of the @id line" + }] proc rawdef_id {rawdef {lvl 1}} { set id "" set found_id_line 0 @@ -3575,6 +3763,20 @@ tcl::namespace::eval punk::args { return $id } #test the rawdef for @dynamic directive + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_is_dynamic + @cmd -name punk::args::rawdef_is_dynamic\ + -summary\ + "Test whether a raw definition list is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the raw definition list contains + a block beginning with the @dynamic directive (or uses the legacy + leading '-dynamic 1' argument form). + See also punk::args::is_dynamic which tests by id." + @values -min 1 -max 1 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + }] proc rawdef_is_dynamic {rawdef} { #temporary - old way set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]}] @@ -3609,6 +3811,21 @@ tcl::namespace::eval punk::args { } #we don't automatically test for (autodef)$id - only direct ids and aliases + lappend PUNKARGS [list { + @id -id ::punk::args::id_exists + @cmd -name punk::args::id_exists\ + -summary\ + "Test whether an id or id alias is already known."\ + -help\ + "Returns a boolean indicating whether the supplied value is a known + definition id or id alias. + Only direct ids and aliases are tested - (autodef) entries are not + automatically checked, and no update_definitions scan is triggered + (compare punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to test" + }] proc id_exists {id} { variable aliases if {[tcl::dict::exists $aliases $id]} { @@ -3617,24 +3834,91 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + lappend PUNKARGS [list { + @id -id ::punk::args::idaliases + @cmd -name punk::args::idaliases\ + -summary\ + "Display the current id alias dictionary."\ + -help\ + "Returns a display (punk::lib::showdict) of the alias -> id mapping + for all id aliases created with punk::args::set_idalias." + @values -min 0 -max 0 + }] proc idaliases {} { variable aliases punk::lib::showdict $aliases } + lappend PUNKARGS [list { + @id -id ::punk::args::set_idalias + @cmd -name punk::args::set_idalias\ + -summary\ + "Create or update an alias for a definition id."\ + -help\ + "Creates (or overwrites) an alias that can be used in place of the + target id in functions such as punk::args::parse (withid), raw_def, + get_spec and usage. + The target id need not exist at the time the alias is created." + @values -min 2 -max 2 + alias -type string -help\ + "alias name" + id -type string -help\ + "target definition id" + }] proc set_idalias {alias id} { variable aliases dict set aliases $alias $id } + lappend PUNKARGS [list { + @id -id ::punk::args::unset_idalias + @cmd -name punk::args::unset_idalias\ + -summary\ + "Remove an id alias."\ + -help\ + "Removes an alias created with punk::args::set_idalias. + A no-op if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to remove" + }] proc unset_idalias {alias} { variable aliases dict unset aliases $alias } + lappend PUNKARGS [list { + @id -id ::punk::args::get_idalias + @cmd -name punk::args::get_idalias\ + -summary\ + "Return the target id for an alias."\ + -help\ + "Returns the id that the alias points to, + or an empty string if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to look up" + }] proc get_idalias {alias} { variable aliases if {[dict exists $aliases $alias]} { return [tcl::dict::get $aliases $alias] } } + lappend PUNKARGS [list { + @id -id ::punk::args::id_query + @cmd -name punk::args::id_query\ + -summary\ + "Developer report of cache state for a definition id."\ + -help\ + "Returns a text report showing the raw definition, id info + (dynamic flag, defining namespace) and argdata cache entries + for the id - including a warning if the id is unexpectedly + present under multiple rawdef keys. + Intended for interactive/debug use. + Returns an empty string if the id isn't directly present + (aliases are not followed - see punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "definition id to report on" + }] proc id_query {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3689,6 +3973,24 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::real_id + @cmd -name punk::args::real_id\ + -summary\ + "Resolve an id or alias to the actual id under which a definition is stored."\ + -help\ + "Resolves aliases and returns the id under which the definition is + actually stored - which may be the supplied id itself, the alias + target, or an (autodef) entry created by the introspection + facilities. + If the id isn't found, punk::args::update_definitions is called for + the id's namespace qualifiers so that definitions in newly loaded + packages are discovered. + Returns an empty string if no definition can be found." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to resolve" + }] proc real_id {id} { variable id_cache_rawdef variable aliases @@ -3721,6 +4023,20 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::status + @cmd -name punk::args::status\ + -summary\ + "Report scan/load status of registered definition namespaces."\ + -help\ + "Returns a table of namespaces registered in + punk::args::register::NAMESPACES showing, for each, how many ids were + found when scanned, how many definitions have been loaded, and the + time taken (microseconds) for the scan/load passes. + Definitions are scanned and loaded lazily - a blank row indicates a + package whose definitions haven't been needed yet." + @values -min 0 -max 0 + }] proc status {} { upvar ::punk::args::register::NAMESPACES registered upvar ::punk::args::register::loaded_packages loaded_packages @@ -3986,29 +4302,29 @@ tcl::namespace::eval punk::args { # -------------------------------------- - #test of Get_caller + #developer diagnostics - test of Get_caller (unexported - leading underscores exclude from export pattern) lappend PUNKARGS [list { - @id -id ::punk::args::test1 + @id -id ::punk::args::__test1 @values -min 0 -max 0 }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args + proc __test_get_dict {args} { + punk::args::get_dict [punk::args::raw_def ::punk::args::__test1] $args } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args + proc __test_get_by_id {args} { + punk::args::get_by_id ::punk::args::__test1 $args } #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { + proc __test_callers {args} { if {![llength $args]} { puts "these test functions accept no arguments" puts "Call with arg(s) to compare error output" } - if {[catch {test_get_dict {*}$args} errM]} { + if {[catch {__test_get_dict {*}$args} errM]} { puts $errM } puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { + if {[catch {__test_get_by_id {*}$args} errM]} { puts $errM } return done @@ -4073,8 +4389,9 @@ tcl::namespace::eval punk::args { variable arg_error_CLR_error array set arg_error_CLR_error {} - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR + proc private::argerror_load_colours {{forcereload 0}} { + #colour state arrays live in the parent punk::args namespace (shared with arg_error) + upvar #0 ::punk::args::arg_error_CLR arg_error_CLR #todo - option for reload/retry? if {!$forcereload && [array size arg_error_CLR] > 0} { return @@ -4107,7 +4424,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR(ansibase_header) [a+ bold] set arg_error_CLR(ansibase_body) [a+ white] set arg_error_CLR(parsekey_hint) [a+ term-grey46] - variable arg_error_CLR_nocolour + upvar #0 ::punk::args::arg_error_CLR_nocolour arg_error_CLR_nocolour #array set arg_error_CLR_nocolour {} set arg_error_CLR_nocolour(errormsg) [a+ bold] set arg_error_CLR_nocolour(title) [a+ bold] @@ -4123,7 +4440,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_nocolour(ansibase_header) [a+ bold] set arg_error_CLR_nocolour(ansibase_body) "" set arg_error_CLR_nocolour(parsekey_hint) "" - variable arg_error_CLR_info + upvar #0 ::punk::args::arg_error_CLR_info arg_error_CLR_info #array set arg_error_CLR_info {} set arg_error_CLR_info(errormsg) [a+ brightred bold] set arg_error_CLR_info(title) [a+ brightyellow bold] @@ -4134,7 +4451,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_info(ansiborder) [a+ term-grey23 bold] set arg_error_CLR_info(ansibase_header) [a+ cyan] set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error + upvar #0 ::punk::args::arg_error_CLR_error arg_error_CLR_error #array set arg_error_CLR_error {} set arg_error_CLR_error(errormsg) [a+ brightred bold] set arg_error_CLR_error(title) [a+ brightcyan bold] @@ -4198,7 +4515,7 @@ tcl::namespace::eval punk::args { set forcereload 1 } } - _argerror_load_colours $forcereload + private::argerror_load_colours $forcereload if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -5757,58 +6074,11 @@ tcl::namespace::eval punk::args { } return $result } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - #return number of values we can assign to cater for variable length clauses such as: # {"elseif" expr "?then?" body} #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { + proc private::get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { set ARG_INFO [dict get $formdict ARG_INFO] set all_remaining [lrange $values $idx end] set thisname [lindex $names $nameidx] @@ -5850,11 +6120,11 @@ tcl::namespace::eval punk::args { #todo - support complex type members such as -type {{literal a|b} int OR} #for now - require llength 1 - simple type such as -type {literal(ab)|int} if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + error "private::get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" } #foreach tp_alternative [split $tp |] {} - set tp_alternatives [_split_type_expression $tp] + set tp_alternatives [private::split_type_expression $tp] foreach tp_alternative $tp_alternatives { switch -exact -- [lindex $tp_alternative 0] { literal { @@ -5926,7 +6196,7 @@ tcl::namespace::eval punk::args { } } else { - #todo - use _split_type_expression + #todo - use private::split_type_expression #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) @@ -5952,7 +6222,7 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - puts "_get_dict_can_assign_value: checking tp '$tp' against value '$rv'" + puts "private::get_dict_can_assign_value: checking tp '$tp' against value '$rv'" switch -glob -- $tp { "literal(*" { set litmatch [string range $tp 8 end-1] @@ -6094,7 +6364,7 @@ tcl::namespace::eval punk::args { set ctg_stringendswith [list] set ctg_other [list] #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { + foreach tp_alternative [private::split_type_expression $tp] { #JJJJ lassign $tp_alternative t textra switch -exact -- $t { @@ -6244,17 +6514,17 @@ tcl::namespace::eval punk::args { #so that they are not subject to type validation #such elements shouldn't be subject to validation if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - #puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed 0 resultlist {} typelist $thistype] } - #puts ">>>> _get_dict_can_assign_value $d" + #puts ">>>> private::get_dict_can_assign_value $d" return $d } - #_split_type_expression + #private::split_type_expression #only handles toplevel 'or' for type_expression e.g int|char #we have no mechanism for & - (although it would be useful) #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) @@ -6273,7 +6543,7 @@ tcl::namespace::eval punk::args { #mode -type {literalprefix text1 literalprefix text2 OR} #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - proc _split_type_expression {type_expression} { + proc private::split_type_expression {type_expression} { if {[llength $type_expression] == 1} { #simple expressions of length one must be splittable on | #disallowed: things such as literal(|) or literal(x|etc)|int @@ -6289,7 +6559,7 @@ tcl::namespace::eval punk::args { } return $type_alternatives } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + error "private::split_type_expression unimplemented: type_expression length > 1 '$type_expression'" #todo #RPN reverse polish notation #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} @@ -6309,14 +6579,14 @@ tcl::namespace::eval punk::args { set expect_separator 0 continue } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + error "private::split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" } } switch -exact -- $word { literal - literalprefix - stringstartswith - stringendswith - stringcontains { if {$w+1 > [llength $type_expression]} { #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + error "private::split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" } lappend type_alternatives [list $word [lindex $type_expression $w+1]] incr w ;#consume arg @@ -6334,12 +6604,12 @@ tcl::namespace::eval punk::args { } #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ###proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { ### #set type $type_expression ;#todo - 'split' on | ### set vlist $clausevalues_raw ### set vlist_check $clausevalues_check - ### set type_alternatives [_split_type_expression $type_expression] + ### set type_alternatives [private::split_type_expression $type_expression] ### #each type_alternative is a list of varying length depending on arguments supported by first word. ### #TODO? ### #single element types: int double string etc @@ -6736,7 +7006,7 @@ tcl::namespace::eval punk::args { #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname #common basic case: list of single item being a single value clause. #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { #default_type_expression is for the chosen clausecolumn #if {$argname eq "frametype"} { #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" @@ -6747,7 +7017,7 @@ tcl::namespace::eval punk::args { set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - set default_type_alternatives [_split_type_expression $default_type_expression] + set default_type_alternatives [private::split_type_expression $default_type_expression] #--------------------- #pre-calc prefix sets based on the default. set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] @@ -6779,7 +7049,7 @@ tcl::namespace::eval punk::args { set c_idx -1 foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set col_type_alternatives [private::split_type_expression $clause_column_type_expression] set firstany [lsearch -exact $col_type_alternatives any] if {$firstany > -1} { lset clause_results $c_idx $firstany 1 @@ -7461,7 +7731,7 @@ tcl::namespace::eval punk::args { # #} # } - # set type_alternatives [_split_type_expression $type_expression] + # set type_alternatives [private::split_type_expression $type_expression] # #each type_alternative is a list of varying length depending on arguments supported by first word. # #TODO? # #single element types: int double string etc @@ -8141,18 +8411,40 @@ tcl::namespace::eval punk::args { #G-040: the single implementation of choice-word matching - shared by argument parsing #(get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so 'i ' #resolution can never diverge from what parsing accepts. - #Arguments: - # word - the supplied word (callers pass the check-form, e.g. ansistripped when applicable) - # nocase - value of -nocase for the argument - # allchoices - flat list of -choices plus all -choicegroups members (dups allowed) - # choicealiases - -choicealiases dict (alias -> canonical choice) - # choiceprefix - value of -choiceprefix - # denylist - -choiceprefixdenylist (full word required for these names) - # reservelist - -choiceprefixreservelist (phantom prefix-calculation members) - #Returns dict: - # matched - boolean: word identifies a choice (directly, by unique prefix, or via an alias) - # exact - boolean: the raw stored word already equals the resulting choice (no rewrite needed) - # canonical - the resulting choice value (empty when not matched) + lappend PUNKARGS [list { + @id -id ::punk::args::choiceword_match + @cmd -name punk::args::choiceword_match\ + -summary\ + "Match a supplied word against an argument's choices (the shared choice-matching implementation)."\ + -help\ + "The single implementation of choice-word matching - shared by argument + parsing (get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so + 'i ' resolution can never diverge from what parsing accepts. + (G-040) + + Returns a dict with keys: + matched - boolean: word identifies a choice (directly, by unique + prefix, or via an alias) + exact - boolean: the raw stored word already equals the resulting + choice (no rewrite needed) + canonical - the resulting choice value (empty when not matched)" + @values -min 7 -max 7 + word -type string -help\ + "The supplied word. + Callers pass the check-form, e.g ansistripped when applicable" + nocase -type boolean -help\ + "value of -nocase for the argument" + allchoices -type list -help\ + "flat list of -choices plus all -choicegroups members (dups allowed)" + choicealiases -type dict -help\ + "-choicealiases dict (alias -> canonical choice)" + choiceprefix -type boolean -help\ + "value of -choiceprefix" + denylist -type list -help\ + "-choiceprefixdenylist (full word required for these names)" + reservelist -type list -help\ + "-choiceprefixreservelist (phantom prefix-calculation members)" + }] proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} { set aliasnames [tcl::dict::keys $choicealiases] set has_choicealiases [expr {[llength $aliasnames] > 0}] @@ -8291,6 +8583,30 @@ tcl::namespace::eval punk::args { #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. #only supports -flag val pairs, not solo options #If an option is supplied multiple times - only the last value is used. + lappend PUNKARGS [list { + @id -id ::punk::args::get_dict + @cmd -name punk::args::get_dict\ + -summary\ + "Parse an argument list against a raw definition - the core parsing engine."\ + -help\ + "The core argument parsing/validation engine underlying + punk::args::parse (which is the preferred API for procs). + Parses rawargs against the supplied raw definition list and returns + a dict with keys: leaders, opts, values (argument name/value dicts) + and received (names of arguments actually supplied, with ordinal + clause info where applicable), plus solos. + Raises an error (rendering a usage table according to the errorstyle + in effect) if the arguments don't conform to the definition." + @leaders -min 2 -max 2 + deflist -type list -help\ + "Raw definition list (list of definition blocks)" + rawargs -type list -help\ + "list of arguments to parse against the definition" + @opts + -form -default * -help\ + "Ordinal index or name of the command form to parse against. + The default * considers all forms." + }] proc get_dict {deflist rawargs args} { #see arg_error regarding considerations around unhappy-path performance @@ -8651,7 +8967,7 @@ tcl::namespace::eval punk::args { incr tentative_idx } } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set assign_d [private::get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9579,7 +9895,7 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during private::get_dict_can_assign_value set start_position $positionalidx @@ -9592,14 +9908,14 @@ tcl::namespace::eval punk::args { set leadertypelist [tcl::dict::get $argstate $leadername -type] ;#often a single type, but can be a list of types (possibly with some optional) for a type that is a clause accepting multiple values. set leader_clause_size [llength $leadertypelist] - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] + set assign_d [private::get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] if {$consumed == 0} { if {[tcl::dict::get $argstate $leadername -optional]} { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 1?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9607,12 +9923,12 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg } else { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9803,7 +10119,7 @@ tcl::namespace::eval punk::args { set valtypelist [tcl::dict::get $argstate $valname -type] set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] + set assign_d [private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] ;#count set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9818,7 +10134,7 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { @@ -10545,8 +10861,8 @@ tcl::namespace::eval punk::args { #set typespec [lindex $typelist $clausecolumn] #todo - handle type-alternates e.g -type char|double #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #private::check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + private::check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs #------------------------------------------------------------------------------------ @@ -10658,7 +10974,7 @@ tcl::namespace::eval punk::args { } - proc _synopsis_form_arg_display {formdict argname} { + proc private::synopsis_form_arg_display {formdict argname} { #non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings set I "\x1b\[3m" ;#[punk::ansi::a+ italic] set NI "\x1b\[23m" ;# [punk::ansi::a+ noitalic] @@ -10699,7 +11015,7 @@ tcl::namespace::eval punk::args { } else { #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set tp_alternative_word1 [lindex $tp_alternative 0] set match [lindex $tp_alternative 1] @@ -10901,7 +11217,7 @@ tcl::namespace::eval punk::args { set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set arginfo [dict get $forminfo ARG_INFO $argname] @@ -10945,7 +11261,7 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set match [lindex $tp_alternative 1] switch -- [lindex $tp_alternative 0] { @@ -11129,7 +11445,7 @@ tcl::namespace::eval punk::args { foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set ARGD [dict create argname $argname class value] @@ -11522,18 +11838,6 @@ tcl::namespace::eval punk::args { return $argdef } - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - #*** !doctools #[list_end] [comment {--- end definitions namespace punk::args ---}] } @@ -11545,7 +11849,7 @@ tcl::namespace::eval punk::args { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::args::lib { variable PUNKARGS - tcl::namespace::export * + tcl::namespace::export {[a-z]*} tcl::namespace::path [list [tcl::namespace::parent]] #*** !doctools #[subsection {Namespace punk::args::lib}] @@ -11553,6 +11857,22 @@ tcl::namespace::eval punk::args::lib { #[list_begin definitions] #tcl86 compat for string is dict - but without -strict or -failindex options + lappend PUNKARGS [list { + @id -id ::punk::args::lib::string_is_dict + @cmd -name punk::args::lib::string_is_dict\ + -summary\ + "Test whether a string is a valid dict ('string is dict' compatibility wrapper)."\ + -help\ + "Returns a boolean indicating whether the string can be interpreted + as a dictionary (an even-length list). + On Tcl 9+ this is a thin wrapper over 'string is dict'. + On earlier Tcl versions a compatible fallback is used which accepts + but ignores any leading options (-strict/-failindex are NOT implemented)." + @values -min 1 -max -1 + arg -type any -multiple 1 -help\ + "optional 'string is dict' options followed by the string to test. + Only the final argument (the string) is significant on pre-9 Tcl." + }] if {[catch {string is dict {}} errM]} { proc string_is_dict {args} { #compatibility for tcl pre 9.0 @@ -11581,10 +11901,37 @@ tcl::namespace::eval punk::args::lib { # return 1 #} + lappend PUNKARGS [list { + @id -id ::punk::args::lib::flatzip + @cmd -name punk::args::lib::flatzip\ + -summary\ + "Interleave two lists into a single flat list."\ + -help\ + "Returns a flat list alternating elements from l1 and l2. + e.g flatzip {a b c} {1 2 3} -> a 1 b 2 c 3" + @values -min 2 -max 2 + l1 -type list -help\ + "first list" + l2 -type list -help\ + "second list" + }] proc flatzip {l1 l2} { concat {*}[lmap a $l1 b $l2 {list $a $b}] } + lappend PUNKARGS [list { + @id -id ::punk::args::lib::zero_based_posns + @cmd -name punk::args::lib::zero_based_posns\ + -summary\ + "Return the list of zero-based positions 0..count-1."\ + -help\ + "Returns a list of integers from 0 to count-1, + or an empty result if count < 1. + Uses lseq where available (Tcl 8.7+) for efficiency." + @values -min 1 -max 1 + count -type integer -help\ + "number of positions" + }] if {[info commands lseq] ne ""} { #tcl 8.7+ lseq significantly faster, especially for larger ranges #The internal rep can be an 'arithseries' with no string representation @@ -11601,6 +11948,22 @@ tcl::namespace::eval punk::args::lib { } #return list of single column-width marks - possibly with ansi + lappend PUNKARGS [list { + @id -id ::punk::args::lib::choiceinfo_marks + @cmd -name punk::args::lib::choiceinfo_marks\ + -summary\ + "Return display marks for a choice based on its -choiceinfo entries."\ + -help\ + "Returns a list of single column-width marks (possibly containing ANSI) + indicating documentation types (punkargs/ensemble/oo/native etc.) + recorded for the choice in a -choiceinfo dictionary. + Used when rendering choice tables in usage output." + @values -min 2 -max 2 + choice -type string -help\ + "choice name to look up" + choiceinfodict -type dict -help\ + "-choiceinfo dictionary from an argument definition" + }] proc choiceinfo_marks {choice choiceinfodict} { set marks [list] if {[dict exists $choiceinfodict $choice]} { @@ -11866,7 +12229,7 @@ tcl::namespace::eval punk::args::lib { if {[string first \$\{ $templatestring] < 0} { set parts [list $templatestring] } else { - set parts [_parse_tstr_parts $templatestring] + set parts [private::parse_tstr_parts $templatestring] } set textchunks [list] #set expressions [list] @@ -11962,7 +12325,7 @@ tcl::namespace::eval punk::args::lib { return [list $textchunks $params] } args { - #see example in tstr_test_one + #see example in __tstr_test_one return [list $textchunks {*}$params] } string { @@ -12012,13 +12375,13 @@ tcl::namespace::eval punk::args::lib { } } } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { + #developer example/test - single placeholder tstr args where single placeholder must be an int (unexported) + proc __tstr_test_one {args} { set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. + @cmd -name ::punk::args::lib::__tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. example: set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] + __tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] } @values -min 2 -max 2 @@ -12033,7 +12396,7 @@ tcl::namespace::eval punk::args::lib { set result [string cat [lindex $template 0] $where [lindex $template 1]] return $result } - proc _parse_tstr_parts {templatestring} { + proc ::punk::args::private::parse_tstr_parts {templatestring} { if {$templatestring eq ""} { return [list] } @@ -12084,6 +12447,22 @@ tcl::namespace::eval punk::args::lib { } #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. + lappend PUNKARGS [list { + @id -id ::punk::args::lib::indent + @cmd -name punk::args::lib::indent\ + -summary\ + "Indent each non-blank line of a text block."\ + -help\ + "Returns the text with the prefix prepended to each non-blank line. + Whitespace-only lines become empty, and remaining lines are right-trimmed. + Like textutil::adjust::indent - but doesn't strip trailing lines, + and doesn't implement the skip parameter." + @values -min 1 -max 2 + text -type string -help\ + "text block to indent" + prefix -type string -default " " -optional 1 -help\ + "string prepended to each non-blank line" + }] proc indent {text {prefix " "}} { set result [list] foreach line [split $text \n] { @@ -12096,6 +12475,20 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #dedent? + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undent + @cmd -name punk::args::lib::undent\ + -summary\ + "Remove the common leading whitespace from all lines of a text block."\ + -help\ + "Determines the longest common leading whitespace across the non-blank + lines and removes it from every line. + Whitespace-only lines become empty. + Returns the text unchanged if there is no common leading whitespace." + @values -min 1 -max 1 + text -type string -help\ + "text block to undent (dedent)" + }] proc undent {text} { if {$text eq ""} { return "" @@ -12129,6 +12522,24 @@ tcl::namespace::eval punk::args::lib { } #hacky + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undentleader + @cmd -name punk::args::lib::undentleader\ + -summary\ + "Remove a specific leading string (or its common prefix) from all lines of a text block."\ + -help\ + "Like punk::args::lib::undent - but instead of calculating the common + leading whitespace, removes up to the supplied leader string from each + line (the actual removal is the common prefix of the leader and the + non-blank lines, so is never longer than the leader). + The leader is usually whitespace - but doesn't have to be. + Whitespace-only lines become empty." + @values -min 2 -max 2 + text -type string -help\ + "text block to undent" + leader -type string -help\ + "leading string to remove from each line" + }] proc undentleader {text leader} { #leader usually whitespace - but doesn't have to be if {$text eq ""} { @@ -12166,6 +12577,19 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #A version of textutil::string::longestCommonPrefixList + lappend PUNKARGS [list { + @id -id ::punk::args::lib::longestCommonPrefix + @cmd -name punk::args::lib::longestCommonPrefix\ + -summary\ + "Return the longest common prefix of a list of strings."\ + -help\ + "Returns the longest string that is a prefix of every item in the list. + An empty list returns an empty string; a single-item list returns the item. + A version of textutil::string::longestCommonPrefixList." + @values -min 1 -max 1 + items -type list -help\ + "list of strings" + }] proc longestCommonPrefix {items} { if {[llength $items] <= 1} { return [lindex $items 0] @@ -12195,6 +12619,19 @@ tcl::namespace::eval punk::args::lib { #order-preserving #(same as punk::lib) + lappend PUNKARGS [list { + @id -id ::punk::args::lib::lunique + @cmd -name punk::args::lib::lunique\ + -summary\ + "Return the unique elements of a list, preserving order."\ + -help\ + "Returns the list with duplicate elements removed, + keeping the first occurrence of each element in original order. + (same behaviour as punk::lib::lunique)" + @values -min 1 -max 1 + list -type list -help\ + "list to filter" + }] proc lunique {list} { set new {} foreach item $list { @@ -12211,6 +12648,7 @@ tcl::namespace::eval punk::args::lib { } tcl::namespace::eval punk::args::argdocbase { + variable PUNKARGS namespace export * #use a? to test and create literal ansi here rather than relying on punk::ansi package presence #e.g @@ -12218,9 +12656,53 @@ tcl::namespace::eval punk::args::argdocbase { #- bold │SGR 1│sample│␛[1msample #- ──────┼─────┼──────┼────────── #- RESULT│ │sample│␛[1msample + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::B + @cmd -name punk::args::argdocbase::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::N + @cmd -name punk::args::argdocbase::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends a bold span opened with B in tstr-processed -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::I + @cmd -name punk::args::argdocbase::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::NI + @cmd -name punk::args::argdocbase::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends an italic span opened with I in tstr-processed -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic } @@ -12390,7 +12872,7 @@ tcl::namespace::eval punk::args::package { # set PUNKARGS "" #} -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers +lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers ::punk::args::argdocbase # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools @@ -12551,7 +13033,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.3.2 + set version 0.4.0 }] return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.3.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.0.tm similarity index 95% rename from src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.3.2.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.0.tm index ad66351a..06065b32 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.3.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.3.2 +# Application punk::args 0.4.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.3.2] +#[manpage_begin punkshell_module_punk::args 0 0.4.0] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -269,13 +269,66 @@ tcl::namespace::eval punk::args::register { } tcl::namespace::eval ::punk::args {} +#lib & system namespaces must exist for the private namespace path below (populated further down) +tcl::namespace::eval ::punk::args::lib {} +tcl::namespace::eval ::punk::args::system {} + +tcl::namespace::eval ::punk::args::private { + #Internal implementation helpers - not part of the punk::args API and not exported. + #Naming: no underscore prefix - the namespace itself denotes internal status. + #The namespace path allows unqualified calls to siblings in punk::args and its lib/system namespaces + #(mirroring the path set on ::punk::args itself at package-provide time). + tcl::namespace::path [list ::punk::args ::punk::args::lib ::punk::args::system] +} tcl::namespace::eval ::punk::args::helpers { variable PUNKARGS namespace export * + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::B + @cmd -name punk::args::helpers::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::N + @cmd -name punk::args::helpers::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Ends a bold span opened with punk::args::helpers::B in tstr-processed + argdoc -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::I + @cmd -name punk::args::helpers::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::NI + @cmd -name punk::args::helpers::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Ends an italic span opened with punk::args::helpers::I in + tstr-processed argdoc -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic #proc I {} {punk::ansi::a+ italic} #proc B {} {punk::ansi::a+ bold} @@ -1147,11 +1200,26 @@ tcl::namespace::eval punk::args { } + lappend PUNKARGS [list { + @id -id ::punk::args::errorstyle + @cmd -name punk::args::errorstyle\ + -summary\ + "Set or query the default errorstyle (unimplemented)."\ + -help\ + "Set or query the running configuration's default -errorstyle, + as used by punk::args::parse when no explicit -errorstyle + option is supplied in the parse call. + + NOT YET IMPLEMENTED - currently raises an error. + (review - is this an override or a default with respect to + an -errorstyle explicitly passed to punk::args::parse?)" + @values -min 0 -max 1 + errorstyle -type string -optional 1 -choices {debug enhanced standard basic minimal} -help\ + "New default errorstyle. If omitted, the current value is returned." + }] proc errorstyle {args} { #set or query the running config -errorstyle #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal error todo } proc define {args} { @@ -1175,6 +1243,24 @@ tcl::namespace::eval punk::args { return $id } } + lappend PUNKARGS [list { + @id -id ::punk::args::undefine + @cmd -name punk::args::undefine\ + -summary\ + "Remove a definition and its cached data by id."\ + -help\ + "Clears the raw definition and any resolved/cached data for the + supplied id from the punk::args caches. + The definition will no longer be retrievable by id unless redefined + (or rediscovered via update_definitions if it lives in a registered + namespace's PUNKARGS variable). + Emits a note to stderr unless quiet is true." + @values -min 1 -max 2 + id -type string -help\ + "id of the definition to remove (as shown by punk::args::get_ids)" + quiet -type boolean -default 0 -optional 1 -help\ + "Suppress stderr notices about clearing/missing ids" + }] proc undefine {id {quiet 0}} { #review - alias? variable rawdef_cache_about @@ -1209,6 +1295,22 @@ tcl::namespace::eval punk::args { } #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated # In this case we don't see the autoid in order to delete it + lappend PUNKARGS [list { + @id -id ::punk::args::undefine_deflist + @cmd -name punk::args::undefine_deflist\ + -summary\ + "Remove a definition and its cached data by raw definition list."\ + -help\ + "Clears cached data for a definition keyed by the raw definition list + itself rather than by id. This is useful where an automatic id was + generated (e.g by 'punk::args::parse ... withdef ') and the + caller never saw the autoid in order to pass it to punk::args::undefine. + A no-op if the definition list isn't in the cache." + @values -min 1 -max 1 + deflist -type list -help\ + "Raw definition list (list of definition blocks) as originally supplied + to define/parse" + }] proc undefine_deflist {deflist} { variable rawdef_cache_about variable id_cache_rawdef @@ -1222,17 +1324,24 @@ tcl::namespace::eval punk::args { } - proc define2 {args} { - dict get [resolve {*}$args] id - } - lappend PUNKARGS [list { @id -id ::punk::args::resolve - @cmd -name punk::args::resolve -help\ - "" + @cmd -name punk::args::resolve\ + -summary\ + "Resolve raw definition blocks to the internal argument specification dictionary."\ + -help\ + "Takes one or more raw definition blocks (as would be supplied to + punk::args::define) and returns the internal specification dictionary + - the parsed/optimised structure used by the argument parser and the + usage/synopsis renderers. + Results are cached; dynamic definitions (@dynamic, or leading + '-dynamic 1' args) are re-substituted in the caller's definition + namespace as required. + Normally called indirectly (via define/parse/get_spec) - but can be + called directly for inspection of the resolved structure." @values -min 0 -max -1 arg -type any -multiple 1 -help\ - "rawdef line block" + "raw definition block" }] proc resolve {args} { variable rawdef_cache_about @@ -2552,7 +2661,7 @@ tcl::namespace::eval punk::args { set tspec $typespec set optional_clausemember false } - set type_alternatives [_split_type_expression $tspec] + set type_alternatives [private::split_type_expression $tspec] set normlist [list] foreach alt $type_alternatives { set firstword [lindex $alt 0] @@ -2602,7 +2711,7 @@ tcl::namespace::eval punk::args { packageversion {set normtype packageversion} packagerequirement {set normtype packagerequirement} literal { - #value was split out by _split_type_expression + #value was split out by private::split_type_expression set normtype literal([lindex $alt 1]) } literalprefix { @@ -3114,6 +3223,21 @@ tcl::namespace::eval punk::args { #return raw definition list as created with 'define' # - possibly with unresolved dynamic parts + lappend PUNKARGS [list { + @id -id ::punk::args::raw_def + @cmd -name punk::args::raw_def\ + -summary\ + "Return the raw (unresolved) definition list for an id."\ + -help\ + "Returns the raw definition list (list of definition blocks) as + originally supplied for the id. + The id is first resolved via punk::args::real_id, so aliases and + definitions in not-yet-scanned registered namespaces are found. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc raw_def {id} { variable id_cache_rawdef set realid [real_id $id] @@ -3481,6 +3605,22 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::resolved_def_values + @cmd -name punk::args::resolved_def_values\ + -summary\ + "Return textual definitions for the trailing values of a resolved definition."\ + -help\ + "Returns a text block of 'name ' lines for the + @values (trailing positional) arguments of the resolved definition, + optionally filtered by glob patterns on the value names. + See also punk::args::resolved_def for the full definition." + @values -min 1 -max 2 + id -type string -help\ + "id or id alias of the definition" + patternlist -type list -default * -optional 1 -help\ + "List of glob patterns to match against value argument names" + }] proc resolved_def_values {id {patternlist *}} { variable id_cache_rawdef set realid [real_id $id] @@ -3514,6 +3654,20 @@ tcl::namespace::eval punk::args { #proc resolved_def_leaders ?? #proc resolved_def_opts ?? + lappend PUNKARGS [list { + @id -id ::punk::args::get_spec + @cmd -name punk::args::get_spec\ + -summary\ + "Return the resolved internal specification dictionary for an id."\ + -help\ + "Retrieves the raw definition for the id (resolving aliases) and + returns the resolved internal specification dictionary as produced + by punk::args::resolve. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc get_spec {id} { set deflist [raw_def $id] if {$deflist eq ""} { @@ -3524,6 +3678,21 @@ tcl::namespace::eval punk::args { # return [resolve {*}[raw_def $id]] #} } + lappend PUNKARGS [list { + @id -id ::punk::args::is_dynamic + @cmd -name punk::args::is_dynamic\ + -summary\ + "Test whether the definition for an id is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the definition contains an + @dynamic directive (or legacy leading '-dynamic 1' arguments). + Dynamic definitions are re-substituted (dollar variables and + bracketed commands within the text) in the defining namespace on + resolution rather than being resolved once and cached in final form." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc is_dynamic {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3537,6 +3706,25 @@ tcl::namespace::eval punk::args { #@id must be within first 4 lines of first 3 blocks - or assign auto #review - @dynamic block where -id not explicitly set? - disallow? + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_id + @cmd -name punk::args::rawdef_id\ + -summary\ + "Determine the id for a raw definition list."\ + -help\ + "Extracts the id from the @id -id line of a raw definition list. + The @id line must appear within the first 4 lines of one of the first + 3 definition blocks. If no @id line is present (or its -id value is + 'auto') an automatic id of the form autoid_ is generated. + A malformed @id line (found but no retrievable -id value) raises an error. + The -id value undergoes tstr substitution in the caller's context at + the given level." + @values -min 1 -max 2 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + lvl -type integer -default 1 -optional 1 -help\ + "uplevel offset at which to perform tstr substitution of the @id line" + }] proc rawdef_id {rawdef {lvl 1}} { set id "" set found_id_line 0 @@ -3575,6 +3763,20 @@ tcl::namespace::eval punk::args { return $id } #test the rawdef for @dynamic directive + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_is_dynamic + @cmd -name punk::args::rawdef_is_dynamic\ + -summary\ + "Test whether a raw definition list is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the raw definition list contains + a block beginning with the @dynamic directive (or uses the legacy + leading '-dynamic 1' argument form). + See also punk::args::is_dynamic which tests by id." + @values -min 1 -max 1 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + }] proc rawdef_is_dynamic {rawdef} { #temporary - old way set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]}] @@ -3609,6 +3811,21 @@ tcl::namespace::eval punk::args { } #we don't automatically test for (autodef)$id - only direct ids and aliases + lappend PUNKARGS [list { + @id -id ::punk::args::id_exists + @cmd -name punk::args::id_exists\ + -summary\ + "Test whether an id or id alias is already known."\ + -help\ + "Returns a boolean indicating whether the supplied value is a known + definition id or id alias. + Only direct ids and aliases are tested - (autodef) entries are not + automatically checked, and no update_definitions scan is triggered + (compare punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to test" + }] proc id_exists {id} { variable aliases if {[tcl::dict::exists $aliases $id]} { @@ -3617,24 +3834,91 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + lappend PUNKARGS [list { + @id -id ::punk::args::idaliases + @cmd -name punk::args::idaliases\ + -summary\ + "Display the current id alias dictionary."\ + -help\ + "Returns a display (punk::lib::showdict) of the alias -> id mapping + for all id aliases created with punk::args::set_idalias." + @values -min 0 -max 0 + }] proc idaliases {} { variable aliases punk::lib::showdict $aliases } + lappend PUNKARGS [list { + @id -id ::punk::args::set_idalias + @cmd -name punk::args::set_idalias\ + -summary\ + "Create or update an alias for a definition id."\ + -help\ + "Creates (or overwrites) an alias that can be used in place of the + target id in functions such as punk::args::parse (withid), raw_def, + get_spec and usage. + The target id need not exist at the time the alias is created." + @values -min 2 -max 2 + alias -type string -help\ + "alias name" + id -type string -help\ + "target definition id" + }] proc set_idalias {alias id} { variable aliases dict set aliases $alias $id } + lappend PUNKARGS [list { + @id -id ::punk::args::unset_idalias + @cmd -name punk::args::unset_idalias\ + -summary\ + "Remove an id alias."\ + -help\ + "Removes an alias created with punk::args::set_idalias. + A no-op if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to remove" + }] proc unset_idalias {alias} { variable aliases dict unset aliases $alias } + lappend PUNKARGS [list { + @id -id ::punk::args::get_idalias + @cmd -name punk::args::get_idalias\ + -summary\ + "Return the target id for an alias."\ + -help\ + "Returns the id that the alias points to, + or an empty string if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to look up" + }] proc get_idalias {alias} { variable aliases if {[dict exists $aliases $alias]} { return [tcl::dict::get $aliases $alias] } } + lappend PUNKARGS [list { + @id -id ::punk::args::id_query + @cmd -name punk::args::id_query\ + -summary\ + "Developer report of cache state for a definition id."\ + -help\ + "Returns a text report showing the raw definition, id info + (dynamic flag, defining namespace) and argdata cache entries + for the id - including a warning if the id is unexpectedly + present under multiple rawdef keys. + Intended for interactive/debug use. + Returns an empty string if the id isn't directly present + (aliases are not followed - see punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "definition id to report on" + }] proc id_query {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3689,6 +3973,24 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::real_id + @cmd -name punk::args::real_id\ + -summary\ + "Resolve an id or alias to the actual id under which a definition is stored."\ + -help\ + "Resolves aliases and returns the id under which the definition is + actually stored - which may be the supplied id itself, the alias + target, or an (autodef) entry created by the introspection + facilities. + If the id isn't found, punk::args::update_definitions is called for + the id's namespace qualifiers so that definitions in newly loaded + packages are discovered. + Returns an empty string if no definition can be found." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to resolve" + }] proc real_id {id} { variable id_cache_rawdef variable aliases @@ -3721,6 +4023,20 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::status + @cmd -name punk::args::status\ + -summary\ + "Report scan/load status of registered definition namespaces."\ + -help\ + "Returns a table of namespaces registered in + punk::args::register::NAMESPACES showing, for each, how many ids were + found when scanned, how many definitions have been loaded, and the + time taken (microseconds) for the scan/load passes. + Definitions are scanned and loaded lazily - a blank row indicates a + package whose definitions haven't been needed yet." + @values -min 0 -max 0 + }] proc status {} { upvar ::punk::args::register::NAMESPACES registered upvar ::punk::args::register::loaded_packages loaded_packages @@ -3986,29 +4302,29 @@ tcl::namespace::eval punk::args { # -------------------------------------- - #test of Get_caller + #developer diagnostics - test of Get_caller (unexported - leading underscores exclude from export pattern) lappend PUNKARGS [list { - @id -id ::punk::args::test1 + @id -id ::punk::args::__test1 @values -min 0 -max 0 }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args + proc __test_get_dict {args} { + punk::args::get_dict [punk::args::raw_def ::punk::args::__test1] $args } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args + proc __test_get_by_id {args} { + punk::args::get_by_id ::punk::args::__test1 $args } #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { + proc __test_callers {args} { if {![llength $args]} { puts "these test functions accept no arguments" puts "Call with arg(s) to compare error output" } - if {[catch {test_get_dict {*}$args} errM]} { + if {[catch {__test_get_dict {*}$args} errM]} { puts $errM } puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { + if {[catch {__test_get_by_id {*}$args} errM]} { puts $errM } return done @@ -4073,8 +4389,9 @@ tcl::namespace::eval punk::args { variable arg_error_CLR_error array set arg_error_CLR_error {} - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR + proc private::argerror_load_colours {{forcereload 0}} { + #colour state arrays live in the parent punk::args namespace (shared with arg_error) + upvar #0 ::punk::args::arg_error_CLR arg_error_CLR #todo - option for reload/retry? if {!$forcereload && [array size arg_error_CLR] > 0} { return @@ -4107,7 +4424,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR(ansibase_header) [a+ bold] set arg_error_CLR(ansibase_body) [a+ white] set arg_error_CLR(parsekey_hint) [a+ term-grey46] - variable arg_error_CLR_nocolour + upvar #0 ::punk::args::arg_error_CLR_nocolour arg_error_CLR_nocolour #array set arg_error_CLR_nocolour {} set arg_error_CLR_nocolour(errormsg) [a+ bold] set arg_error_CLR_nocolour(title) [a+ bold] @@ -4123,7 +4440,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_nocolour(ansibase_header) [a+ bold] set arg_error_CLR_nocolour(ansibase_body) "" set arg_error_CLR_nocolour(parsekey_hint) "" - variable arg_error_CLR_info + upvar #0 ::punk::args::arg_error_CLR_info arg_error_CLR_info #array set arg_error_CLR_info {} set arg_error_CLR_info(errormsg) [a+ brightred bold] set arg_error_CLR_info(title) [a+ brightyellow bold] @@ -4134,7 +4451,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_info(ansiborder) [a+ term-grey23 bold] set arg_error_CLR_info(ansibase_header) [a+ cyan] set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error + upvar #0 ::punk::args::arg_error_CLR_error arg_error_CLR_error #array set arg_error_CLR_error {} set arg_error_CLR_error(errormsg) [a+ brightred bold] set arg_error_CLR_error(title) [a+ brightcyan bold] @@ -4198,7 +4515,7 @@ tcl::namespace::eval punk::args { set forcereload 1 } } - _argerror_load_colours $forcereload + private::argerror_load_colours $forcereload if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -5757,58 +6074,11 @@ tcl::namespace::eval punk::args { } return $result } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - #return number of values we can assign to cater for variable length clauses such as: # {"elseif" expr "?then?" body} #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { + proc private::get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { set ARG_INFO [dict get $formdict ARG_INFO] set all_remaining [lrange $values $idx end] set thisname [lindex $names $nameidx] @@ -5850,11 +6120,11 @@ tcl::namespace::eval punk::args { #todo - support complex type members such as -type {{literal a|b} int OR} #for now - require llength 1 - simple type such as -type {literal(ab)|int} if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + error "private::get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" } #foreach tp_alternative [split $tp |] {} - set tp_alternatives [_split_type_expression $tp] + set tp_alternatives [private::split_type_expression $tp] foreach tp_alternative $tp_alternatives { switch -exact -- [lindex $tp_alternative 0] { literal { @@ -5926,7 +6196,7 @@ tcl::namespace::eval punk::args { } } else { - #todo - use _split_type_expression + #todo - use private::split_type_expression #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) @@ -5952,7 +6222,7 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - puts "_get_dict_can_assign_value: checking tp '$tp' against value '$rv'" + puts "private::get_dict_can_assign_value: checking tp '$tp' against value '$rv'" switch -glob -- $tp { "literal(*" { set litmatch [string range $tp 8 end-1] @@ -6094,7 +6364,7 @@ tcl::namespace::eval punk::args { set ctg_stringendswith [list] set ctg_other [list] #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { + foreach tp_alternative [private::split_type_expression $tp] { #JJJJ lassign $tp_alternative t textra switch -exact -- $t { @@ -6244,17 +6514,17 @@ tcl::namespace::eval punk::args { #so that they are not subject to type validation #such elements shouldn't be subject to validation if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - #puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed 0 resultlist {} typelist $thistype] } - #puts ">>>> _get_dict_can_assign_value $d" + #puts ">>>> private::get_dict_can_assign_value $d" return $d } - #_split_type_expression + #private::split_type_expression #only handles toplevel 'or' for type_expression e.g int|char #we have no mechanism for & - (although it would be useful) #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) @@ -6273,7 +6543,7 @@ tcl::namespace::eval punk::args { #mode -type {literalprefix text1 literalprefix text2 OR} #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - proc _split_type_expression {type_expression} { + proc private::split_type_expression {type_expression} { if {[llength $type_expression] == 1} { #simple expressions of length one must be splittable on | #disallowed: things such as literal(|) or literal(x|etc)|int @@ -6289,7 +6559,7 @@ tcl::namespace::eval punk::args { } return $type_alternatives } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + error "private::split_type_expression unimplemented: type_expression length > 1 '$type_expression'" #todo #RPN reverse polish notation #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} @@ -6309,14 +6579,14 @@ tcl::namespace::eval punk::args { set expect_separator 0 continue } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + error "private::split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" } } switch -exact -- $word { literal - literalprefix - stringstartswith - stringendswith - stringcontains { if {$w+1 > [llength $type_expression]} { #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + error "private::split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" } lappend type_alternatives [list $word [lindex $type_expression $w+1]] incr w ;#consume arg @@ -6334,12 +6604,12 @@ tcl::namespace::eval punk::args { } #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ###proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { ### #set type $type_expression ;#todo - 'split' on | ### set vlist $clausevalues_raw ### set vlist_check $clausevalues_check - ### set type_alternatives [_split_type_expression $type_expression] + ### set type_alternatives [private::split_type_expression $type_expression] ### #each type_alternative is a list of varying length depending on arguments supported by first word. ### #TODO? ### #single element types: int double string etc @@ -6736,7 +7006,7 @@ tcl::namespace::eval punk::args { #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname #common basic case: list of single item being a single value clause. #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { #default_type_expression is for the chosen clausecolumn #if {$argname eq "frametype"} { #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" @@ -6747,7 +7017,7 @@ tcl::namespace::eval punk::args { set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - set default_type_alternatives [_split_type_expression $default_type_expression] + set default_type_alternatives [private::split_type_expression $default_type_expression] #--------------------- #pre-calc prefix sets based on the default. set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] @@ -6779,7 +7049,7 @@ tcl::namespace::eval punk::args { set c_idx -1 foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set col_type_alternatives [private::split_type_expression $clause_column_type_expression] set firstany [lsearch -exact $col_type_alternatives any] if {$firstany > -1} { lset clause_results $c_idx $firstany 1 @@ -7461,7 +7731,7 @@ tcl::namespace::eval punk::args { # #} # } - # set type_alternatives [_split_type_expression $type_expression] + # set type_alternatives [private::split_type_expression $type_expression] # #each type_alternative is a list of varying length depending on arguments supported by first word. # #TODO? # #single element types: int double string etc @@ -8141,18 +8411,40 @@ tcl::namespace::eval punk::args { #G-040: the single implementation of choice-word matching - shared by argument parsing #(get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so 'i ' #resolution can never diverge from what parsing accepts. - #Arguments: - # word - the supplied word (callers pass the check-form, e.g. ansistripped when applicable) - # nocase - value of -nocase for the argument - # allchoices - flat list of -choices plus all -choicegroups members (dups allowed) - # choicealiases - -choicealiases dict (alias -> canonical choice) - # choiceprefix - value of -choiceprefix - # denylist - -choiceprefixdenylist (full word required for these names) - # reservelist - -choiceprefixreservelist (phantom prefix-calculation members) - #Returns dict: - # matched - boolean: word identifies a choice (directly, by unique prefix, or via an alias) - # exact - boolean: the raw stored word already equals the resulting choice (no rewrite needed) - # canonical - the resulting choice value (empty when not matched) + lappend PUNKARGS [list { + @id -id ::punk::args::choiceword_match + @cmd -name punk::args::choiceword_match\ + -summary\ + "Match a supplied word against an argument's choices (the shared choice-matching implementation)."\ + -help\ + "The single implementation of choice-word matching - shared by argument + parsing (get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so + 'i ' resolution can never diverge from what parsing accepts. + (G-040) + + Returns a dict with keys: + matched - boolean: word identifies a choice (directly, by unique + prefix, or via an alias) + exact - boolean: the raw stored word already equals the resulting + choice (no rewrite needed) + canonical - the resulting choice value (empty when not matched)" + @values -min 7 -max 7 + word -type string -help\ + "The supplied word. + Callers pass the check-form, e.g ansistripped when applicable" + nocase -type boolean -help\ + "value of -nocase for the argument" + allchoices -type list -help\ + "flat list of -choices plus all -choicegroups members (dups allowed)" + choicealiases -type dict -help\ + "-choicealiases dict (alias -> canonical choice)" + choiceprefix -type boolean -help\ + "value of -choiceprefix" + denylist -type list -help\ + "-choiceprefixdenylist (full word required for these names)" + reservelist -type list -help\ + "-choiceprefixreservelist (phantom prefix-calculation members)" + }] proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} { set aliasnames [tcl::dict::keys $choicealiases] set has_choicealiases [expr {[llength $aliasnames] > 0}] @@ -8291,6 +8583,30 @@ tcl::namespace::eval punk::args { #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. #only supports -flag val pairs, not solo options #If an option is supplied multiple times - only the last value is used. + lappend PUNKARGS [list { + @id -id ::punk::args::get_dict + @cmd -name punk::args::get_dict\ + -summary\ + "Parse an argument list against a raw definition - the core parsing engine."\ + -help\ + "The core argument parsing/validation engine underlying + punk::args::parse (which is the preferred API for procs). + Parses rawargs against the supplied raw definition list and returns + a dict with keys: leaders, opts, values (argument name/value dicts) + and received (names of arguments actually supplied, with ordinal + clause info where applicable), plus solos. + Raises an error (rendering a usage table according to the errorstyle + in effect) if the arguments don't conform to the definition." + @leaders -min 2 -max 2 + deflist -type list -help\ + "Raw definition list (list of definition blocks)" + rawargs -type list -help\ + "list of arguments to parse against the definition" + @opts + -form -default * -help\ + "Ordinal index or name of the command form to parse against. + The default * considers all forms." + }] proc get_dict {deflist rawargs args} { #see arg_error regarding considerations around unhappy-path performance @@ -8651,7 +8967,7 @@ tcl::namespace::eval punk::args { incr tentative_idx } } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set assign_d [private::get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9579,7 +9895,7 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during private::get_dict_can_assign_value set start_position $positionalidx @@ -9592,14 +9908,14 @@ tcl::namespace::eval punk::args { set leadertypelist [tcl::dict::get $argstate $leadername -type] ;#often a single type, but can be a list of types (possibly with some optional) for a type that is a clause accepting multiple values. set leader_clause_size [llength $leadertypelist] - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] + set assign_d [private::get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] if {$consumed == 0} { if {[tcl::dict::get $argstate $leadername -optional]} { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 1?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9607,12 +9923,12 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg } else { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9803,7 +10119,7 @@ tcl::namespace::eval punk::args { set valtypelist [tcl::dict::get $argstate $valname -type] set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] + set assign_d [private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] ;#count set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9818,7 +10134,7 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { @@ -10545,8 +10861,8 @@ tcl::namespace::eval punk::args { #set typespec [lindex $typelist $clausecolumn] #todo - handle type-alternates e.g -type char|double #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #private::check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + private::check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs #------------------------------------------------------------------------------------ @@ -10658,7 +10974,7 @@ tcl::namespace::eval punk::args { } - proc _synopsis_form_arg_display {formdict argname} { + proc private::synopsis_form_arg_display {formdict argname} { #non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings set I "\x1b\[3m" ;#[punk::ansi::a+ italic] set NI "\x1b\[23m" ;# [punk::ansi::a+ noitalic] @@ -10699,7 +11015,7 @@ tcl::namespace::eval punk::args { } else { #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set tp_alternative_word1 [lindex $tp_alternative 0] set match [lindex $tp_alternative 1] @@ -10901,7 +11217,7 @@ tcl::namespace::eval punk::args { set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set arginfo [dict get $forminfo ARG_INFO $argname] @@ -10945,7 +11261,7 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set match [lindex $tp_alternative 1] switch -- [lindex $tp_alternative 0] { @@ -11129,7 +11445,7 @@ tcl::namespace::eval punk::args { foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set ARGD [dict create argname $argname class value] @@ -11522,18 +11838,6 @@ tcl::namespace::eval punk::args { return $argdef } - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - #*** !doctools #[list_end] [comment {--- end definitions namespace punk::args ---}] } @@ -11545,7 +11849,7 @@ tcl::namespace::eval punk::args { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::args::lib { variable PUNKARGS - tcl::namespace::export * + tcl::namespace::export {[a-z]*} tcl::namespace::path [list [tcl::namespace::parent]] #*** !doctools #[subsection {Namespace punk::args::lib}] @@ -11553,6 +11857,22 @@ tcl::namespace::eval punk::args::lib { #[list_begin definitions] #tcl86 compat for string is dict - but without -strict or -failindex options + lappend PUNKARGS [list { + @id -id ::punk::args::lib::string_is_dict + @cmd -name punk::args::lib::string_is_dict\ + -summary\ + "Test whether a string is a valid dict ('string is dict' compatibility wrapper)."\ + -help\ + "Returns a boolean indicating whether the string can be interpreted + as a dictionary (an even-length list). + On Tcl 9+ this is a thin wrapper over 'string is dict'. + On earlier Tcl versions a compatible fallback is used which accepts + but ignores any leading options (-strict/-failindex are NOT implemented)." + @values -min 1 -max -1 + arg -type any -multiple 1 -help\ + "optional 'string is dict' options followed by the string to test. + Only the final argument (the string) is significant on pre-9 Tcl." + }] if {[catch {string is dict {}} errM]} { proc string_is_dict {args} { #compatibility for tcl pre 9.0 @@ -11581,10 +11901,37 @@ tcl::namespace::eval punk::args::lib { # return 1 #} + lappend PUNKARGS [list { + @id -id ::punk::args::lib::flatzip + @cmd -name punk::args::lib::flatzip\ + -summary\ + "Interleave two lists into a single flat list."\ + -help\ + "Returns a flat list alternating elements from l1 and l2. + e.g flatzip {a b c} {1 2 3} -> a 1 b 2 c 3" + @values -min 2 -max 2 + l1 -type list -help\ + "first list" + l2 -type list -help\ + "second list" + }] proc flatzip {l1 l2} { concat {*}[lmap a $l1 b $l2 {list $a $b}] } + lappend PUNKARGS [list { + @id -id ::punk::args::lib::zero_based_posns + @cmd -name punk::args::lib::zero_based_posns\ + -summary\ + "Return the list of zero-based positions 0..count-1."\ + -help\ + "Returns a list of integers from 0 to count-1, + or an empty result if count < 1. + Uses lseq where available (Tcl 8.7+) for efficiency." + @values -min 1 -max 1 + count -type integer -help\ + "number of positions" + }] if {[info commands lseq] ne ""} { #tcl 8.7+ lseq significantly faster, especially for larger ranges #The internal rep can be an 'arithseries' with no string representation @@ -11601,6 +11948,22 @@ tcl::namespace::eval punk::args::lib { } #return list of single column-width marks - possibly with ansi + lappend PUNKARGS [list { + @id -id ::punk::args::lib::choiceinfo_marks + @cmd -name punk::args::lib::choiceinfo_marks\ + -summary\ + "Return display marks for a choice based on its -choiceinfo entries."\ + -help\ + "Returns a list of single column-width marks (possibly containing ANSI) + indicating documentation types (punkargs/ensemble/oo/native etc.) + recorded for the choice in a -choiceinfo dictionary. + Used when rendering choice tables in usage output." + @values -min 2 -max 2 + choice -type string -help\ + "choice name to look up" + choiceinfodict -type dict -help\ + "-choiceinfo dictionary from an argument definition" + }] proc choiceinfo_marks {choice choiceinfodict} { set marks [list] if {[dict exists $choiceinfodict $choice]} { @@ -11866,7 +12229,7 @@ tcl::namespace::eval punk::args::lib { if {[string first \$\{ $templatestring] < 0} { set parts [list $templatestring] } else { - set parts [_parse_tstr_parts $templatestring] + set parts [private::parse_tstr_parts $templatestring] } set textchunks [list] #set expressions [list] @@ -11962,7 +12325,7 @@ tcl::namespace::eval punk::args::lib { return [list $textchunks $params] } args { - #see example in tstr_test_one + #see example in __tstr_test_one return [list $textchunks {*}$params] } string { @@ -12012,13 +12375,13 @@ tcl::namespace::eval punk::args::lib { } } } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { + #developer example/test - single placeholder tstr args where single placeholder must be an int (unexported) + proc __tstr_test_one {args} { set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. + @cmd -name ::punk::args::lib::__tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. example: set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] + __tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] } @values -min 2 -max 2 @@ -12033,7 +12396,7 @@ tcl::namespace::eval punk::args::lib { set result [string cat [lindex $template 0] $where [lindex $template 1]] return $result } - proc _parse_tstr_parts {templatestring} { + proc ::punk::args::private::parse_tstr_parts {templatestring} { if {$templatestring eq ""} { return [list] } @@ -12084,6 +12447,22 @@ tcl::namespace::eval punk::args::lib { } #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. + lappend PUNKARGS [list { + @id -id ::punk::args::lib::indent + @cmd -name punk::args::lib::indent\ + -summary\ + "Indent each non-blank line of a text block."\ + -help\ + "Returns the text with the prefix prepended to each non-blank line. + Whitespace-only lines become empty, and remaining lines are right-trimmed. + Like textutil::adjust::indent - but doesn't strip trailing lines, + and doesn't implement the skip parameter." + @values -min 1 -max 2 + text -type string -help\ + "text block to indent" + prefix -type string -default " " -optional 1 -help\ + "string prepended to each non-blank line" + }] proc indent {text {prefix " "}} { set result [list] foreach line [split $text \n] { @@ -12096,6 +12475,20 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #dedent? + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undent + @cmd -name punk::args::lib::undent\ + -summary\ + "Remove the common leading whitespace from all lines of a text block."\ + -help\ + "Determines the longest common leading whitespace across the non-blank + lines and removes it from every line. + Whitespace-only lines become empty. + Returns the text unchanged if there is no common leading whitespace." + @values -min 1 -max 1 + text -type string -help\ + "text block to undent (dedent)" + }] proc undent {text} { if {$text eq ""} { return "" @@ -12129,6 +12522,24 @@ tcl::namespace::eval punk::args::lib { } #hacky + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undentleader + @cmd -name punk::args::lib::undentleader\ + -summary\ + "Remove a specific leading string (or its common prefix) from all lines of a text block."\ + -help\ + "Like punk::args::lib::undent - but instead of calculating the common + leading whitespace, removes up to the supplied leader string from each + line (the actual removal is the common prefix of the leader and the + non-blank lines, so is never longer than the leader). + The leader is usually whitespace - but doesn't have to be. + Whitespace-only lines become empty." + @values -min 2 -max 2 + text -type string -help\ + "text block to undent" + leader -type string -help\ + "leading string to remove from each line" + }] proc undentleader {text leader} { #leader usually whitespace - but doesn't have to be if {$text eq ""} { @@ -12166,6 +12577,19 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #A version of textutil::string::longestCommonPrefixList + lappend PUNKARGS [list { + @id -id ::punk::args::lib::longestCommonPrefix + @cmd -name punk::args::lib::longestCommonPrefix\ + -summary\ + "Return the longest common prefix of a list of strings."\ + -help\ + "Returns the longest string that is a prefix of every item in the list. + An empty list returns an empty string; a single-item list returns the item. + A version of textutil::string::longestCommonPrefixList." + @values -min 1 -max 1 + items -type list -help\ + "list of strings" + }] proc longestCommonPrefix {items} { if {[llength $items] <= 1} { return [lindex $items 0] @@ -12195,6 +12619,19 @@ tcl::namespace::eval punk::args::lib { #order-preserving #(same as punk::lib) + lappend PUNKARGS [list { + @id -id ::punk::args::lib::lunique + @cmd -name punk::args::lib::lunique\ + -summary\ + "Return the unique elements of a list, preserving order."\ + -help\ + "Returns the list with duplicate elements removed, + keeping the first occurrence of each element in original order. + (same behaviour as punk::lib::lunique)" + @values -min 1 -max 1 + list -type list -help\ + "list to filter" + }] proc lunique {list} { set new {} foreach item $list { @@ -12211,6 +12648,7 @@ tcl::namespace::eval punk::args::lib { } tcl::namespace::eval punk::args::argdocbase { + variable PUNKARGS namespace export * #use a? to test and create literal ansi here rather than relying on punk::ansi package presence #e.g @@ -12218,9 +12656,53 @@ tcl::namespace::eval punk::args::argdocbase { #- bold │SGR 1│sample│␛[1msample #- ──────┼─────┼──────┼────────── #- RESULT│ │sample│␛[1msample + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::B + @cmd -name punk::args::argdocbase::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::N + @cmd -name punk::args::argdocbase::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends a bold span opened with B in tstr-processed -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::I + @cmd -name punk::args::argdocbase::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::NI + @cmd -name punk::args::argdocbase::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends an italic span opened with I in tstr-processed -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic } @@ -12390,7 +12872,7 @@ tcl::namespace::eval punk::args::package { # set PUNKARGS "" #} -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers +lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers ::punk::args::argdocbase # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools @@ -12551,7 +13033,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.3.2 + set version 0.4.0 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.3.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.4.0.tm similarity index 95% rename from src/vfs/_vfscommon.vfs/modules/punk/args-0.3.2.tm rename to src/vfs/_vfscommon.vfs/modules/punk/args-0.4.0.tm index ad66351a..06065b32 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.3.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args-0.4.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.3.2 +# Application punk::args 0.4.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.3.2] +#[manpage_begin punkshell_module_punk::args 0 0.4.0] #[copyright "2024"] #[titledesc {args parsing}] [comment {-- Name section and table of contents description --}] #[moddesc {args to nested dict of opts and values}] [comment {-- Description at end of page heading --}] @@ -269,13 +269,66 @@ tcl::namespace::eval punk::args::register { } tcl::namespace::eval ::punk::args {} +#lib & system namespaces must exist for the private namespace path below (populated further down) +tcl::namespace::eval ::punk::args::lib {} +tcl::namespace::eval ::punk::args::system {} + +tcl::namespace::eval ::punk::args::private { + #Internal implementation helpers - not part of the punk::args API and not exported. + #Naming: no underscore prefix - the namespace itself denotes internal status. + #The namespace path allows unqualified calls to siblings in punk::args and its lib/system namespaces + #(mirroring the path set on ::punk::args itself at package-provide time). + tcl::namespace::path [list ::punk::args ::punk::args::lib ::punk::args::system] +} tcl::namespace::eval ::punk::args::helpers { variable PUNKARGS namespace export * + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::B + @cmd -name punk::args::helpers::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::N + @cmd -name punk::args::helpers::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Ends a bold span opened with punk::args::helpers::B in tstr-processed + argdoc -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::I + @cmd -name punk::args::helpers::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Intended for use as a bracketed command substitution placeholder in + tstr-processed argdoc -help text, paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::helpers::NI + @cmd -name punk::args::helpers::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Ends an italic span opened with punk::args::helpers::I in + tstr-processed argdoc -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic #proc I {} {punk::ansi::a+ italic} #proc B {} {punk::ansi::a+ bold} @@ -1147,11 +1200,26 @@ tcl::namespace::eval punk::args { } + lappend PUNKARGS [list { + @id -id ::punk::args::errorstyle + @cmd -name punk::args::errorstyle\ + -summary\ + "Set or query the default errorstyle (unimplemented)."\ + -help\ + "Set or query the running configuration's default -errorstyle, + as used by punk::args::parse when no explicit -errorstyle + option is supplied in the parse call. + + NOT YET IMPLEMENTED - currently raises an error. + (review - is this an override or a default with respect to + an -errorstyle explicitly passed to punk::args::parse?)" + @values -min 0 -max 1 + errorstyle -type string -optional 1 -choices {debug enhanced standard basic minimal} -help\ + "New default errorstyle. If omitted, the current value is returned." + }] proc errorstyle {args} { #set or query the running config -errorstyle #review - is this an override or a default? - what happens with punk::args::parse specifically set value of -errorstyle? - #values: - #debug, enhanced, standard, basic, minimal error todo } proc define {args} { @@ -1175,6 +1243,24 @@ tcl::namespace::eval punk::args { return $id } } + lappend PUNKARGS [list { + @id -id ::punk::args::undefine + @cmd -name punk::args::undefine\ + -summary\ + "Remove a definition and its cached data by id."\ + -help\ + "Clears the raw definition and any resolved/cached data for the + supplied id from the punk::args caches. + The definition will no longer be retrievable by id unless redefined + (or rediscovered via update_definitions if it lives in a registered + namespace's PUNKARGS variable). + Emits a note to stderr unless quiet is true." + @values -min 1 -max 2 + id -type string -help\ + "id of the definition to remove (as shown by punk::args::get_ids)" + quiet -type boolean -default 0 -optional 1 -help\ + "Suppress stderr notices about clearing/missing ids" + }] proc undefine {id {quiet 0}} { #review - alias? variable rawdef_cache_about @@ -1209,6 +1295,22 @@ tcl::namespace::eval punk::args { } #'punk::args::parse $args withdef $deflist' can raise parsing error after an autoid was generated # In this case we don't see the autoid in order to delete it + lappend PUNKARGS [list { + @id -id ::punk::args::undefine_deflist + @cmd -name punk::args::undefine_deflist\ + -summary\ + "Remove a definition and its cached data by raw definition list."\ + -help\ + "Clears cached data for a definition keyed by the raw definition list + itself rather than by id. This is useful where an automatic id was + generated (e.g by 'punk::args::parse ... withdef ') and the + caller never saw the autoid in order to pass it to punk::args::undefine. + A no-op if the definition list isn't in the cache." + @values -min 1 -max 1 + deflist -type list -help\ + "Raw definition list (list of definition blocks) as originally supplied + to define/parse" + }] proc undefine_deflist {deflist} { variable rawdef_cache_about variable id_cache_rawdef @@ -1222,17 +1324,24 @@ tcl::namespace::eval punk::args { } - proc define2 {args} { - dict get [resolve {*}$args] id - } - lappend PUNKARGS [list { @id -id ::punk::args::resolve - @cmd -name punk::args::resolve -help\ - "" + @cmd -name punk::args::resolve\ + -summary\ + "Resolve raw definition blocks to the internal argument specification dictionary."\ + -help\ + "Takes one or more raw definition blocks (as would be supplied to + punk::args::define) and returns the internal specification dictionary + - the parsed/optimised structure used by the argument parser and the + usage/synopsis renderers. + Results are cached; dynamic definitions (@dynamic, or leading + '-dynamic 1' args) are re-substituted in the caller's definition + namespace as required. + Normally called indirectly (via define/parse/get_spec) - but can be + called directly for inspection of the resolved structure." @values -min 0 -max -1 arg -type any -multiple 1 -help\ - "rawdef line block" + "raw definition block" }] proc resolve {args} { variable rawdef_cache_about @@ -2552,7 +2661,7 @@ tcl::namespace::eval punk::args { set tspec $typespec set optional_clausemember false } - set type_alternatives [_split_type_expression $tspec] + set type_alternatives [private::split_type_expression $tspec] set normlist [list] foreach alt $type_alternatives { set firstword [lindex $alt 0] @@ -2602,7 +2711,7 @@ tcl::namespace::eval punk::args { packageversion {set normtype packageversion} packagerequirement {set normtype packagerequirement} literal { - #value was split out by _split_type_expression + #value was split out by private::split_type_expression set normtype literal([lindex $alt 1]) } literalprefix { @@ -3114,6 +3223,21 @@ tcl::namespace::eval punk::args { #return raw definition list as created with 'define' # - possibly with unresolved dynamic parts + lappend PUNKARGS [list { + @id -id ::punk::args::raw_def + @cmd -name punk::args::raw_def\ + -summary\ + "Return the raw (unresolved) definition list for an id."\ + -help\ + "Returns the raw definition list (list of definition blocks) as + originally supplied for the id. + The id is first resolved via punk::args::real_id, so aliases and + definitions in not-yet-scanned registered namespaces are found. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc raw_def {id} { variable id_cache_rawdef set realid [real_id $id] @@ -3481,6 +3605,22 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::resolved_def_values + @cmd -name punk::args::resolved_def_values\ + -summary\ + "Return textual definitions for the trailing values of a resolved definition."\ + -help\ + "Returns a text block of 'name ' lines for the + @values (trailing positional) arguments of the resolved definition, + optionally filtered by glob patterns on the value names. + See also punk::args::resolved_def for the full definition." + @values -min 1 -max 2 + id -type string -help\ + "id or id alias of the definition" + patternlist -type list -default * -optional 1 -help\ + "List of glob patterns to match against value argument names" + }] proc resolved_def_values {id {patternlist *}} { variable id_cache_rawdef set realid [real_id $id] @@ -3514,6 +3654,20 @@ tcl::namespace::eval punk::args { #proc resolved_def_leaders ?? #proc resolved_def_opts ?? + lappend PUNKARGS [list { + @id -id ::punk::args::get_spec + @cmd -name punk::args::get_spec\ + -summary\ + "Return the resolved internal specification dictionary for an id."\ + -help\ + "Retrieves the raw definition for the id (resolving aliases) and + returns the resolved internal specification dictionary as produced + by punk::args::resolve. + Returns an empty string if the id can't be resolved." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc get_spec {id} { set deflist [raw_def $id] if {$deflist eq ""} { @@ -3524,6 +3678,21 @@ tcl::namespace::eval punk::args { # return [resolve {*}[raw_def $id]] #} } + lappend PUNKARGS [list { + @id -id ::punk::args::is_dynamic + @cmd -name punk::args::is_dynamic\ + -summary\ + "Test whether the definition for an id is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the definition contains an + @dynamic directive (or legacy leading '-dynamic 1' arguments). + Dynamic definitions are re-substituted (dollar variables and + bracketed commands within the text) in the defining namespace on + resolution rather than being resolved once and cached in final form." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias of the definition" + }] proc is_dynamic {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3537,6 +3706,25 @@ tcl::namespace::eval punk::args { #@id must be within first 4 lines of first 3 blocks - or assign auto #review - @dynamic block where -id not explicitly set? - disallow? + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_id + @cmd -name punk::args::rawdef_id\ + -summary\ + "Determine the id for a raw definition list."\ + -help\ + "Extracts the id from the @id -id line of a raw definition list. + The @id line must appear within the first 4 lines of one of the first + 3 definition blocks. If no @id line is present (or its -id value is + 'auto') an automatic id of the form autoid_ is generated. + A malformed @id line (found but no retrievable -id value) raises an error. + The -id value undergoes tstr substitution in the caller's context at + the given level." + @values -min 1 -max 2 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + lvl -type integer -default 1 -optional 1 -help\ + "uplevel offset at which to perform tstr substitution of the @id line" + }] proc rawdef_id {rawdef {lvl 1}} { set id "" set found_id_line 0 @@ -3575,6 +3763,20 @@ tcl::namespace::eval punk::args { return $id } #test the rawdef for @dynamic directive + lappend PUNKARGS [list { + @id -id ::punk::args::rawdef_is_dynamic + @cmd -name punk::args::rawdef_is_dynamic\ + -summary\ + "Test whether a raw definition list is flagged as dynamic."\ + -help\ + "Returns a boolean indicating whether the raw definition list contains + a block beginning with the @dynamic directive (or uses the legacy + leading '-dynamic 1' argument form). + See also punk::args::is_dynamic which tests by id." + @values -min 1 -max 1 + rawdef -type list -help\ + "Raw definition list (list of definition blocks)" + }] proc rawdef_is_dynamic {rawdef} { #temporary - old way set flagged_dynamic [expr {[lindex $rawdef 0] eq "-dynamic" && [lindex $rawdef 1]}] @@ -3609,6 +3811,21 @@ tcl::namespace::eval punk::args { } #we don't automatically test for (autodef)$id - only direct ids and aliases + lappend PUNKARGS [list { + @id -id ::punk::args::id_exists + @cmd -name punk::args::id_exists\ + -summary\ + "Test whether an id or id alias is already known."\ + -help\ + "Returns a boolean indicating whether the supplied value is a known + definition id or id alias. + Only direct ids and aliases are tested - (autodef) entries are not + automatically checked, and no update_definitions scan is triggered + (compare punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to test" + }] proc id_exists {id} { variable aliases if {[tcl::dict::exists $aliases $id]} { @@ -3617,24 +3834,91 @@ tcl::namespace::eval punk::args { variable id_cache_rawdef tcl::dict::exists $id_cache_rawdef $id } + lappend PUNKARGS [list { + @id -id ::punk::args::idaliases + @cmd -name punk::args::idaliases\ + -summary\ + "Display the current id alias dictionary."\ + -help\ + "Returns a display (punk::lib::showdict) of the alias -> id mapping + for all id aliases created with punk::args::set_idalias." + @values -min 0 -max 0 + }] proc idaliases {} { variable aliases punk::lib::showdict $aliases } + lappend PUNKARGS [list { + @id -id ::punk::args::set_idalias + @cmd -name punk::args::set_idalias\ + -summary\ + "Create or update an alias for a definition id."\ + -help\ + "Creates (or overwrites) an alias that can be used in place of the + target id in functions such as punk::args::parse (withid), raw_def, + get_spec and usage. + The target id need not exist at the time the alias is created." + @values -min 2 -max 2 + alias -type string -help\ + "alias name" + id -type string -help\ + "target definition id" + }] proc set_idalias {alias id} { variable aliases dict set aliases $alias $id } + lappend PUNKARGS [list { + @id -id ::punk::args::unset_idalias + @cmd -name punk::args::unset_idalias\ + -summary\ + "Remove an id alias."\ + -help\ + "Removes an alias created with punk::args::set_idalias. + A no-op if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to remove" + }] proc unset_idalias {alias} { variable aliases dict unset aliases $alias } + lappend PUNKARGS [list { + @id -id ::punk::args::get_idalias + @cmd -name punk::args::get_idalias\ + -summary\ + "Return the target id for an alias."\ + -help\ + "Returns the id that the alias points to, + or an empty string if the alias doesn't exist." + @values -min 1 -max 1 + alias -type string -help\ + "alias name to look up" + }] proc get_idalias {alias} { variable aliases if {[dict exists $aliases $alias]} { return [tcl::dict::get $aliases $alias] } } + lappend PUNKARGS [list { + @id -id ::punk::args::id_query + @cmd -name punk::args::id_query\ + -summary\ + "Developer report of cache state for a definition id."\ + -help\ + "Returns a text report showing the raw definition, id info + (dynamic flag, defining namespace) and argdata cache entries + for the id - including a warning if the id is unexpectedly + present under multiple rawdef keys. + Intended for interactive/debug use. + Returns an empty string if the id isn't directly present + (aliases are not followed - see punk::args::real_id)." + @values -min 1 -max 1 + id -type string -help\ + "definition id to report on" + }] proc id_query {id} { variable id_cache_rawdef variable rawdef_cache_about @@ -3689,6 +3973,24 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::real_id + @cmd -name punk::args::real_id\ + -summary\ + "Resolve an id or alias to the actual id under which a definition is stored."\ + -help\ + "Resolves aliases and returns the id under which the definition is + actually stored - which may be the supplied id itself, the alias + target, or an (autodef) entry created by the introspection + facilities. + If the id isn't found, punk::args::update_definitions is called for + the id's namespace qualifiers so that definitions in newly loaded + packages are discovered. + Returns an empty string if no definition can be found." + @values -min 1 -max 1 + id -type string -help\ + "id or id alias to resolve" + }] proc real_id {id} { variable id_cache_rawdef variable aliases @@ -3721,6 +4023,20 @@ tcl::namespace::eval punk::args { } } + lappend PUNKARGS [list { + @id -id ::punk::args::status + @cmd -name punk::args::status\ + -summary\ + "Report scan/load status of registered definition namespaces."\ + -help\ + "Returns a table of namespaces registered in + punk::args::register::NAMESPACES showing, for each, how many ids were + found when scanned, how many definitions have been loaded, and the + time taken (microseconds) for the scan/load passes. + Definitions are scanned and loaded lazily - a blank row indicates a + package whose definitions haven't been needed yet." + @values -min 0 -max 0 + }] proc status {} { upvar ::punk::args::register::NAMESPACES registered upvar ::punk::args::register::loaded_packages loaded_packages @@ -3986,29 +4302,29 @@ tcl::namespace::eval punk::args { # -------------------------------------- - #test of Get_caller + #developer diagnostics - test of Get_caller (unexported - leading underscores exclude from export pattern) lappend PUNKARGS [list { - @id -id ::punk::args::test1 + @id -id ::punk::args::__test1 @values -min 0 -max 0 }] - proc test_get_dict {args} { - punk::args::get_dict [punk::args::raw_def ::punk::args::test1] $args + proc __test_get_dict {args} { + punk::args::get_dict [punk::args::raw_def ::punk::args::__test1] $args } - proc test_get_by_id {args} { - punk::args::get_by_id ::punk::args::test1 $args + proc __test_get_by_id {args} { + punk::args::get_by_id ::punk::args::__test1 $args } #supply an arg to cause usage error for test functions - check initial message to see if Get_caller is correct. - proc test_callers {args} { + proc __test_callers {args} { if {![llength $args]} { puts "these test functions accept no arguments" puts "Call with arg(s) to compare error output" } - if {[catch {test_get_dict {*}$args} errM]} { + if {[catch {__test_get_dict {*}$args} errM]} { puts $errM } puts "------------" - if {[catch {test_get_by_id {*}$args} errM]} { + if {[catch {__test_get_by_id {*}$args} errM]} { puts $errM } return done @@ -4073,8 +4389,9 @@ tcl::namespace::eval punk::args { variable arg_error_CLR_error array set arg_error_CLR_error {} - proc _argerror_load_colours {{forcereload 0}} { - variable arg_error_CLR + proc private::argerror_load_colours {{forcereload 0}} { + #colour state arrays live in the parent punk::args namespace (shared with arg_error) + upvar #0 ::punk::args::arg_error_CLR arg_error_CLR #todo - option for reload/retry? if {!$forcereload && [array size arg_error_CLR] > 0} { return @@ -4107,7 +4424,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR(ansibase_header) [a+ bold] set arg_error_CLR(ansibase_body) [a+ white] set arg_error_CLR(parsekey_hint) [a+ term-grey46] - variable arg_error_CLR_nocolour + upvar #0 ::punk::args::arg_error_CLR_nocolour arg_error_CLR_nocolour #array set arg_error_CLR_nocolour {} set arg_error_CLR_nocolour(errormsg) [a+ bold] set arg_error_CLR_nocolour(title) [a+ bold] @@ -4123,7 +4440,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_nocolour(ansibase_header) [a+ bold] set arg_error_CLR_nocolour(ansibase_body) "" set arg_error_CLR_nocolour(parsekey_hint) "" - variable arg_error_CLR_info + upvar #0 ::punk::args::arg_error_CLR_info arg_error_CLR_info #array set arg_error_CLR_info {} set arg_error_CLR_info(errormsg) [a+ brightred bold] set arg_error_CLR_info(title) [a+ brightyellow bold] @@ -4134,7 +4451,7 @@ tcl::namespace::eval punk::args { set arg_error_CLR_info(ansiborder) [a+ term-grey23 bold] set arg_error_CLR_info(ansibase_header) [a+ cyan] set arg_error_CLR_info(ansibase_body) [a+ white] - variable arg_error_CLR_error + upvar #0 ::punk::args::arg_error_CLR_error arg_error_CLR_error #array set arg_error_CLR_error {} set arg_error_CLR_error(errormsg) [a+ brightred bold] set arg_error_CLR_error(title) [a+ brightcyan bold] @@ -4198,7 +4515,7 @@ tcl::namespace::eval punk::args { set forcereload 1 } } - _argerror_load_colours $forcereload + private::argerror_load_colours $forcereload if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -5757,58 +6074,11 @@ tcl::namespace::eval punk::args { } return $result } - proc parseXXX {args} { - #no solo flags allowed for parse function itself. (ok for arglist being parsed) - set opts [dict create] ;#repeated flags will override earlier. That's ok here. - set arglist {} - set got_arglist 0 - set tailtype "" ;#withid|withdef - set id "" - for {set i 0} {$i < [llength $args]} {incr i} { - set a [lindex $args $i] - if {[string match -* $a]} { - dict set opts $a [lindex $args $i+1] - incr i - } else { - set arglist $a - set got_arglist 1 - set tailtype [lindex $args $i+1] - if {$tailtype eq "withid"} { - if {[llength $args] != $i+3} { - error "punk::args::parse - invalid call. Expected exactly one argument after 'withid'" - } - set id [lindex $args $i+2] - break - } elseif {$tailtype eq "withdef"} { - if {[llength $args] < $i+3} { - error "punk::args::parse - invalid call. Expected at least one argument after 'withdef'" - } - set deflist [lrange $args $i+2 end] - break - } else { - error "punk::args::parse - invalid call. Argument following arglist must be 'withid' or 'withdef'" - } - } - } - if {!$got_arglist} { - error "punk::args::parse - invalid call. Argument list not found: usage parse ?-flag val?... arglist withid|withdef ..." - } - #assert tailtype eq withid|withdef - if {$tailtype eq "withid"} { - #assert $id was provided - return "parse [llength $arglist] args withid $id, options:$opts" - } else { - #assert llength deflist >=1 - return "parse [llength $arglist] with [llength $deflist] definition blocks, options:$opts" - } - #TODO - } - #return number of values we can assign to cater for variable length clauses such as: # {"elseif" expr "?then?" body} #review - efficiency? each time we call this - we are looking ahead at the same info - proc _get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { + proc private::get_dict_can_assign_value {idx values nameidx names namesreceived formdict} { set ARG_INFO [dict get $formdict ARG_INFO] set all_remaining [lrange $values $idx end] set thisname [lindex $names $nameidx] @@ -5850,11 +6120,11 @@ tcl::namespace::eval punk::args { #todo - support complex type members such as -type {{literal a|b} int OR} #for now - require llength 1 - simple type such as -type {literal(ab)|int} if {[llength $tp] !=1} { - error "_get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" + error "private::get_dict_can_assign_value: complex -type not yet supported (tp:'$tp')" } #foreach tp_alternative [split $tp |] {} - set tp_alternatives [_split_type_expression $tp] + set tp_alternatives [private::split_type_expression $tp] foreach tp_alternative $tp_alternatives { switch -exact -- [lindex $tp_alternative 0] { literal { @@ -5926,7 +6196,7 @@ tcl::namespace::eval punk::args { } } else { - #todo - use _split_type_expression + #todo - use private::split_type_expression #review - we assume here that we don't have a set of clause-members where all are marked optional (?membertype?) @@ -5952,7 +6222,7 @@ tcl::namespace::eval punk::args { set clause_member_optional 0 } set tp [string trim $tp ?] - puts "_get_dict_can_assign_value: checking tp '$tp' against value '$rv'" + puts "private::get_dict_can_assign_value: checking tp '$tp' against value '$rv'" switch -glob -- $tp { "literal(*" { set litmatch [string range $tp 8 end-1] @@ -6094,7 +6364,7 @@ tcl::namespace::eval punk::args { set ctg_stringendswith [list] set ctg_other [list] #foreach tp_alternative [split $tp |] {} - foreach tp_alternative [_split_type_expression $tp] { + foreach tp_alternative [private::split_type_expression $tp] { #JJJJ lassign $tp_alternative t textra switch -exact -- $t { @@ -6244,17 +6514,17 @@ tcl::namespace::eval punk::args { #so that they are not subject to type validation #such elements shouldn't be subject to validation if {$alloc_ok} { - #puts stderr ">>>_get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed $alloc_count resultlist $resultlist typelist $newtypelist] } else { - #puts stderr ">>>_get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" + #puts stderr ">>>private::get_dict_can_assign_value NOT alloc_ok: idx:$idx v:[lindex $values $idx] consumed:$alloc_count thistype:$thistype" set d [dict create consumed 0 resultlist {} typelist $thistype] } - #puts ">>>> _get_dict_can_assign_value $d" + #puts ">>>> private::get_dict_can_assign_value $d" return $d } - #_split_type_expression + #private::split_type_expression #only handles toplevel 'or' for type_expression e.g int|char #we have no mechanism for & - (although it would be useful) #more complex type_expressions would require a bracketing syntax - (and probably pre-parsing) @@ -6273,7 +6543,7 @@ tcl::namespace::eval punk::args { #mode -type {literalprefix text1 literalprefix text2 OR} #mode -type {stringstartswith x stringstartswith y OR stringendswith z AND int OR} - proc _split_type_expression {type_expression} { + proc private::split_type_expression {type_expression} { if {[llength $type_expression] == 1} { #simple expressions of length one must be splittable on | #disallowed: things such as literal(|) or literal(x|etc)|int @@ -6289,7 +6559,7 @@ tcl::namespace::eval punk::args { } return $type_alternatives } else { - error "_split_type_expression unimplemented: type_expression length > 1 '$type_expression'" + error "private::split_type_expression unimplemented: type_expression length > 1 '$type_expression'" #todo #RPN reverse polish notation #e.g {stringstartswith x stringstartswith y OR stringendswith z AND int OR} @@ -6309,14 +6579,14 @@ tcl::namespace::eval punk::args { set expect_separator 0 continue } else { - error "_split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" + error "private::split_type_expression expected separator but received '$word' in type_expression:'$type_expression'" } } switch -exact -- $word { literal - literalprefix - stringstartswith - stringendswith - stringcontains { if {$w+1 > [llength $type_expression]} { #premature end - no arg available for type which requires one - error "_split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" + error "private::split_type_expression missing argument for type '$word' in type_expression:'$type_expression'" } lappend type_alternatives [list $word [lindex $type_expression $w+1]] incr w ;#consume arg @@ -6334,12 +6604,12 @@ tcl::namespace::eval punk::args { } #old version - ###proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { + ###proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn type_expression clausevalues_raw clausevalues_check argspecs} { ### #set type $type_expression ;#todo - 'split' on | ### set vlist $clausevalues_raw ### set vlist_check $clausevalues_check - ### set type_alternatives [_split_type_expression $type_expression] + ### set type_alternatives [private::split_type_expression $type_expression] ### #each type_alternative is a list of varying length depending on arguments supported by first word. ### #TODO? ### #single element types: int double string etc @@ -6736,7 +7006,7 @@ tcl::namespace::eval punk::args { #list_of_clauses_raw list of (possibly)multi-value clauses for a particular argname #common basic case: list of single item being a single value clause. #precondition: list_of_clauses_raw has 'list protected' clauses of length 1 e.g if value is a dict {a A} - proc _check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { + proc private::check_clausecolumn {argname argclass thisarg thisarg_checks clausecolumn default_type_expression list_of_clauses_raw list_of_clauses_check list_of_clauses_types argspecs} { #default_type_expression is for the chosen clausecolumn #if {$argname eq "frametype"} { #puts "--->checking arg:$argname clausecolumn:$clausecolumn checkvalues:[lsearch -all -inline -index $clausecolumn -subindices $list_of_clauses_check *] against default_type_expression $default_type_expression" @@ -6747,7 +7017,7 @@ tcl::namespace::eval punk::args { set clause_size [llength [dict get $thisarg -type]] ;#length of full type - not just the default_type_expression for the clausecolumn - set default_type_alternatives [_split_type_expression $default_type_expression] + set default_type_alternatives [private::split_type_expression $default_type_expression] #--------------------- #pre-calc prefix sets based on the default. set alt_literals [lsearch -all -inline -index 0 $default_type_alternatives literal] @@ -6779,7 +7049,7 @@ tcl::namespace::eval punk::args { set c_idx -1 foreach e $e_vals e_check $check_vals clause_column_type_expression $typelist_vals { incr c_idx - set col_type_alternatives [_split_type_expression $clause_column_type_expression] + set col_type_alternatives [private::split_type_expression $clause_column_type_expression] set firstany [lsearch -exact $col_type_alternatives any] if {$firstany > -1} { lset clause_results $c_idx $firstany 1 @@ -7461,7 +7731,7 @@ tcl::namespace::eval punk::args { # #} # } - # set type_alternatives [_split_type_expression $type_expression] + # set type_alternatives [private::split_type_expression $type_expression] # #each type_alternative is a list of varying length depending on arguments supported by first word. # #TODO? # #single element types: int double string etc @@ -8141,18 +8411,40 @@ tcl::namespace::eval punk::args { #G-040: the single implementation of choice-word matching - shared by argument parsing #(get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so 'i ' #resolution can never diverge from what parsing accepts. - #Arguments: - # word - the supplied word (callers pass the check-form, e.g. ansistripped when applicable) - # nocase - value of -nocase for the argument - # allchoices - flat list of -choices plus all -choicegroups members (dups allowed) - # choicealiases - -choicealiases dict (alias -> canonical choice) - # choiceprefix - value of -choiceprefix - # denylist - -choiceprefixdenylist (full word required for these names) - # reservelist - -choiceprefixreservelist (phantom prefix-calculation members) - #Returns dict: - # matched - boolean: word identifies a choice (directly, by unique prefix, or via an alias) - # exact - boolean: the raw stored word already equals the resulting choice (no rewrite needed) - # canonical - the resulting choice value (empty when not matched) + lappend PUNKARGS [list { + @id -id ::punk::args::choiceword_match + @cmd -name punk::args::choiceword_match\ + -summary\ + "Match a supplied word against an argument's choices (the shared choice-matching implementation)."\ + -help\ + "The single implementation of choice-word matching - shared by argument + parsing (get_dict) and the punk::ns doc-lookup walk (cmd_traverse), so + 'i ' resolution can never diverge from what parsing accepts. + (G-040) + + Returns a dict with keys: + matched - boolean: word identifies a choice (directly, by unique + prefix, or via an alias) + exact - boolean: the raw stored word already equals the resulting + choice (no rewrite needed) + canonical - the resulting choice value (empty when not matched)" + @values -min 7 -max 7 + word -type string -help\ + "The supplied word. + Callers pass the check-form, e.g ansistripped when applicable" + nocase -type boolean -help\ + "value of -nocase for the argument" + allchoices -type list -help\ + "flat list of -choices plus all -choicegroups members (dups allowed)" + choicealiases -type dict -help\ + "-choicealiases dict (alias -> canonical choice)" + choiceprefix -type boolean -help\ + "value of -choiceprefix" + denylist -type list -help\ + "-choiceprefixdenylist (full word required for these names)" + reservelist -type list -help\ + "-choiceprefixreservelist (phantom prefix-calculation members)" + }] proc choiceword_match {word nocase allchoices choicealiases choiceprefix denylist reservelist} { set aliasnames [tcl::dict::keys $choicealiases] set has_choicealiases [expr {[llength $aliasnames] > 0}] @@ -8291,6 +8583,30 @@ tcl::namespace::eval punk::args { #If no eopts (--) specified we stop looking for opts at the first nondash encountered in a position we'd expect a dash - so without eopt, values could contain dashes - but not in first position after flags. #only supports -flag val pairs, not solo options #If an option is supplied multiple times - only the last value is used. + lappend PUNKARGS [list { + @id -id ::punk::args::get_dict + @cmd -name punk::args::get_dict\ + -summary\ + "Parse an argument list against a raw definition - the core parsing engine."\ + -help\ + "The core argument parsing/validation engine underlying + punk::args::parse (which is the preferred API for procs). + Parses rawargs against the supplied raw definition list and returns + a dict with keys: leaders, opts, values (argument name/value dicts) + and received (names of arguments actually supplied, with ordinal + clause info where applicable), plus solos. + Raises an error (rendering a usage table according to the errorstyle + in effect) if the arguments don't conform to the definition." + @leaders -min 2 -max 2 + deflist -type list -help\ + "Raw definition list (list of definition blocks)" + rawargs -type list -help\ + "list of arguments to parse against the definition" + @opts + -form -default * -help\ + "Ordinal index or name of the command form to parse against. + The default * considers all forms." + }] proc get_dict {deflist rawargs args} { #see arg_error regarding considerations around unhappy-path performance @@ -8651,7 +8967,7 @@ tcl::namespace::eval punk::args { incr tentative_idx } } - set assign_d [_get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] + set assign_d [private::get_dict_can_assign_value 0 $tentative_pre_values 0 [list $leader_posn_name] $leadernames_seen $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9579,7 +9895,7 @@ tcl::namespace::eval punk::args { set leaders_dict [dict merge $leaders_dict $LEADER_DEFAULTS] #---------------------------------------- - set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during _get_dict_can_assign_value + set argument_clause_typestate [dict create] ;#Track *updated* -type info for argument clauses for those subelements that were fully validated during private::get_dict_can_assign_value set start_position $positionalidx @@ -9592,14 +9908,14 @@ tcl::namespace::eval punk::args { set leadertypelist [tcl::dict::get $argstate $leadername -type] ;#often a single type, but can be a list of types (possibly with some optional) for a type that is a clause accepting multiple values. set leader_clause_size [llength $leadertypelist] - set assign_d [_get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] + set assign_d [private::get_dict_can_assign_value $ldridx $leaders $nameidx $LEADER_NAMES $leadernames_received $formdict] set consumed [dict get $assign_d consumed] set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] if {$consumed == 0} { if {[tcl::dict::get $argstate $leadername -optional]} { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername leaders:$leaders (111)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 1?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 1?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9607,12 +9923,12 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$leadername ni $leadernames_received} { - #puts stderr "_get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $ldridx $values $nameidx $VAL_NAMES" set msg "Bad number of leaders for %caller%. Not enough remaining values to assign to required arguments (fail on $leadername)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -argspecs $argspecs]] $msg } else { puts stderr "get_dict cannot assign val:$ldr to leadername:$leadername (222)" - #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "_get_dict_can_assign_value consumed 0 unexpected 2?" + #return -options [list -code error -errorcode [list PUNKARGS UNCONSUMED -argspecs $argspecs]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" incr ldridx -1 set leadername_multiple "" incr nameidx @@ -9803,7 +10119,7 @@ tcl::namespace::eval punk::args { set valtypelist [tcl::dict::get $argstate $valname -type] set clause_size [llength $valtypelist] ;#common case is clause_size == 1 - set assign_d [_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] + set assign_d [private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES $valnames_received $formdict] set consumed [dict get $assign_d consumed] ;#count set resultlist [dict get $assign_d resultlist] set newtypelist [dict get $assign_d typelist] @@ -9818,7 +10134,7 @@ tcl::namespace::eval punk::args { } else { #required named arg if {$valname ni $valnames_received} { - #puts stderr "_get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" + #puts stderr "private::get_dict_can_assign_value $validx $values $nameidx $VAL_NAMES" set msg "Bad number of values for %caller%. Not enough remaining values to assign to required arguments (fail on $valname)." return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -argspecs $argspecs]] $msg } else { @@ -10545,8 +10861,8 @@ tcl::namespace::eval punk::args { #set typespec [lindex $typelist $clausecolumn] #todo - handle type-alternates e.g -type char|double #------------------------------------------------------------------------------------ - #_check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist - _check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs + #private::check_clausecolumn argname argclass thisarg thisarg_checks column default_type_expression list_of_clauses list_of_clauses_check list_of_clauses_typelist + private::check_clausecolumn $argname $argclass $thisarg $thisarg_checks $clausecolumn $type_expression $vlist $vlist_check $vlist_typelist $argspecs #------------------------------------------------------------------------------------ @@ -10658,7 +10974,7 @@ tcl::namespace::eval punk::args { } - proc _synopsis_form_arg_display {formdict argname} { + proc private::synopsis_form_arg_display {formdict argname} { #non-colour SGR such as bold/italic/strike - so we don't need to worry about NOCOLOR settings set I "\x1b\[3m" ;#[punk::ansi::a+ italic] set NI "\x1b\[23m" ;# [punk::ansi::a+ noitalic] @@ -10699,7 +11015,7 @@ tcl::namespace::eval punk::args { } else { #handle alternate-types e.g literal(text)|literal(binary) set alternates [list] - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set tp_alternative_word1 [lindex $tp_alternative 0] set match [lindex $tp_alternative 1] @@ -10901,7 +11217,7 @@ tcl::namespace::eval punk::args { set FORMARGS [list] foreach argname [dict get $forminfo LEADER_NAMES] { - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set arginfo [dict get $forminfo ARG_INFO $argname] @@ -10945,7 +11261,7 @@ tcl::namespace::eval punk::args { } else { set alternates [list];#alternate acceptable types e.g literal(yes)|literal(ok) or indexpression|literal(first) - set type_alternatives [_split_type_expression $tp] + set type_alternatives [private::split_type_expression $tp] foreach tp_alternative $type_alternatives { set match [lindex $tp_alternative 1] switch -- [lindex $tp_alternative 0] { @@ -11129,7 +11445,7 @@ tcl::namespace::eval punk::args { foreach argname [dict get $forminfo VAL_NAMES] { set arginfo [dict get $forminfo ARG_INFO $argname] - set display [_synopsis_form_arg_display $forminfo $argname] + set display [private::synopsis_form_arg_display $forminfo $argname] append syn " $display" set ARGD [dict create argname $argname class value] @@ -11522,18 +11838,6 @@ tcl::namespace::eval punk::args { return $argdef } - lappend PUNKARGS [list { - @id -id ::punk::args::TEST - @opts -optional 0 - -o1 -default 111 -help "opt 1 mandatory" - @opts -optional 1 - -o2 -default 222 -help "opt 2 optional" - @values -min 0 -max 1 - v -help\ - "v1 optional" - }] - - #*** !doctools #[list_end] [comment {--- end definitions namespace punk::args ---}] } @@ -11545,7 +11849,7 @@ tcl::namespace::eval punk::args { # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ tcl::namespace::eval punk::args::lib { variable PUNKARGS - tcl::namespace::export * + tcl::namespace::export {[a-z]*} tcl::namespace::path [list [tcl::namespace::parent]] #*** !doctools #[subsection {Namespace punk::args::lib}] @@ -11553,6 +11857,22 @@ tcl::namespace::eval punk::args::lib { #[list_begin definitions] #tcl86 compat for string is dict - but without -strict or -failindex options + lappend PUNKARGS [list { + @id -id ::punk::args::lib::string_is_dict + @cmd -name punk::args::lib::string_is_dict\ + -summary\ + "Test whether a string is a valid dict ('string is dict' compatibility wrapper)."\ + -help\ + "Returns a boolean indicating whether the string can be interpreted + as a dictionary (an even-length list). + On Tcl 9+ this is a thin wrapper over 'string is dict'. + On earlier Tcl versions a compatible fallback is used which accepts + but ignores any leading options (-strict/-failindex are NOT implemented)." + @values -min 1 -max -1 + arg -type any -multiple 1 -help\ + "optional 'string is dict' options followed by the string to test. + Only the final argument (the string) is significant on pre-9 Tcl." + }] if {[catch {string is dict {}} errM]} { proc string_is_dict {args} { #compatibility for tcl pre 9.0 @@ -11581,10 +11901,37 @@ tcl::namespace::eval punk::args::lib { # return 1 #} + lappend PUNKARGS [list { + @id -id ::punk::args::lib::flatzip + @cmd -name punk::args::lib::flatzip\ + -summary\ + "Interleave two lists into a single flat list."\ + -help\ + "Returns a flat list alternating elements from l1 and l2. + e.g flatzip {a b c} {1 2 3} -> a 1 b 2 c 3" + @values -min 2 -max 2 + l1 -type list -help\ + "first list" + l2 -type list -help\ + "second list" + }] proc flatzip {l1 l2} { concat {*}[lmap a $l1 b $l2 {list $a $b}] } + lappend PUNKARGS [list { + @id -id ::punk::args::lib::zero_based_posns + @cmd -name punk::args::lib::zero_based_posns\ + -summary\ + "Return the list of zero-based positions 0..count-1."\ + -help\ + "Returns a list of integers from 0 to count-1, + or an empty result if count < 1. + Uses lseq where available (Tcl 8.7+) for efficiency." + @values -min 1 -max 1 + count -type integer -help\ + "number of positions" + }] if {[info commands lseq] ne ""} { #tcl 8.7+ lseq significantly faster, especially for larger ranges #The internal rep can be an 'arithseries' with no string representation @@ -11601,6 +11948,22 @@ tcl::namespace::eval punk::args::lib { } #return list of single column-width marks - possibly with ansi + lappend PUNKARGS [list { + @id -id ::punk::args::lib::choiceinfo_marks + @cmd -name punk::args::lib::choiceinfo_marks\ + -summary\ + "Return display marks for a choice based on its -choiceinfo entries."\ + -help\ + "Returns a list of single column-width marks (possibly containing ANSI) + indicating documentation types (punkargs/ensemble/oo/native etc.) + recorded for the choice in a -choiceinfo dictionary. + Used when rendering choice tables in usage output." + @values -min 2 -max 2 + choice -type string -help\ + "choice name to look up" + choiceinfodict -type dict -help\ + "-choiceinfo dictionary from an argument definition" + }] proc choiceinfo_marks {choice choiceinfodict} { set marks [list] if {[dict exists $choiceinfodict $choice]} { @@ -11866,7 +12229,7 @@ tcl::namespace::eval punk::args::lib { if {[string first \$\{ $templatestring] < 0} { set parts [list $templatestring] } else { - set parts [_parse_tstr_parts $templatestring] + set parts [private::parse_tstr_parts $templatestring] } set textchunks [list] #set expressions [list] @@ -11962,7 +12325,7 @@ tcl::namespace::eval punk::args::lib { return [list $textchunks $params] } args { - #see example in tstr_test_one + #see example in __tstr_test_one return [list $textchunks {*}$params] } string { @@ -12012,13 +12375,13 @@ tcl::namespace::eval punk::args::lib { } } } - #test single placeholder tstr args where single placeholder must be an int - proc tstr_test_one {args} { + #developer example/test - single placeholder tstr args where single placeholder must be an int (unexported) + proc __tstr_test_one {args} { set argd [punk::args::parse $args withdef { - @cmd -name ::punk::args::lib::tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. + @cmd -name ::punk::args::lib::__tstr_test_one -help {An example/test of a function designed to be called with a js-style curly-braced Tstr. example: set id 2 - tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] + __tstr_test_one {*}[tstr -return args {Select * from table where id = \$\{$id\} and etc... ;}] } @values -min 2 -max 2 @@ -12033,7 +12396,7 @@ tcl::namespace::eval punk::args::lib { set result [string cat [lindex $template 0] $where [lindex $template 1]] return $result } - proc _parse_tstr_parts {templatestring} { + proc ::punk::args::private::parse_tstr_parts {templatestring} { if {$templatestring eq ""} { return [list] } @@ -12084,6 +12447,22 @@ tcl::namespace::eval punk::args::lib { } #like textutil::adjust::indent - but doesn't strip trailing lines, and doesn't implement skip parameter. + lappend PUNKARGS [list { + @id -id ::punk::args::lib::indent + @cmd -name punk::args::lib::indent\ + -summary\ + "Indent each non-blank line of a text block."\ + -help\ + "Returns the text with the prefix prepended to each non-blank line. + Whitespace-only lines become empty, and remaining lines are right-trimmed. + Like textutil::adjust::indent - but doesn't strip trailing lines, + and doesn't implement the skip parameter." + @values -min 1 -max 2 + text -type string -help\ + "text block to indent" + prefix -type string -default " " -optional 1 -help\ + "string prepended to each non-blank line" + }] proc indent {text {prefix " "}} { set result [list] foreach line [split $text \n] { @@ -12096,6 +12475,20 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #dedent? + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undent + @cmd -name punk::args::lib::undent\ + -summary\ + "Remove the common leading whitespace from all lines of a text block."\ + -help\ + "Determines the longest common leading whitespace across the non-blank + lines and removes it from every line. + Whitespace-only lines become empty. + Returns the text unchanged if there is no common leading whitespace." + @values -min 1 -max 1 + text -type string -help\ + "text block to undent (dedent)" + }] proc undent {text} { if {$text eq ""} { return "" @@ -12129,6 +12522,24 @@ tcl::namespace::eval punk::args::lib { } #hacky + lappend PUNKARGS [list { + @id -id ::punk::args::lib::undentleader + @cmd -name punk::args::lib::undentleader\ + -summary\ + "Remove a specific leading string (or its common prefix) from all lines of a text block."\ + -help\ + "Like punk::args::lib::undent - but instead of calculating the common + leading whitespace, removes up to the supplied leader string from each + line (the actual removal is the common prefix of the leader and the + non-blank lines, so is never longer than the leader). + The leader is usually whitespace - but doesn't have to be. + Whitespace-only lines become empty." + @values -min 2 -max 2 + text -type string -help\ + "text block to undent" + leader -type string -help\ + "leading string to remove from each line" + }] proc undentleader {text leader} { #leader usually whitespace - but doesn't have to be if {$text eq ""} { @@ -12166,6 +12577,19 @@ tcl::namespace::eval punk::args::lib { return [join $result \n] } #A version of textutil::string::longestCommonPrefixList + lappend PUNKARGS [list { + @id -id ::punk::args::lib::longestCommonPrefix + @cmd -name punk::args::lib::longestCommonPrefix\ + -summary\ + "Return the longest common prefix of a list of strings."\ + -help\ + "Returns the longest string that is a prefix of every item in the list. + An empty list returns an empty string; a single-item list returns the item. + A version of textutil::string::longestCommonPrefixList." + @values -min 1 -max 1 + items -type list -help\ + "list of strings" + }] proc longestCommonPrefix {items} { if {[llength $items] <= 1} { return [lindex $items 0] @@ -12195,6 +12619,19 @@ tcl::namespace::eval punk::args::lib { #order-preserving #(same as punk::lib) + lappend PUNKARGS [list { + @id -id ::punk::args::lib::lunique + @cmd -name punk::args::lib::lunique\ + -summary\ + "Return the unique elements of a list, preserving order."\ + -help\ + "Returns the list with duplicate elements removed, + keeping the first occurrence of each element in original order. + (same behaviour as punk::lib::lunique)" + @values -min 1 -max 1 + list -type list -help\ + "list to filter" + }] proc lunique {list} { set new {} foreach item $list { @@ -12211,6 +12648,7 @@ tcl::namespace::eval punk::args::lib { } tcl::namespace::eval punk::args::argdocbase { + variable PUNKARGS namespace export * #use a? to test and create literal ansi here rather than relying on punk::ansi package presence #e.g @@ -12218,9 +12656,53 @@ tcl::namespace::eval punk::args::argdocbase { #- bold │SGR 1│sample│␛[1msample #- ──────┼─────┼──────┼────────── #- RESULT│ │sample│␛[1msample + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::B + @cmd -name punk::args::argdocbase::B\ + -summary\ + "Return the literal ANSI SGR code for bold (a+ bold)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing N call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc B {} {return \x1b\[1m} ;#a+ bold + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::N + @cmd -name punk::args::argdocbase::N\ + -summary\ + "Return the literal ANSI SGR code for normal intensity (a+ normal)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends a bold span opened with B in tstr-processed -help text." + @values -min 0 -max 0 + }] proc N {} {return \x1b\[22m} ;#a+ normal + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::I + @cmd -name punk::args::argdocbase::I\ + -summary\ + "Return the literal ANSI SGR code for italic (a+ italic)."\ + -help\ + "Base building-block for import into argdoc namespaces, for use as a + bracketed command substitution placeholder in tstr-processed -help text. + Paired with a closing NI call. + A literal is returned so punk::ansi presence isn't required." + @values -min 0 -max 0 + }] proc I {} {return \x1b\[3m} ;#a+ italic + lappend PUNKARGS [list { + @id -id ::punk::args::argdocbase::NI + @cmd -name punk::args::argdocbase::NI\ + -summary\ + "Return the literal ANSI SGR code for noitalic (a+ noitalic)."\ + -help\ + "Base building-block for import into argdoc namespaces. + Ends an italic span opened with I in tstr-processed -help text." + @values -min 0 -max 0 + }] proc NI {} {return \x1b\[23m} ;#a+ noitalic } @@ -12390,7 +12872,7 @@ tcl::namespace::eval punk::args::package { # set PUNKARGS "" #} -lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers +lappend ::punk::args::register::NAMESPACES ::punk::args::argdoc ::punk::args ::punk::args::lib ::punk::args::package ::punk::args::helpers ::punk::args::argdocbase # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools @@ -12551,7 +13033,7 @@ package provide punk::args [tcl::namespace::eval punk::args { tcl::namespace::path {::punk::args::lib ::punk::args::system} variable pkg punk::args variable version - set version 0.3.2 + set version 0.4.0 }] return