From 296e1b876a7887df133dc58d0f0eadd3a98aa847 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Fri, 10 Jul 2026 18:48:35 +1000 Subject: [PATCH] update bootsupport + vfs + layout bootsupport: punk::args 0.5.0, punk::ns 0.2.0 punkcheck-managed regeneration propagating the G-049 module releases (parse-status data model, machine-parsable cmdhelp returns) into src/bootsupport/modules, the punk.project-0.1 and punk.shell-0.1 layout bootsupport copies, and _vfscommon.vfs/modules. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- .../punk/{args-0.4.2.tm => args-0.5.0.tm} | 468 +++++++++++++++--- .../modules/punk/ns-0.2.0.tm} | 141 +++--- .../bootsupport/modules/punk/args-0.5.0.tm} | 468 +++++++++++++++--- .../src/bootsupport/modules/punk/ns-0.2.0.tm} | 141 +++--- .../bootsupport/modules/punk/args-0.5.0.tm} | 468 +++++++++++++++--- .../src/bootsupport/modules/punk/ns-0.2.0.tm} | 141 +++--- .../punk/{args-0.4.2.tm => args-0.5.0.tm} | 468 +++++++++++++++--- .../_vfscommon.vfs/modules/punk/ns-0.2.0.tm} | 141 +++--- 8 files changed, 1924 insertions(+), 512 deletions(-) rename src/bootsupport/modules/punk/{args-0.4.2.tm => args-0.5.0.tm} (97%) rename src/{project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm => bootsupport/modules/punk/ns-0.2.0.tm} (98%) rename src/project_layouts/custom/_project/{punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.2.tm => punk.project-0.1/src/bootsupport/modules/punk/args-0.5.0.tm} (97%) rename src/{bootsupport/modules/punk/ns-0.1.4.tm => project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm} (98%) rename src/project_layouts/custom/_project/{punk.project-0.1/src/bootsupport/modules/punk/args-0.4.2.tm => punk.shell-0.1/src/bootsupport/modules/punk/args-0.5.0.tm} (97%) rename src/{vfs/_vfscommon.vfs/modules/punk/ns-0.1.4.tm => project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm} (98%) rename src/vfs/_vfscommon.vfs/modules/punk/{args-0.4.2.tm => args-0.5.0.tm} (97%) rename src/{project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm => vfs/_vfscommon.vfs/modules/punk/ns-0.2.0.tm} (98%) diff --git a/src/bootsupport/modules/punk/args-0.4.2.tm b/src/bootsupport/modules/punk/args-0.5.0.tm similarity index 97% rename from src/bootsupport/modules/punk/args-0.4.2.tm rename to src/bootsupport/modules/punk/args-0.5.0.tm index 23ed8406..811ef21e 100644 --- a/src/bootsupport/modules/punk/args-0.4.2.tm +++ b/src/bootsupport/modules/punk/args-0.5.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.4.2 +# Application punk::args 0.5.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.4.2] +#[manpage_begin punkshell_module_punk::args 0 0.5.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 --}] @@ -4624,8 +4624,17 @@ tcl::namespace::eval punk::args { -badarg -type string -help\ "name of an argument to highlight" -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" + "Result of successful punk::args::parse. + Together with -badarg this is converted internally to the + parse-status structure the renderers consume (received argument + rows get the goodarg style, choice words matching an argument's + value-in-effect are highlighted)." + -parsestatus -type dict -help\ + "Parse-status structure as returned by punk::args::parse_status. + The renderers derive goodarg/badarg row marking and choice + value-in-effect highlighting from it. Takes precedence over + -badarg/-parsedargs. Should be built for the same form the + display renders (see -form)." -aserror -type boolean -help\ "If true, the usage table is raised as an error message, otherwise it is returned as a value." @@ -4770,17 +4779,22 @@ tcl::namespace::eval punk::args { } #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR + variable arg_error_CLR set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { + if {[info exists arg_error_CLR(testsinglecolour)]} { set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}] if {$terminal_colour_is_on ^ $error_colour_is_on} { #results differ set forcereload 1 } } private::argerror_load_colours $forcereload + #per-render colour resolution: CLR is proc-local, seeded from the shared base array; + #a scheme's overrides are merged into this local copy only. (Merging into the shared + #arg_error_CLR leaked scheme overrides into every subsequent render until a colour + #on/off state flip forced an array reload.) + array set CLR [array get arg_error_CLR] if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -4791,7 +4805,7 @@ tcl::namespace::eval punk::args { set badarg "" set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] + set parsestatus "" ;#parse-status structure (see ::punk::args::parse_status) - takes precedence over -badarg/-parsedargs #----------------------- #todo!! make changeable from config file #JJJ 2025-07-16 @@ -4802,26 +4816,17 @@ tcl::namespace::eval punk::args { set scheme error set form 0 dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] + set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -parsestatus -aserror -return -scheme -form} $k] switch -- $fullk { -badarg { set badarg $v } -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } set parsedargs $v } + -parsestatus { + set parsestatus $v + } -aserror { if {![string is boolean -strict $v]} { set arg_error_isrunning 0 @@ -4844,13 +4849,15 @@ tcl::namespace::eval punk::args { } default { set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" + error "arg_error invalid option $k. Known_options: -badarg -parsedargs -parsestatus -aserror -scheme -return -form" } } } #todo - scheme - use config and iterm toml definitions etc switch -- $scheme { - "" - -nocolor - -nocolour { + "" - nocolor - nocolour - -nocolor - -nocolour { + #the documented choice value is 'nocolour' - dash spellings and 'nocolor' + #accepted for backward compatibility set scheme nocolour } info - error {} @@ -4883,7 +4890,7 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - #CLR has been defaulted to arg_error_CLR for its defaults + #CLR is a proc-local copy of arg_error_CLR (see above) #The nocolour,info,error arrays have overrides for some keys. switch -- $scheme { nocolour { @@ -4902,6 +4909,18 @@ tcl::namespace::eval punk::args { } } + #parse-status structure the renderers derive goodarg/badarg row marking and choice + #value-in-effect highlighting from (see ::punk::args::parse_status for the shape). + #An explicit -parsestatus takes precedence; otherwise build one for the displayed + #form from the -parsedargs/-badarg primitives. + if {$parsestatus ne ""} { + set PSTAT $parsestatus + } else { + set PSTAT [private::parse_status_build $spec_dict [lindex $selected_forms 0] -badarg $badarg -parsedargs $parsedargs] + } + set argstatusd [dict get $PSTAT argstatus] + set receivednames [dict get $PSTAT receivednames] + #set RST [a] set RST "\x1b\[0m" @@ -5192,17 +5211,9 @@ tcl::namespace::eval punk::args { #goodargs } } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs + #received-name normalization to optionset names (formerly done here on the + #transient goodargs local) lives in private::parse_status_build - the + #renderers below consume $argstatusd/$receivednames from the structure if {![catch {package require punk::trie}]} { #todo - reservelist for future options - or just to affect the prefix calculation # (similar to -choiceprefixreservelist) @@ -5304,18 +5315,25 @@ tcl::namespace::eval punk::args { set leading_val_names_hints {} set trailing_val_names_display $trailing_val_names set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues + foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names] [list opts $opt_names_display $opt_names_hints $opt_names] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names]] { + lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames set lastgroup "" set lastgroup_parsekey "" foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + #this argument's entry in the parse-status structure: row marking from + #its status, choice value-in-effect highlighting from hasvalue/value + if {[dict exists $argstatusd $arg]} { + set arg_status [dict get $argstatusd $arg status] + set arg_hasvalue [dict get $argstatusd $arg hasvalue] + set arg_value [dict get $argstatusd $arg value] + } else { + set arg_status unparsed + set arg_hasvalue 0 + set arg_value "" + } if {$argumentclass eq "opts"} { set thisgroup [dict get $arginfo -group] @@ -5354,9 +5372,9 @@ tcl::namespace::eval punk::args { } if {$use_table} { $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + } elseif {$arg_status eq "ok" || $thisgroup_parsekey in $receivednames} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5364,9 +5382,9 @@ tcl::namespace::eval punk::args { #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -5527,9 +5545,9 @@ tcl::namespace::eval punk::args { } append cdisplay \n $ctext } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5547,9 +5565,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5613,9 +5631,9 @@ tcl::namespace::eval punk::args { append cdisplay \n $ctext } #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5640,7 +5658,7 @@ tcl::namespace::eval punk::args { append cdisplay \n [dict get $choicelabeldict $c] } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5657,9 +5675,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5809,9 +5827,9 @@ tcl::namespace::eval punk::args { set col1 $argshow } $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5823,9 +5841,9 @@ tcl::namespace::eval punk::args { set arghelp "[a+ bold]$argshow$RST $linetail" } append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -6116,6 +6134,13 @@ tcl::namespace::eval punk::args { #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance #todo - configurable per interp/namespace -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} + -caller -type string -default "" -help\ + "Caller attribution for validation error messages. + When non-empty, this string replaces the %caller% placeholder in + validation failure messages instead of the automatic call-frame + walk - use it when parsing on behalf of another command (e.g a + usage/help display naming the queried command rather than the + internal parse call site)." -cache -type boolean -default 0 -help\ {Use sparingly. This caches the entire parse result or formatted validation error for the @@ -6200,6 +6225,7 @@ tcl::namespace::eval punk::args { -form {*} -errorstyle standard -cache 0 + -caller {} }] #todo - load override_errorstyle from configuration @@ -6208,7 +6234,7 @@ tcl::namespace::eval punk::args { set opts [dict merge $defaultopts $opts] dict for {k v} $opts { switch -- $k { - -form - -errorstyle - -cache { + -form - -errorstyle - -cache - -caller { } default { #punk::args::usage $args withid ::punk::args::parse ?? @@ -6246,7 +6272,7 @@ tcl::namespace::eval punk::args { set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] } else { variable parse_cache - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] if {[dict exists $parse_cache $key]} { set cached [dict get $parse_cache $key] if {[dict get $cached type] eq "result"} { @@ -6272,7 +6298,13 @@ tcl::namespace::eval punk::args { set ecode [dict get $erroropts -errorcode] #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] + if {[dict get $opts -caller] ne ""} { + #explicit caller attribution supplied (e.g punk::ns::cmdhelp usage display + #naming the queried command) - frame walking would name internal call sites. + set msg [string map [list %caller% [dict get $opts -caller]] $msg] + } else { + set msg [string map [list %caller% [Get_caller]] $msg] + } switch -- $matched_errorstyle { minimal { #return -options [list -code error -errorcode $ecode] $msg @@ -6337,7 +6369,7 @@ tcl::namespace::eval punk::args { } } - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] dict set parse_cache $key [dict create type "error" value $elist] return {*}$elist } trap {PUNKARGS} {msg erropts} { @@ -6352,6 +6384,312 @@ tcl::namespace::eval punk::args { return $result } + #classify a PUNKARGS VALIDATION failure class-word into the parse-status overall status + #payload = elements of the errorcode description after the class word + proc private::parse_status_classify {failureclass payload} { + switch -- $failureclass { + missingrequiredleader - missingrequiredvalue - leadermissing - optionmissing - valuemissing - missingoptionvalue { + return incomplete + } + leadingvaluecount - trailingvaluecount { + #payload: min max + set num [lindex $payload 0] + set min [Dict_getdef [lrange $payload 1 end] min ""] + if {[string is integer -strict $num] && [string is integer -strict $min] && $num < $min} { + return incomplete + } + return invalid + } + default { + return invalid + } + } + } + + #build the parse-status structure documented in the ::punk::args::parse_status definition. + #fid selects the form whose argument set the per-argument statuses cover (the form usage + #displays render). Options are unvalidated internal inputs - see parse_status/arg_error. + proc private::parse_status_build {spec_dict fid args} { + set defaults [dict create {*}{ + -ok "" + -status "" + -scheme "" + -message "" + -errorcode "" + -failureclass "" + -badarg "" + -parsedargs {} + }] + set opts [dict merge $defaults $args] + set parsedargs [dict get $opts -parsedargs] + set badarg [dict get $opts -badarg] + set form_dict [dict get $spec_dict FORMS $fid] + + #map each member of an -alias1|-alias2|-realname optionset to the optionset (the ARG_INFO key) + set lookup_optset [dict create] + foreach optionset [dict get $form_dict OPT_NAMES] { + foreach o [split $optionset |] { + dict set lookup_optset $o $optionset + } + } + #received argument names normalized to definition argument names. + #received is dict-like but may repeat keys for -multiple arguments; values are clause ordinals. + set receivednames [list] + set positions [dict create] ;#argname -> list of clause ordinals + foreach {r rpos} [Dict_getdef $parsedargs received {}] { + if {[string match -* $r] && [dict exists $lookup_optset $r]} { + set r [dict get $lookup_optset $r] + } + if {$r ni $receivednames} { + lappend receivednames $r + } + dict lappend positions $r $rpos + } + if {$badarg ne "" && [string match -* $badarg] && [dict exists $lookup_optset $badarg]} { + set badarg [dict get $lookup_optset $badarg] + } + set parsed_leaders [Dict_getdef $parsedargs leaders {}] + set parsed_opts [Dict_getdef $parsedargs opts {}] + set parsed_values [Dict_getdef $parsedargs values {}] + + set argstatus [dict create] + foreach {names class parsedvalues} [list {*}{ + } [dict get $form_dict LEADER_NAMES] leader {*}{ + } $parsed_leaders {*}{ + } [dict get $form_dict OPT_NAMES] option {*}{ + } $parsed_opts {*}{ + } [dict get $form_dict VAL_NAMES] value {*}{ + } $parsed_values {*}{ + }] { + foreach arg $names { + set argpositions [Dict_getdef $positions $arg {}] + set received [llength $argpositions] + if {$arg eq $badarg} { + set status bad + set argfailclass [dict get $opts -failureclass] + } elseif {$received > 0} { + set status ok + set argfailclass "" + } else { + set status unparsed + set argfailclass "" + } + #value-in-effect (includes values the parse filled from -default). + #Direct lookup by definition argument name, matching the display renderers: + #aliased optionsets store under their canonical name and are not resolved here. + if {[dict exists $parsedvalues $arg]} { + set hasvalue 1 + set value [dict get $parsedvalues $arg] + } else { + set hasvalue 0 + set value "" + } + dict set argstatus $arg [dict create class $class status $status received $received positions $argpositions hasvalue $hasvalue value $value failureclass $argfailclass] + } + } + return [dict create {*}{ + } ok [dict get $opts -ok] {*}{ + } status [dict get $opts -status] {*}{ + } scheme [dict get $opts -scheme] {*}{ + } message [dict get $opts -message] {*}{ + } errorcode [dict get $opts -errorcode] {*}{ + } failureclass [dict get $opts -failureclass] {*}{ + } badarg $badarg {*}{ + } id [Dict_getdef $spec_dict id ""] {*}{ + } form $fid {*}{ + } receivednames $receivednames {*}{ + } argstatus $argstatus {*}{ + }] + } + + #resolve the form name whose argument set parse-status per-argument entries cover + #(first form matching the -form selection - mirrors arg_error's display-form resolution) + proc private::parse_status_formname {spec_dict formselection} { + set formnames [dict get $spec_dict form_names] + set first [lindex $formselection 0] + if {$formselection eq "*" || $formselection eq ""} { + return [lindex $formnames 0] + } + if {[string is integer -strict $first]} { + set fid [lindex $formnames $first] + if {$fid ne ""} { + return $fid + } + return [lindex $formnames 0] + } + set fmatch [tcl::prefix::match -error "" $formnames $first] + if {$fmatch ne ""} { + return $fmatch + } + return [lindex $formnames 0] + } + + lappend PUNKARGS [list { + @id -id ::punk::args::parse_status + @cmd -name punk::args::parse_status\ + -summary\ + "Parse-status structure from a parse attempt (validation failures reported, not raised)."\ + -help\ + "Run a parse attempt of arglist against a punk::args definition and + return a parse-status dict describing the outcome - for success AND + for validation failure (a validation failure is reported in the + returned structure instead of being raised as an error; other errors + such as an unknown id still raise). + + This is the machine-parsable form of the information behind the + usage-display argument marking (punk::ns::cmdhelp / 'i '): + which supplied arguments validated, which argument failed and why, + and which display scheme applies. punk::args::arg_error accepts the + structure via its -parsestatus option and derives its goodarg/badarg + row marking and choice value-in-effect highlighting from it. + + Overall keys: + ok 1 parse succeeded, 0 validation failure + status valid | invalid | incomplete + invalid - a supplied word failed validation + incomplete - required arguments missing (a count or + allocation shortfall). Note a supplied word + failing its -type check can also surface as + an allocation shortfall (missingrequiredvalue) + - badarg and the per-argument statuses carry + the specifics in both situations. + scheme suggested display scheme: info (ok) | error (failure) + message empty | the validation failure message + errorcode empty | the -errorcode of the validation failure with + the bulky -argspecs payload removed + failureclass empty | first word of the errorcode description + (e.g choiceviolation, typemismatch, missingrequiredvalue) + badarg empty | name of the offending/unfillable argument + id the definition id + form form name the per-argument statuses were built for + (the first form matching -form - the form usage + displays render) + receivednames received argument names normalized to definition + argument names (opt aliases folded to their + -alias|-name optionset) + argstatus dict keyed by definition argument name, each value a + dict with keys: + class leader | option | value + status ok (received and validated) | + bad (the offending argument) | + unparsed (not received) + received count of received clauses (0 if unreceived) + positions ordinal positions of received clauses + hasvalue 1 if a value-in-effect is known + value value-in-effect - includes values the + parse filled from -default; empty when + hasvalue is 0. No values are reported + for a failed parse. + " + @form -form {withid withdef} + @leaders -min 1 -max 1 + arglist -type list -optional 0 -help\ + "Arguments to parse - supplied as a single list" + @opts -prefix 0 + -form -type list -default * -help\ + "Restrict parsing to the set of forms listed (see punk::args::parse). + The per-argument statuses are built for the first matching form." + -caller -type string -default "" -help\ + "Caller attribution for the validation failure message + (see punk::args::parse -caller). When empty, defaults to the + definition's @cmd -name (or its id) rather than the call-frame + walk parse would perform." + @values -min 2 + + @form -form withid -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withid $id" + @values -max 2 + withid -type literal(withid) -help\ + "The literal value 'withid'" + id -type string -help\ + "id of punk::args definition for a command" + + @form -form withdef -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withdef $def ?$def?" + withdef -type literal(withdef) -help\ + "The literal value 'withdef'" + def -type string -multiple 1 -optional 0 -help\ + "Each remaining argument is a block of text + defining argument definitions." + }] + proc parse_status {args} { + if {[llength $args] < 3} { + punk::args::parse $args withid ::punk::args::parse_status + } + set opts_and_vals $args + set arglist [lpop opts_and_vals 0] + set opts [dict create {*}{ + -form {*} + -caller {} + }] + while {[llength $opts_and_vals] && [string match -* [lindex $opts_and_vals 0]]} { + set k [lpop opts_and_vals 0] + switch -- $k { + -form - -caller { + if {![llength $opts_and_vals]} { + punk::args::parse $args withid ::punk::args::parse_status + } + dict set opts $k [lpop opts_and_vals 0] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + } + set tailtype [lpop opts_and_vals 0] + switch -- $tailtype { + withid { + if {[llength $opts_and_vals] != 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set id [lindex $opts_and_vals 0] + set spec_dict [get_spec $id] + if {$spec_dict eq ""} { + error "punk::args::parse_status - no such id: $id" + } + } + withdef { + if {[llength $opts_and_vals] < 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set spec_dict [resolve {*}$opts_and_vals] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + set fid [private::parse_status_formname $spec_dict [dict get $opts -form]] + set caller [dict get $opts -caller] + if {$caller eq ""} { + #the frame walk parse would do by default names parse_status's own internal + #parse call - the definition's command name is the meaningful attribution here + set caller [Dict_getdef $spec_dict cmd_info -name [Dict_getdef $spec_dict id ""]] + } + if {[catch { + punk::args::parse $arglist -form [dict get $opts -form] -errorstyle minimal -caller $caller $tailtype {*}$opts_and_vals + } r ropts]} { + set ecode [Dict_getdef $ropts -errorcode ""] + if {[lrange $ecode 0 1] ne [list PUNKARGS VALIDATION]} { + #not an argument validation failure (e.g definition error) - propagate + return -options $ropts $r + } + set classinfo [lindex $ecode 2] + set failureclass [lindex $classinfo 0] + set payload [lrange $classinfo 1 end] + set customdict [lrange $ecode 3 end] + set badarg [Dict_getdef $customdict -badarg ""] + set status [private::parse_status_classify $failureclass $payload] + #the -argspecs payload (the whole resolved spec) is display machinery - too bulky + #for a status structure; keep the classification and the small custom keys + set ecode_slim [lrange $ecode 0 2] + foreach {k v} $customdict { + if {$k ne "-argspecs"} { + lappend ecode_slim $k $v + } + } + return [private::parse_status_build $spec_dict $fid -ok 0 -status $status -scheme error -message $r -errorcode $ecode_slim -failureclass $failureclass -badarg $badarg] + } + return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r] + } + #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 @@ -10202,7 +10540,7 @@ tcl::namespace::eval punk::args { if {$leadername ni $leadernames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -badarg $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]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" @@ -10413,7 +10751,7 @@ tcl::namespace::eval punk::args { if {$valname ni $valnames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -badarg $valname -argspecs $argspecs]] $msg } else { #error 444 puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" @@ -13332,7 +13670,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.4.2 + set version 0.5.0 }] return diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm b/src/bootsupport/modules/punk/ns-0.2.0.tm similarity index 98% rename from src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm rename to src/bootsupport/modules/punk/ns-0.2.0.tm index 442d2746..74d4cdcc 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm +++ b/src/bootsupport/modules/punk/ns-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::ns 0.1.4 +# Application punk::ns 0.2.0 # Meta platform tcl # Meta license # @@ Meta End @@ -5503,7 +5503,20 @@ y" {return quirkykeyscript} usage information as this is inherently risky. (could create a file, exit the interp etc) " - -return -type string -default table -choices {string table tableobject} + -return -type string -default table -choices {string table tableobject dict} -choicelabels { + string "no table layout" + table "full table layout" + tableobject "table object cmd" + dict "machine-parsable resolution + parse-status" + } -help\ + "Return form of the usage information. + 'dict' returns a dict with keys origin, docid, cmdtype, + args_remaining and parsestatus - where parsestatus is the + parse-status structure of the supplied argument words against + the resolved definition (see punk::args::parse_status for the + documented structure; empty if the command is undocumented). + Its per-argument statuses distinguish a fully-valid, an invalid + and an incomplete argument set machine-parsably." } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { @@ -5564,17 +5577,6 @@ y" {return quirkykeyscript} set opt_grepstr [dict get $opts -grepstr] set opt_form [dict get $opts -form] set opt_return [dict get $opts -return] - switch -- $opt_return { - string { - set estyle "basic" - } - tableobject { - set estyle "minimal" - } - default { - set estyle "standard" - } - } set nextopts [dict remove $opts -grepstr] #JJJ @@ -5597,35 +5599,37 @@ y" {return quirkykeyscript} if {[llength $testresolved] == 1} { #only the command itself is in the args_resolved list - so we can't resolve to a deeper subcommand ledit queryargs -1 -1 {*}$args_remaining ;#prepend - #advisory goodargs parse (marking of supplied words). With NO supplied args - #a failure only reflects missing required leaders/values - nothing to mark and - #nothing wrong with the user's (absent) input, so we show plain usage in the - #info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the - #full usage table INSIDE the raised error only for it to be discarded here - - #doubling display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $queryargs]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $queryargs -form $opt_form -errorstyle $advisory_estyle withid $rootdoc} parseresult]} { - if {![llength $queryargs]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally + #-errorstyle minimal so no usage table is built inside a discarded error). + #-caller attributes any failure message to the queried command rather than + #an internal parse call site. + set pstatus [punk::args::parse_status $queryargs -form $opt_form -caller $querycommand withid $rootdoc] + #With NO supplied args a failure only reflects missing required + #leaders/values - nothing to mark and nothing wrong with the user's (absent) + #input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $queryargs]} { + dict set pstatus scheme info } - } else { + return [dict create origin $rootorigin docid $rootdoc cmdtype $rootorigintype args_remaining $queryargs parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $queryargs]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5695,36 +5699,44 @@ y" {return quirkykeyscript} } } if {$origindoc ne ""} { - #important not to use "-cache 1" for this parse - need to reflect dynamically updated ensembles etc - #advisory goodargs parse (marking of supplied words). With NO supplied trailing args - #a failure only reflects missing required leaders/values (e.g 'i string is') - nothing - #to mark and nothing wrong with the user's (absent) input, so we show plain usage in - #the info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the full - #usage table INSIDE the raised error only for it to be discarded here - doubling - #display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $args_remaining]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $args_remaining -cache 0 -form $opt_form -errorstyle $advisory_estyle withid $origindoc} parseresult]} { - if {![llength $args_remaining]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult - } + #caller attribution for any validation failure message: the queried command plus + #the subcommand words consumed by resolution (not the trailing argument words) + if {[llength $args_remaining]} { + set consumedwords [lrange $queryargs 0 end-[llength $args_remaining]] } else { + set consumedwords $queryargs + } + set caller_display [string trim "$querycommand [join $consumedwords { }]"] + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally uncached and + #-errorstyle minimal so no usage table is built inside a discarded error and + #dynamically updated ensembles are reflected). + set pstatus [punk::args::parse_status $args_remaining -form $opt_form -caller $caller_display withid $origindoc] + #With NO supplied trailing args a failure only reflects missing required + #leaders/values (e.g 'i string is') - nothing to mark and nothing wrong with the + #user's (absent) input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $args_remaining]} { + dict set pstatus scheme info + } + return [dict create origin $origin docid $origindoc cmdtype $origintype args_remaining $args_remaining parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $args_remaining]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5735,6 +5747,9 @@ y" {return quirkykeyscript} } return $result } else { + if {$opt_return eq "dict"} { + return [dict create origin $origin docid "" cmdtype $origintype args_remaining $args_remaining parsestatus {}] + } return "Undocumented command $origin. Type: $origintype" } @@ -7642,6 +7657,6 @@ namespace eval ::punk::args::register { ## Ready package provide punk::ns [tcl::namespace::eval punk::ns { variable version - set version 0.1.4 + set version 0.2.0 }] return \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.2.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.5.0.tm similarity index 97% rename from src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.2.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.5.0.tm index 23ed8406..811ef21e 100644 --- a/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.4.2.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.5.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.4.2 +# Application punk::args 0.5.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.4.2] +#[manpage_begin punkshell_module_punk::args 0 0.5.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 --}] @@ -4624,8 +4624,17 @@ tcl::namespace::eval punk::args { -badarg -type string -help\ "name of an argument to highlight" -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" + "Result of successful punk::args::parse. + Together with -badarg this is converted internally to the + parse-status structure the renderers consume (received argument + rows get the goodarg style, choice words matching an argument's + value-in-effect are highlighted)." + -parsestatus -type dict -help\ + "Parse-status structure as returned by punk::args::parse_status. + The renderers derive goodarg/badarg row marking and choice + value-in-effect highlighting from it. Takes precedence over + -badarg/-parsedargs. Should be built for the same form the + display renders (see -form)." -aserror -type boolean -help\ "If true, the usage table is raised as an error message, otherwise it is returned as a value." @@ -4770,17 +4779,22 @@ tcl::namespace::eval punk::args { } #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR + variable arg_error_CLR set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { + if {[info exists arg_error_CLR(testsinglecolour)]} { set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}] if {$terminal_colour_is_on ^ $error_colour_is_on} { #results differ set forcereload 1 } } private::argerror_load_colours $forcereload + #per-render colour resolution: CLR is proc-local, seeded from the shared base array; + #a scheme's overrides are merged into this local copy only. (Merging into the shared + #arg_error_CLR leaked scheme overrides into every subsequent render until a colour + #on/off state flip forced an array reload.) + array set CLR [array get arg_error_CLR] if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -4791,7 +4805,7 @@ tcl::namespace::eval punk::args { set badarg "" set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] + set parsestatus "" ;#parse-status structure (see ::punk::args::parse_status) - takes precedence over -badarg/-parsedargs #----------------------- #todo!! make changeable from config file #JJJ 2025-07-16 @@ -4802,26 +4816,17 @@ tcl::namespace::eval punk::args { set scheme error set form 0 dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] + set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -parsestatus -aserror -return -scheme -form} $k] switch -- $fullk { -badarg { set badarg $v } -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } set parsedargs $v } + -parsestatus { + set parsestatus $v + } -aserror { if {![string is boolean -strict $v]} { set arg_error_isrunning 0 @@ -4844,13 +4849,15 @@ tcl::namespace::eval punk::args { } default { set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" + error "arg_error invalid option $k. Known_options: -badarg -parsedargs -parsestatus -aserror -scheme -return -form" } } } #todo - scheme - use config and iterm toml definitions etc switch -- $scheme { - "" - -nocolor - -nocolour { + "" - nocolor - nocolour - -nocolor - -nocolour { + #the documented choice value is 'nocolour' - dash spellings and 'nocolor' + #accepted for backward compatibility set scheme nocolour } info - error {} @@ -4883,7 +4890,7 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - #CLR has been defaulted to arg_error_CLR for its defaults + #CLR is a proc-local copy of arg_error_CLR (see above) #The nocolour,info,error arrays have overrides for some keys. switch -- $scheme { nocolour { @@ -4902,6 +4909,18 @@ tcl::namespace::eval punk::args { } } + #parse-status structure the renderers derive goodarg/badarg row marking and choice + #value-in-effect highlighting from (see ::punk::args::parse_status for the shape). + #An explicit -parsestatus takes precedence; otherwise build one for the displayed + #form from the -parsedargs/-badarg primitives. + if {$parsestatus ne ""} { + set PSTAT $parsestatus + } else { + set PSTAT [private::parse_status_build $spec_dict [lindex $selected_forms 0] -badarg $badarg -parsedargs $parsedargs] + } + set argstatusd [dict get $PSTAT argstatus] + set receivednames [dict get $PSTAT receivednames] + #set RST [a] set RST "\x1b\[0m" @@ -5192,17 +5211,9 @@ tcl::namespace::eval punk::args { #goodargs } } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs + #received-name normalization to optionset names (formerly done here on the + #transient goodargs local) lives in private::parse_status_build - the + #renderers below consume $argstatusd/$receivednames from the structure if {![catch {package require punk::trie}]} { #todo - reservelist for future options - or just to affect the prefix calculation # (similar to -choiceprefixreservelist) @@ -5304,18 +5315,25 @@ tcl::namespace::eval punk::args { set leading_val_names_hints {} set trailing_val_names_display $trailing_val_names set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues + foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names] [list opts $opt_names_display $opt_names_hints $opt_names] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names]] { + lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames set lastgroup "" set lastgroup_parsekey "" foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + #this argument's entry in the parse-status structure: row marking from + #its status, choice value-in-effect highlighting from hasvalue/value + if {[dict exists $argstatusd $arg]} { + set arg_status [dict get $argstatusd $arg status] + set arg_hasvalue [dict get $argstatusd $arg hasvalue] + set arg_value [dict get $argstatusd $arg value] + } else { + set arg_status unparsed + set arg_hasvalue 0 + set arg_value "" + } if {$argumentclass eq "opts"} { set thisgroup [dict get $arginfo -group] @@ -5354,9 +5372,9 @@ tcl::namespace::eval punk::args { } if {$use_table} { $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + } elseif {$arg_status eq "ok" || $thisgroup_parsekey in $receivednames} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5364,9 +5382,9 @@ tcl::namespace::eval punk::args { #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -5527,9 +5545,9 @@ tcl::namespace::eval punk::args { } append cdisplay \n $ctext } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5547,9 +5565,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5613,9 +5631,9 @@ tcl::namespace::eval punk::args { append cdisplay \n $ctext } #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5640,7 +5658,7 @@ tcl::namespace::eval punk::args { append cdisplay \n [dict get $choicelabeldict $c] } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5657,9 +5675,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5809,9 +5827,9 @@ tcl::namespace::eval punk::args { set col1 $argshow } $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5823,9 +5841,9 @@ tcl::namespace::eval punk::args { set arghelp "[a+ bold]$argshow$RST $linetail" } append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -6116,6 +6134,13 @@ tcl::namespace::eval punk::args { #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance #todo - configurable per interp/namespace -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} + -caller -type string -default "" -help\ + "Caller attribution for validation error messages. + When non-empty, this string replaces the %caller% placeholder in + validation failure messages instead of the automatic call-frame + walk - use it when parsing on behalf of another command (e.g a + usage/help display naming the queried command rather than the + internal parse call site)." -cache -type boolean -default 0 -help\ {Use sparingly. This caches the entire parse result or formatted validation error for the @@ -6200,6 +6225,7 @@ tcl::namespace::eval punk::args { -form {*} -errorstyle standard -cache 0 + -caller {} }] #todo - load override_errorstyle from configuration @@ -6208,7 +6234,7 @@ tcl::namespace::eval punk::args { set opts [dict merge $defaultopts $opts] dict for {k v} $opts { switch -- $k { - -form - -errorstyle - -cache { + -form - -errorstyle - -cache - -caller { } default { #punk::args::usage $args withid ::punk::args::parse ?? @@ -6246,7 +6272,7 @@ tcl::namespace::eval punk::args { set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] } else { variable parse_cache - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] if {[dict exists $parse_cache $key]} { set cached [dict get $parse_cache $key] if {[dict get $cached type] eq "result"} { @@ -6272,7 +6298,13 @@ tcl::namespace::eval punk::args { set ecode [dict get $erroropts -errorcode] #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] + if {[dict get $opts -caller] ne ""} { + #explicit caller attribution supplied (e.g punk::ns::cmdhelp usage display + #naming the queried command) - frame walking would name internal call sites. + set msg [string map [list %caller% [dict get $opts -caller]] $msg] + } else { + set msg [string map [list %caller% [Get_caller]] $msg] + } switch -- $matched_errorstyle { minimal { #return -options [list -code error -errorcode $ecode] $msg @@ -6337,7 +6369,7 @@ tcl::namespace::eval punk::args { } } - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] dict set parse_cache $key [dict create type "error" value $elist] return {*}$elist } trap {PUNKARGS} {msg erropts} { @@ -6352,6 +6384,312 @@ tcl::namespace::eval punk::args { return $result } + #classify a PUNKARGS VALIDATION failure class-word into the parse-status overall status + #payload = elements of the errorcode description after the class word + proc private::parse_status_classify {failureclass payload} { + switch -- $failureclass { + missingrequiredleader - missingrequiredvalue - leadermissing - optionmissing - valuemissing - missingoptionvalue { + return incomplete + } + leadingvaluecount - trailingvaluecount { + #payload: min max + set num [lindex $payload 0] + set min [Dict_getdef [lrange $payload 1 end] min ""] + if {[string is integer -strict $num] && [string is integer -strict $min] && $num < $min} { + return incomplete + } + return invalid + } + default { + return invalid + } + } + } + + #build the parse-status structure documented in the ::punk::args::parse_status definition. + #fid selects the form whose argument set the per-argument statuses cover (the form usage + #displays render). Options are unvalidated internal inputs - see parse_status/arg_error. + proc private::parse_status_build {spec_dict fid args} { + set defaults [dict create {*}{ + -ok "" + -status "" + -scheme "" + -message "" + -errorcode "" + -failureclass "" + -badarg "" + -parsedargs {} + }] + set opts [dict merge $defaults $args] + set parsedargs [dict get $opts -parsedargs] + set badarg [dict get $opts -badarg] + set form_dict [dict get $spec_dict FORMS $fid] + + #map each member of an -alias1|-alias2|-realname optionset to the optionset (the ARG_INFO key) + set lookup_optset [dict create] + foreach optionset [dict get $form_dict OPT_NAMES] { + foreach o [split $optionset |] { + dict set lookup_optset $o $optionset + } + } + #received argument names normalized to definition argument names. + #received is dict-like but may repeat keys for -multiple arguments; values are clause ordinals. + set receivednames [list] + set positions [dict create] ;#argname -> list of clause ordinals + foreach {r rpos} [Dict_getdef $parsedargs received {}] { + if {[string match -* $r] && [dict exists $lookup_optset $r]} { + set r [dict get $lookup_optset $r] + } + if {$r ni $receivednames} { + lappend receivednames $r + } + dict lappend positions $r $rpos + } + if {$badarg ne "" && [string match -* $badarg] && [dict exists $lookup_optset $badarg]} { + set badarg [dict get $lookup_optset $badarg] + } + set parsed_leaders [Dict_getdef $parsedargs leaders {}] + set parsed_opts [Dict_getdef $parsedargs opts {}] + set parsed_values [Dict_getdef $parsedargs values {}] + + set argstatus [dict create] + foreach {names class parsedvalues} [list {*}{ + } [dict get $form_dict LEADER_NAMES] leader {*}{ + } $parsed_leaders {*}{ + } [dict get $form_dict OPT_NAMES] option {*}{ + } $parsed_opts {*}{ + } [dict get $form_dict VAL_NAMES] value {*}{ + } $parsed_values {*}{ + }] { + foreach arg $names { + set argpositions [Dict_getdef $positions $arg {}] + set received [llength $argpositions] + if {$arg eq $badarg} { + set status bad + set argfailclass [dict get $opts -failureclass] + } elseif {$received > 0} { + set status ok + set argfailclass "" + } else { + set status unparsed + set argfailclass "" + } + #value-in-effect (includes values the parse filled from -default). + #Direct lookup by definition argument name, matching the display renderers: + #aliased optionsets store under their canonical name and are not resolved here. + if {[dict exists $parsedvalues $arg]} { + set hasvalue 1 + set value [dict get $parsedvalues $arg] + } else { + set hasvalue 0 + set value "" + } + dict set argstatus $arg [dict create class $class status $status received $received positions $argpositions hasvalue $hasvalue value $value failureclass $argfailclass] + } + } + return [dict create {*}{ + } ok [dict get $opts -ok] {*}{ + } status [dict get $opts -status] {*}{ + } scheme [dict get $opts -scheme] {*}{ + } message [dict get $opts -message] {*}{ + } errorcode [dict get $opts -errorcode] {*}{ + } failureclass [dict get $opts -failureclass] {*}{ + } badarg $badarg {*}{ + } id [Dict_getdef $spec_dict id ""] {*}{ + } form $fid {*}{ + } receivednames $receivednames {*}{ + } argstatus $argstatus {*}{ + }] + } + + #resolve the form name whose argument set parse-status per-argument entries cover + #(first form matching the -form selection - mirrors arg_error's display-form resolution) + proc private::parse_status_formname {spec_dict formselection} { + set formnames [dict get $spec_dict form_names] + set first [lindex $formselection 0] + if {$formselection eq "*" || $formselection eq ""} { + return [lindex $formnames 0] + } + if {[string is integer -strict $first]} { + set fid [lindex $formnames $first] + if {$fid ne ""} { + return $fid + } + return [lindex $formnames 0] + } + set fmatch [tcl::prefix::match -error "" $formnames $first] + if {$fmatch ne ""} { + return $fmatch + } + return [lindex $formnames 0] + } + + lappend PUNKARGS [list { + @id -id ::punk::args::parse_status + @cmd -name punk::args::parse_status\ + -summary\ + "Parse-status structure from a parse attempt (validation failures reported, not raised)."\ + -help\ + "Run a parse attempt of arglist against a punk::args definition and + return a parse-status dict describing the outcome - for success AND + for validation failure (a validation failure is reported in the + returned structure instead of being raised as an error; other errors + such as an unknown id still raise). + + This is the machine-parsable form of the information behind the + usage-display argument marking (punk::ns::cmdhelp / 'i '): + which supplied arguments validated, which argument failed and why, + and which display scheme applies. punk::args::arg_error accepts the + structure via its -parsestatus option and derives its goodarg/badarg + row marking and choice value-in-effect highlighting from it. + + Overall keys: + ok 1 parse succeeded, 0 validation failure + status valid | invalid | incomplete + invalid - a supplied word failed validation + incomplete - required arguments missing (a count or + allocation shortfall). Note a supplied word + failing its -type check can also surface as + an allocation shortfall (missingrequiredvalue) + - badarg and the per-argument statuses carry + the specifics in both situations. + scheme suggested display scheme: info (ok) | error (failure) + message empty | the validation failure message + errorcode empty | the -errorcode of the validation failure with + the bulky -argspecs payload removed + failureclass empty | first word of the errorcode description + (e.g choiceviolation, typemismatch, missingrequiredvalue) + badarg empty | name of the offending/unfillable argument + id the definition id + form form name the per-argument statuses were built for + (the first form matching -form - the form usage + displays render) + receivednames received argument names normalized to definition + argument names (opt aliases folded to their + -alias|-name optionset) + argstatus dict keyed by definition argument name, each value a + dict with keys: + class leader | option | value + status ok (received and validated) | + bad (the offending argument) | + unparsed (not received) + received count of received clauses (0 if unreceived) + positions ordinal positions of received clauses + hasvalue 1 if a value-in-effect is known + value value-in-effect - includes values the + parse filled from -default; empty when + hasvalue is 0. No values are reported + for a failed parse. + " + @form -form {withid withdef} + @leaders -min 1 -max 1 + arglist -type list -optional 0 -help\ + "Arguments to parse - supplied as a single list" + @opts -prefix 0 + -form -type list -default * -help\ + "Restrict parsing to the set of forms listed (see punk::args::parse). + The per-argument statuses are built for the first matching form." + -caller -type string -default "" -help\ + "Caller attribution for the validation failure message + (see punk::args::parse -caller). When empty, defaults to the + definition's @cmd -name (or its id) rather than the call-frame + walk parse would perform." + @values -min 2 + + @form -form withid -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withid $id" + @values -max 2 + withid -type literal(withid) -help\ + "The literal value 'withid'" + id -type string -help\ + "id of punk::args definition for a command" + + @form -form withdef -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withdef $def ?$def?" + withdef -type literal(withdef) -help\ + "The literal value 'withdef'" + def -type string -multiple 1 -optional 0 -help\ + "Each remaining argument is a block of text + defining argument definitions." + }] + proc parse_status {args} { + if {[llength $args] < 3} { + punk::args::parse $args withid ::punk::args::parse_status + } + set opts_and_vals $args + set arglist [lpop opts_and_vals 0] + set opts [dict create {*}{ + -form {*} + -caller {} + }] + while {[llength $opts_and_vals] && [string match -* [lindex $opts_and_vals 0]]} { + set k [lpop opts_and_vals 0] + switch -- $k { + -form - -caller { + if {![llength $opts_and_vals]} { + punk::args::parse $args withid ::punk::args::parse_status + } + dict set opts $k [lpop opts_and_vals 0] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + } + set tailtype [lpop opts_and_vals 0] + switch -- $tailtype { + withid { + if {[llength $opts_and_vals] != 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set id [lindex $opts_and_vals 0] + set spec_dict [get_spec $id] + if {$spec_dict eq ""} { + error "punk::args::parse_status - no such id: $id" + } + } + withdef { + if {[llength $opts_and_vals] < 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set spec_dict [resolve {*}$opts_and_vals] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + set fid [private::parse_status_formname $spec_dict [dict get $opts -form]] + set caller [dict get $opts -caller] + if {$caller eq ""} { + #the frame walk parse would do by default names parse_status's own internal + #parse call - the definition's command name is the meaningful attribution here + set caller [Dict_getdef $spec_dict cmd_info -name [Dict_getdef $spec_dict id ""]] + } + if {[catch { + punk::args::parse $arglist -form [dict get $opts -form] -errorstyle minimal -caller $caller $tailtype {*}$opts_and_vals + } r ropts]} { + set ecode [Dict_getdef $ropts -errorcode ""] + if {[lrange $ecode 0 1] ne [list PUNKARGS VALIDATION]} { + #not an argument validation failure (e.g definition error) - propagate + return -options $ropts $r + } + set classinfo [lindex $ecode 2] + set failureclass [lindex $classinfo 0] + set payload [lrange $classinfo 1 end] + set customdict [lrange $ecode 3 end] + set badarg [Dict_getdef $customdict -badarg ""] + set status [private::parse_status_classify $failureclass $payload] + #the -argspecs payload (the whole resolved spec) is display machinery - too bulky + #for a status structure; keep the classification and the small custom keys + set ecode_slim [lrange $ecode 0 2] + foreach {k v} $customdict { + if {$k ne "-argspecs"} { + lappend ecode_slim $k $v + } + } + return [private::parse_status_build $spec_dict $fid -ok 0 -status $status -scheme error -message $r -errorcode $ecode_slim -failureclass $failureclass -badarg $badarg] + } + return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r] + } + #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 @@ -10202,7 +10540,7 @@ tcl::namespace::eval punk::args { if {$leadername ni $leadernames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -badarg $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]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" @@ -10413,7 +10751,7 @@ tcl::namespace::eval punk::args { if {$valname ni $valnames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -badarg $valname -argspecs $argspecs]] $msg } else { #error 444 puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" @@ -13332,7 +13670,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.4.2 + set version 0.5.0 }] return diff --git a/src/bootsupport/modules/punk/ns-0.1.4.tm b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm similarity index 98% rename from src/bootsupport/modules/punk/ns-0.1.4.tm rename to src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm index 442d2746..74d4cdcc 100644 --- a/src/bootsupport/modules/punk/ns-0.1.4.tm +++ b/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::ns 0.1.4 +# Application punk::ns 0.2.0 # Meta platform tcl # Meta license # @@ Meta End @@ -5503,7 +5503,20 @@ y" {return quirkykeyscript} usage information as this is inherently risky. (could create a file, exit the interp etc) " - -return -type string -default table -choices {string table tableobject} + -return -type string -default table -choices {string table tableobject dict} -choicelabels { + string "no table layout" + table "full table layout" + tableobject "table object cmd" + dict "machine-parsable resolution + parse-status" + } -help\ + "Return form of the usage information. + 'dict' returns a dict with keys origin, docid, cmdtype, + args_remaining and parsestatus - where parsestatus is the + parse-status structure of the supplied argument words against + the resolved definition (see punk::args::parse_status for the + documented structure; empty if the command is undocumented). + Its per-argument statuses distinguish a fully-valid, an invalid + and an incomplete argument set machine-parsably." } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { @@ -5564,17 +5577,6 @@ y" {return quirkykeyscript} set opt_grepstr [dict get $opts -grepstr] set opt_form [dict get $opts -form] set opt_return [dict get $opts -return] - switch -- $opt_return { - string { - set estyle "basic" - } - tableobject { - set estyle "minimal" - } - default { - set estyle "standard" - } - } set nextopts [dict remove $opts -grepstr] #JJJ @@ -5597,35 +5599,37 @@ y" {return quirkykeyscript} if {[llength $testresolved] == 1} { #only the command itself is in the args_resolved list - so we can't resolve to a deeper subcommand ledit queryargs -1 -1 {*}$args_remaining ;#prepend - #advisory goodargs parse (marking of supplied words). With NO supplied args - #a failure only reflects missing required leaders/values - nothing to mark and - #nothing wrong with the user's (absent) input, so we show plain usage in the - #info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the - #full usage table INSIDE the raised error only for it to be discarded here - - #doubling display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $queryargs]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $queryargs -form $opt_form -errorstyle $advisory_estyle withid $rootdoc} parseresult]} { - if {![llength $queryargs]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally + #-errorstyle minimal so no usage table is built inside a discarded error). + #-caller attributes any failure message to the queried command rather than + #an internal parse call site. + set pstatus [punk::args::parse_status $queryargs -form $opt_form -caller $querycommand withid $rootdoc] + #With NO supplied args a failure only reflects missing required + #leaders/values - nothing to mark and nothing wrong with the user's (absent) + #input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $queryargs]} { + dict set pstatus scheme info } - } else { + return [dict create origin $rootorigin docid $rootdoc cmdtype $rootorigintype args_remaining $queryargs parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $queryargs]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5695,36 +5699,44 @@ y" {return quirkykeyscript} } } if {$origindoc ne ""} { - #important not to use "-cache 1" for this parse - need to reflect dynamically updated ensembles etc - #advisory goodargs parse (marking of supplied words). With NO supplied trailing args - #a failure only reflects missing required leaders/values (e.g 'i string is') - nothing - #to mark and nothing wrong with the user's (absent) input, so we show plain usage in - #the info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the full - #usage table INSIDE the raised error only for it to be discarded here - doubling - #display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $args_remaining]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $args_remaining -cache 0 -form $opt_form -errorstyle $advisory_estyle withid $origindoc} parseresult]} { - if {![llength $args_remaining]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult - } + #caller attribution for any validation failure message: the queried command plus + #the subcommand words consumed by resolution (not the trailing argument words) + if {[llength $args_remaining]} { + set consumedwords [lrange $queryargs 0 end-[llength $args_remaining]] } else { + set consumedwords $queryargs + } + set caller_display [string trim "$querycommand [join $consumedwords { }]"] + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally uncached and + #-errorstyle minimal so no usage table is built inside a discarded error and + #dynamically updated ensembles are reflected). + set pstatus [punk::args::parse_status $args_remaining -form $opt_form -caller $caller_display withid $origindoc] + #With NO supplied trailing args a failure only reflects missing required + #leaders/values (e.g 'i string is') - nothing to mark and nothing wrong with the + #user's (absent) input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $args_remaining]} { + dict set pstatus scheme info + } + return [dict create origin $origin docid $origindoc cmdtype $origintype args_remaining $args_remaining parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $args_remaining]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5735,6 +5747,9 @@ y" {return quirkykeyscript} } return $result } else { + if {$opt_return eq "dict"} { + return [dict create origin $origin docid "" cmdtype $origintype args_remaining $args_remaining parsestatus {}] + } return "Undocumented command $origin. Type: $origintype" } @@ -7642,6 +7657,6 @@ namespace eval ::punk::args::register { ## Ready package provide punk::ns [tcl::namespace::eval punk::ns { variable version - set version 0.1.4 + set version 0.2.0 }] return \ No newline at end of file diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.2.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.5.0.tm similarity index 97% rename from src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.2.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.5.0.tm index 23ed8406..811ef21e 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/args-0.4.2.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/args-0.5.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.4.2 +# Application punk::args 0.5.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.4.2] +#[manpage_begin punkshell_module_punk::args 0 0.5.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 --}] @@ -4624,8 +4624,17 @@ tcl::namespace::eval punk::args { -badarg -type string -help\ "name of an argument to highlight" -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" + "Result of successful punk::args::parse. + Together with -badarg this is converted internally to the + parse-status structure the renderers consume (received argument + rows get the goodarg style, choice words matching an argument's + value-in-effect are highlighted)." + -parsestatus -type dict -help\ + "Parse-status structure as returned by punk::args::parse_status. + The renderers derive goodarg/badarg row marking and choice + value-in-effect highlighting from it. Takes precedence over + -badarg/-parsedargs. Should be built for the same form the + display renders (see -form)." -aserror -type boolean -help\ "If true, the usage table is raised as an error message, otherwise it is returned as a value." @@ -4770,17 +4779,22 @@ tcl::namespace::eval punk::args { } #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR + variable arg_error_CLR set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { + if {[info exists arg_error_CLR(testsinglecolour)]} { set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}] if {$terminal_colour_is_on ^ $error_colour_is_on} { #results differ set forcereload 1 } } private::argerror_load_colours $forcereload + #per-render colour resolution: CLR is proc-local, seeded from the shared base array; + #a scheme's overrides are merged into this local copy only. (Merging into the shared + #arg_error_CLR leaked scheme overrides into every subsequent render until a colour + #on/off state flip forced an array reload.) + array set CLR [array get arg_error_CLR] if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -4791,7 +4805,7 @@ tcl::namespace::eval punk::args { set badarg "" set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] + set parsestatus "" ;#parse-status structure (see ::punk::args::parse_status) - takes precedence over -badarg/-parsedargs #----------------------- #todo!! make changeable from config file #JJJ 2025-07-16 @@ -4802,26 +4816,17 @@ tcl::namespace::eval punk::args { set scheme error set form 0 dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] + set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -parsestatus -aserror -return -scheme -form} $k] switch -- $fullk { -badarg { set badarg $v } -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } set parsedargs $v } + -parsestatus { + set parsestatus $v + } -aserror { if {![string is boolean -strict $v]} { set arg_error_isrunning 0 @@ -4844,13 +4849,15 @@ tcl::namespace::eval punk::args { } default { set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" + error "arg_error invalid option $k. Known_options: -badarg -parsedargs -parsestatus -aserror -scheme -return -form" } } } #todo - scheme - use config and iterm toml definitions etc switch -- $scheme { - "" - -nocolor - -nocolour { + "" - nocolor - nocolour - -nocolor - -nocolour { + #the documented choice value is 'nocolour' - dash spellings and 'nocolor' + #accepted for backward compatibility set scheme nocolour } info - error {} @@ -4883,7 +4890,7 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - #CLR has been defaulted to arg_error_CLR for its defaults + #CLR is a proc-local copy of arg_error_CLR (see above) #The nocolour,info,error arrays have overrides for some keys. switch -- $scheme { nocolour { @@ -4902,6 +4909,18 @@ tcl::namespace::eval punk::args { } } + #parse-status structure the renderers derive goodarg/badarg row marking and choice + #value-in-effect highlighting from (see ::punk::args::parse_status for the shape). + #An explicit -parsestatus takes precedence; otherwise build one for the displayed + #form from the -parsedargs/-badarg primitives. + if {$parsestatus ne ""} { + set PSTAT $parsestatus + } else { + set PSTAT [private::parse_status_build $spec_dict [lindex $selected_forms 0] -badarg $badarg -parsedargs $parsedargs] + } + set argstatusd [dict get $PSTAT argstatus] + set receivednames [dict get $PSTAT receivednames] + #set RST [a] set RST "\x1b\[0m" @@ -5192,17 +5211,9 @@ tcl::namespace::eval punk::args { #goodargs } } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs + #received-name normalization to optionset names (formerly done here on the + #transient goodargs local) lives in private::parse_status_build - the + #renderers below consume $argstatusd/$receivednames from the structure if {![catch {package require punk::trie}]} { #todo - reservelist for future options - or just to affect the prefix calculation # (similar to -choiceprefixreservelist) @@ -5304,18 +5315,25 @@ tcl::namespace::eval punk::args { set leading_val_names_hints {} set trailing_val_names_display $trailing_val_names set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues + foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names] [list opts $opt_names_display $opt_names_hints $opt_names] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names]] { + lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames set lastgroup "" set lastgroup_parsekey "" foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + #this argument's entry in the parse-status structure: row marking from + #its status, choice value-in-effect highlighting from hasvalue/value + if {[dict exists $argstatusd $arg]} { + set arg_status [dict get $argstatusd $arg status] + set arg_hasvalue [dict get $argstatusd $arg hasvalue] + set arg_value [dict get $argstatusd $arg value] + } else { + set arg_status unparsed + set arg_hasvalue 0 + set arg_value "" + } if {$argumentclass eq "opts"} { set thisgroup [dict get $arginfo -group] @@ -5354,9 +5372,9 @@ tcl::namespace::eval punk::args { } if {$use_table} { $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + } elseif {$arg_status eq "ok" || $thisgroup_parsekey in $receivednames} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5364,9 +5382,9 @@ tcl::namespace::eval punk::args { #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -5527,9 +5545,9 @@ tcl::namespace::eval punk::args { } append cdisplay \n $ctext } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5547,9 +5565,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5613,9 +5631,9 @@ tcl::namespace::eval punk::args { append cdisplay \n $ctext } #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5640,7 +5658,7 @@ tcl::namespace::eval punk::args { append cdisplay \n [dict get $choicelabeldict $c] } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5657,9 +5675,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5809,9 +5827,9 @@ tcl::namespace::eval punk::args { set col1 $argshow } $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5823,9 +5841,9 @@ tcl::namespace::eval punk::args { set arghelp "[a+ bold]$argshow$RST $linetail" } append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -6116,6 +6134,13 @@ tcl::namespace::eval punk::args { #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance #todo - configurable per interp/namespace -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} + -caller -type string -default "" -help\ + "Caller attribution for validation error messages. + When non-empty, this string replaces the %caller% placeholder in + validation failure messages instead of the automatic call-frame + walk - use it when parsing on behalf of another command (e.g a + usage/help display naming the queried command rather than the + internal parse call site)." -cache -type boolean -default 0 -help\ {Use sparingly. This caches the entire parse result or formatted validation error for the @@ -6200,6 +6225,7 @@ tcl::namespace::eval punk::args { -form {*} -errorstyle standard -cache 0 + -caller {} }] #todo - load override_errorstyle from configuration @@ -6208,7 +6234,7 @@ tcl::namespace::eval punk::args { set opts [dict merge $defaultopts $opts] dict for {k v} $opts { switch -- $k { - -form - -errorstyle - -cache { + -form - -errorstyle - -cache - -caller { } default { #punk::args::usage $args withid ::punk::args::parse ?? @@ -6246,7 +6272,7 @@ tcl::namespace::eval punk::args { set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] } else { variable parse_cache - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] if {[dict exists $parse_cache $key]} { set cached [dict get $parse_cache $key] if {[dict get $cached type] eq "result"} { @@ -6272,7 +6298,13 @@ tcl::namespace::eval punk::args { set ecode [dict get $erroropts -errorcode] #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] + if {[dict get $opts -caller] ne ""} { + #explicit caller attribution supplied (e.g punk::ns::cmdhelp usage display + #naming the queried command) - frame walking would name internal call sites. + set msg [string map [list %caller% [dict get $opts -caller]] $msg] + } else { + set msg [string map [list %caller% [Get_caller]] $msg] + } switch -- $matched_errorstyle { minimal { #return -options [list -code error -errorcode $ecode] $msg @@ -6337,7 +6369,7 @@ tcl::namespace::eval punk::args { } } - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] dict set parse_cache $key [dict create type "error" value $elist] return {*}$elist } trap {PUNKARGS} {msg erropts} { @@ -6352,6 +6384,312 @@ tcl::namespace::eval punk::args { return $result } + #classify a PUNKARGS VALIDATION failure class-word into the parse-status overall status + #payload = elements of the errorcode description after the class word + proc private::parse_status_classify {failureclass payload} { + switch -- $failureclass { + missingrequiredleader - missingrequiredvalue - leadermissing - optionmissing - valuemissing - missingoptionvalue { + return incomplete + } + leadingvaluecount - trailingvaluecount { + #payload: min max + set num [lindex $payload 0] + set min [Dict_getdef [lrange $payload 1 end] min ""] + if {[string is integer -strict $num] && [string is integer -strict $min] && $num < $min} { + return incomplete + } + return invalid + } + default { + return invalid + } + } + } + + #build the parse-status structure documented in the ::punk::args::parse_status definition. + #fid selects the form whose argument set the per-argument statuses cover (the form usage + #displays render). Options are unvalidated internal inputs - see parse_status/arg_error. + proc private::parse_status_build {spec_dict fid args} { + set defaults [dict create {*}{ + -ok "" + -status "" + -scheme "" + -message "" + -errorcode "" + -failureclass "" + -badarg "" + -parsedargs {} + }] + set opts [dict merge $defaults $args] + set parsedargs [dict get $opts -parsedargs] + set badarg [dict get $opts -badarg] + set form_dict [dict get $spec_dict FORMS $fid] + + #map each member of an -alias1|-alias2|-realname optionset to the optionset (the ARG_INFO key) + set lookup_optset [dict create] + foreach optionset [dict get $form_dict OPT_NAMES] { + foreach o [split $optionset |] { + dict set lookup_optset $o $optionset + } + } + #received argument names normalized to definition argument names. + #received is dict-like but may repeat keys for -multiple arguments; values are clause ordinals. + set receivednames [list] + set positions [dict create] ;#argname -> list of clause ordinals + foreach {r rpos} [Dict_getdef $parsedargs received {}] { + if {[string match -* $r] && [dict exists $lookup_optset $r]} { + set r [dict get $lookup_optset $r] + } + if {$r ni $receivednames} { + lappend receivednames $r + } + dict lappend positions $r $rpos + } + if {$badarg ne "" && [string match -* $badarg] && [dict exists $lookup_optset $badarg]} { + set badarg [dict get $lookup_optset $badarg] + } + set parsed_leaders [Dict_getdef $parsedargs leaders {}] + set parsed_opts [Dict_getdef $parsedargs opts {}] + set parsed_values [Dict_getdef $parsedargs values {}] + + set argstatus [dict create] + foreach {names class parsedvalues} [list {*}{ + } [dict get $form_dict LEADER_NAMES] leader {*}{ + } $parsed_leaders {*}{ + } [dict get $form_dict OPT_NAMES] option {*}{ + } $parsed_opts {*}{ + } [dict get $form_dict VAL_NAMES] value {*}{ + } $parsed_values {*}{ + }] { + foreach arg $names { + set argpositions [Dict_getdef $positions $arg {}] + set received [llength $argpositions] + if {$arg eq $badarg} { + set status bad + set argfailclass [dict get $opts -failureclass] + } elseif {$received > 0} { + set status ok + set argfailclass "" + } else { + set status unparsed + set argfailclass "" + } + #value-in-effect (includes values the parse filled from -default). + #Direct lookup by definition argument name, matching the display renderers: + #aliased optionsets store under their canonical name and are not resolved here. + if {[dict exists $parsedvalues $arg]} { + set hasvalue 1 + set value [dict get $parsedvalues $arg] + } else { + set hasvalue 0 + set value "" + } + dict set argstatus $arg [dict create class $class status $status received $received positions $argpositions hasvalue $hasvalue value $value failureclass $argfailclass] + } + } + return [dict create {*}{ + } ok [dict get $opts -ok] {*}{ + } status [dict get $opts -status] {*}{ + } scheme [dict get $opts -scheme] {*}{ + } message [dict get $opts -message] {*}{ + } errorcode [dict get $opts -errorcode] {*}{ + } failureclass [dict get $opts -failureclass] {*}{ + } badarg $badarg {*}{ + } id [Dict_getdef $spec_dict id ""] {*}{ + } form $fid {*}{ + } receivednames $receivednames {*}{ + } argstatus $argstatus {*}{ + }] + } + + #resolve the form name whose argument set parse-status per-argument entries cover + #(first form matching the -form selection - mirrors arg_error's display-form resolution) + proc private::parse_status_formname {spec_dict formselection} { + set formnames [dict get $spec_dict form_names] + set first [lindex $formselection 0] + if {$formselection eq "*" || $formselection eq ""} { + return [lindex $formnames 0] + } + if {[string is integer -strict $first]} { + set fid [lindex $formnames $first] + if {$fid ne ""} { + return $fid + } + return [lindex $formnames 0] + } + set fmatch [tcl::prefix::match -error "" $formnames $first] + if {$fmatch ne ""} { + return $fmatch + } + return [lindex $formnames 0] + } + + lappend PUNKARGS [list { + @id -id ::punk::args::parse_status + @cmd -name punk::args::parse_status\ + -summary\ + "Parse-status structure from a parse attempt (validation failures reported, not raised)."\ + -help\ + "Run a parse attempt of arglist against a punk::args definition and + return a parse-status dict describing the outcome - for success AND + for validation failure (a validation failure is reported in the + returned structure instead of being raised as an error; other errors + such as an unknown id still raise). + + This is the machine-parsable form of the information behind the + usage-display argument marking (punk::ns::cmdhelp / 'i '): + which supplied arguments validated, which argument failed and why, + and which display scheme applies. punk::args::arg_error accepts the + structure via its -parsestatus option and derives its goodarg/badarg + row marking and choice value-in-effect highlighting from it. + + Overall keys: + ok 1 parse succeeded, 0 validation failure + status valid | invalid | incomplete + invalid - a supplied word failed validation + incomplete - required arguments missing (a count or + allocation shortfall). Note a supplied word + failing its -type check can also surface as + an allocation shortfall (missingrequiredvalue) + - badarg and the per-argument statuses carry + the specifics in both situations. + scheme suggested display scheme: info (ok) | error (failure) + message empty | the validation failure message + errorcode empty | the -errorcode of the validation failure with + the bulky -argspecs payload removed + failureclass empty | first word of the errorcode description + (e.g choiceviolation, typemismatch, missingrequiredvalue) + badarg empty | name of the offending/unfillable argument + id the definition id + form form name the per-argument statuses were built for + (the first form matching -form - the form usage + displays render) + receivednames received argument names normalized to definition + argument names (opt aliases folded to their + -alias|-name optionset) + argstatus dict keyed by definition argument name, each value a + dict with keys: + class leader | option | value + status ok (received and validated) | + bad (the offending argument) | + unparsed (not received) + received count of received clauses (0 if unreceived) + positions ordinal positions of received clauses + hasvalue 1 if a value-in-effect is known + value value-in-effect - includes values the + parse filled from -default; empty when + hasvalue is 0. No values are reported + for a failed parse. + " + @form -form {withid withdef} + @leaders -min 1 -max 1 + arglist -type list -optional 0 -help\ + "Arguments to parse - supplied as a single list" + @opts -prefix 0 + -form -type list -default * -help\ + "Restrict parsing to the set of forms listed (see punk::args::parse). + The per-argument statuses are built for the first matching form." + -caller -type string -default "" -help\ + "Caller attribution for the validation failure message + (see punk::args::parse -caller). When empty, defaults to the + definition's @cmd -name (or its id) rather than the call-frame + walk parse would perform." + @values -min 2 + + @form -form withid -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withid $id" + @values -max 2 + withid -type literal(withid) -help\ + "The literal value 'withid'" + id -type string -help\ + "id of punk::args definition for a command" + + @form -form withdef -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withdef $def ?$def?" + withdef -type literal(withdef) -help\ + "The literal value 'withdef'" + def -type string -multiple 1 -optional 0 -help\ + "Each remaining argument is a block of text + defining argument definitions." + }] + proc parse_status {args} { + if {[llength $args] < 3} { + punk::args::parse $args withid ::punk::args::parse_status + } + set opts_and_vals $args + set arglist [lpop opts_and_vals 0] + set opts [dict create {*}{ + -form {*} + -caller {} + }] + while {[llength $opts_and_vals] && [string match -* [lindex $opts_and_vals 0]]} { + set k [lpop opts_and_vals 0] + switch -- $k { + -form - -caller { + if {![llength $opts_and_vals]} { + punk::args::parse $args withid ::punk::args::parse_status + } + dict set opts $k [lpop opts_and_vals 0] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + } + set tailtype [lpop opts_and_vals 0] + switch -- $tailtype { + withid { + if {[llength $opts_and_vals] != 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set id [lindex $opts_and_vals 0] + set spec_dict [get_spec $id] + if {$spec_dict eq ""} { + error "punk::args::parse_status - no such id: $id" + } + } + withdef { + if {[llength $opts_and_vals] < 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set spec_dict [resolve {*}$opts_and_vals] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + set fid [private::parse_status_formname $spec_dict [dict get $opts -form]] + set caller [dict get $opts -caller] + if {$caller eq ""} { + #the frame walk parse would do by default names parse_status's own internal + #parse call - the definition's command name is the meaningful attribution here + set caller [Dict_getdef $spec_dict cmd_info -name [Dict_getdef $spec_dict id ""]] + } + if {[catch { + punk::args::parse $arglist -form [dict get $opts -form] -errorstyle minimal -caller $caller $tailtype {*}$opts_and_vals + } r ropts]} { + set ecode [Dict_getdef $ropts -errorcode ""] + if {[lrange $ecode 0 1] ne [list PUNKARGS VALIDATION]} { + #not an argument validation failure (e.g definition error) - propagate + return -options $ropts $r + } + set classinfo [lindex $ecode 2] + set failureclass [lindex $classinfo 0] + set payload [lrange $classinfo 1 end] + set customdict [lrange $ecode 3 end] + set badarg [Dict_getdef $customdict -badarg ""] + set status [private::parse_status_classify $failureclass $payload] + #the -argspecs payload (the whole resolved spec) is display machinery - too bulky + #for a status structure; keep the classification and the small custom keys + set ecode_slim [lrange $ecode 0 2] + foreach {k v} $customdict { + if {$k ne "-argspecs"} { + lappend ecode_slim $k $v + } + } + return [private::parse_status_build $spec_dict $fid -ok 0 -status $status -scheme error -message $r -errorcode $ecode_slim -failureclass $failureclass -badarg $badarg] + } + return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r] + } + #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 @@ -10202,7 +10540,7 @@ tcl::namespace::eval punk::args { if {$leadername ni $leadernames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -badarg $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]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" @@ -10413,7 +10751,7 @@ tcl::namespace::eval punk::args { if {$valname ni $valnames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -badarg $valname -argspecs $argspecs]] $msg } else { #error 444 puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" @@ -13332,7 +13670,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.4.2 + set version 0.5.0 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.4.tm b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm similarity index 98% rename from src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.4.tm rename to src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm index 442d2746..74d4cdcc 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/ns-0.1.4.tm +++ b/src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punk/ns-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::ns 0.1.4 +# Application punk::ns 0.2.0 # Meta platform tcl # Meta license # @@ Meta End @@ -5503,7 +5503,20 @@ y" {return quirkykeyscript} usage information as this is inherently risky. (could create a file, exit the interp etc) " - -return -type string -default table -choices {string table tableobject} + -return -type string -default table -choices {string table tableobject dict} -choicelabels { + string "no table layout" + table "full table layout" + tableobject "table object cmd" + dict "machine-parsable resolution + parse-status" + } -help\ + "Return form of the usage information. + 'dict' returns a dict with keys origin, docid, cmdtype, + args_remaining and parsestatus - where parsestatus is the + parse-status structure of the supplied argument words against + the resolved definition (see punk::args::parse_status for the + documented structure; empty if the command is undocumented). + Its per-argument statuses distinguish a fully-valid, an invalid + and an incomplete argument set machine-parsably." } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { @@ -5564,17 +5577,6 @@ y" {return quirkykeyscript} set opt_grepstr [dict get $opts -grepstr] set opt_form [dict get $opts -form] set opt_return [dict get $opts -return] - switch -- $opt_return { - string { - set estyle "basic" - } - tableobject { - set estyle "minimal" - } - default { - set estyle "standard" - } - } set nextopts [dict remove $opts -grepstr] #JJJ @@ -5597,35 +5599,37 @@ y" {return quirkykeyscript} if {[llength $testresolved] == 1} { #only the command itself is in the args_resolved list - so we can't resolve to a deeper subcommand ledit queryargs -1 -1 {*}$args_remaining ;#prepend - #advisory goodargs parse (marking of supplied words). With NO supplied args - #a failure only reflects missing required leaders/values - nothing to mark and - #nothing wrong with the user's (absent) input, so we show plain usage in the - #info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the - #full usage table INSIDE the raised error only for it to be discarded here - - #doubling display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $queryargs]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $queryargs -form $opt_form -errorstyle $advisory_estyle withid $rootdoc} parseresult]} { - if {![llength $queryargs]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally + #-errorstyle minimal so no usage table is built inside a discarded error). + #-caller attributes any failure message to the queried command rather than + #an internal parse call site. + set pstatus [punk::args::parse_status $queryargs -form $opt_form -caller $querycommand withid $rootdoc] + #With NO supplied args a failure only reflects missing required + #leaders/values - nothing to mark and nothing wrong with the user's (absent) + #input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $queryargs]} { + dict set pstatus scheme info } - } else { + return [dict create origin $rootorigin docid $rootdoc cmdtype $rootorigintype args_remaining $queryargs parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $queryargs]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5695,36 +5699,44 @@ y" {return quirkykeyscript} } } if {$origindoc ne ""} { - #important not to use "-cache 1" for this parse - need to reflect dynamically updated ensembles etc - #advisory goodargs parse (marking of supplied words). With NO supplied trailing args - #a failure only reflects missing required leaders/values (e.g 'i string is') - nothing - #to mark and nothing wrong with the user's (absent) input, so we show plain usage in - #the info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the full - #usage table INSIDE the raised error only for it to be discarded here - doubling - #display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $args_remaining]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $args_remaining -cache 0 -form $opt_form -errorstyle $advisory_estyle withid $origindoc} parseresult]} { - if {![llength $args_remaining]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult - } + #caller attribution for any validation failure message: the queried command plus + #the subcommand words consumed by resolution (not the trailing argument words) + if {[llength $args_remaining]} { + set consumedwords [lrange $queryargs 0 end-[llength $args_remaining]] } else { + set consumedwords $queryargs + } + set caller_display [string trim "$querycommand [join $consumedwords { }]"] + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally uncached and + #-errorstyle minimal so no usage table is built inside a discarded error and + #dynamically updated ensembles are reflected). + set pstatus [punk::args::parse_status $args_remaining -form $opt_form -caller $caller_display withid $origindoc] + #With NO supplied trailing args a failure only reflects missing required + #leaders/values (e.g 'i string is') - nothing to mark and nothing wrong with the + #user's (absent) input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $args_remaining]} { + dict set pstatus scheme info + } + return [dict create origin $origin docid $origindoc cmdtype $origintype args_remaining $args_remaining parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $args_remaining]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5735,6 +5747,9 @@ y" {return quirkykeyscript} } return $result } else { + if {$opt_return eq "dict"} { + return [dict create origin $origin docid "" cmdtype $origintype args_remaining $args_remaining parsestatus {}] + } return "Undocumented command $origin. Type: $origintype" } @@ -7642,6 +7657,6 @@ namespace eval ::punk::args::register { ## Ready package provide punk::ns [tcl::namespace::eval punk::ns { variable version - set version 0.1.4 + set version 0.2.0 }] return \ No newline at end of file diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args-0.4.2.tm b/src/vfs/_vfscommon.vfs/modules/punk/args-0.5.0.tm similarity index 97% rename from src/vfs/_vfscommon.vfs/modules/punk/args-0.4.2.tm rename to src/vfs/_vfscommon.vfs/modules/punk/args-0.5.0.tm index 23ed8406..811ef21e 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args-0.4.2.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args-0.5.0.tm @@ -8,7 +8,7 @@ # (C) 2024 # # @@ Meta Begin -# Application punk::args 0.4.2 +# Application punk::args 0.5.0 # Meta platform tcl # Meta license # @@ Meta End @@ -18,7 +18,7 @@ # doctools header # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools -#[manpage_begin punkshell_module_punk::args 0 0.4.2] +#[manpage_begin punkshell_module_punk::args 0 0.5.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 --}] @@ -4624,8 +4624,17 @@ tcl::namespace::eval punk::args { -badarg -type string -help\ "name of an argument to highlight" -parsedargs -type dict -help\ - "Result of successful punk::pargs::parse - (currently only looks at 'received')" + "Result of successful punk::args::parse. + Together with -badarg this is converted internally to the + parse-status structure the renderers consume (received argument + rows get the goodarg style, choice words matching an argument's + value-in-effect are highlighted)." + -parsestatus -type dict -help\ + "Parse-status structure as returned by punk::args::parse_status. + The renderers derive goodarg/badarg row marking and choice + value-in-effect highlighting from it. Takes precedence over + -badarg/-parsedargs. Should be built for the same form the + display renders (see -form)." -aserror -type boolean -help\ "If true, the usage table is raised as an error message, otherwise it is returned as a value." @@ -4770,17 +4779,22 @@ tcl::namespace::eval punk::args { } #set arg_error_CLR(testsinglecolour) [a+ brightred] - upvar ::punk::args::arg_error_CLR CLR + variable arg_error_CLR set forcereload 0 ;#no need for forcereload to be true for initial run - empty array will trigger initial load - if {[info exists CLR(testsinglecolour)]} { + if {[info exists arg_error_CLR(testsinglecolour)]} { set terminal_colour_is_on [expr {[string length [a+ yellow]]}] - set error_colour_is_on [expr {[string length $CLR(testsinglecolour)]}] + set error_colour_is_on [expr {[string length $arg_error_CLR(testsinglecolour)]}] if {$terminal_colour_is_on ^ $error_colour_is_on} { #results differ set forcereload 1 } } private::argerror_load_colours $forcereload + #per-render colour resolution: CLR is proc-local, seeded from the shared base array; + #a scheme's overrides are merged into this local copy only. (Merging into the shared + #arg_error_CLR leaked scheme overrides into every subsequent render until a colour + #on/off state flip forced an array reload.) + array set CLR [array get arg_error_CLR] if {[llength $args] %2 != 0} { set arg_error_isrunning 0 @@ -4791,7 +4805,7 @@ tcl::namespace::eval punk::args { set badarg "" set parsedargs [dict create] ;#dict with keys: leaders,opts,values,received,solos,multis (as from punk::args::parse) - set goodargs [list] + set parsestatus "" ;#parse-status structure (see ::punk::args::parse_status) - takes precedence over -badarg/-parsedargs #----------------------- #todo!! make changeable from config file #JJJ 2025-07-16 @@ -4802,26 +4816,17 @@ tcl::namespace::eval punk::args { set scheme error set form 0 dict for {k v} $args { - set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -aserror -return -scheme -form} $k] + set fullk [tcl::prefix::match -error "" {-badarg -parsedargs -parsestatus -aserror -return -scheme -form} $k] switch -- $fullk { -badarg { set badarg $v } -parsedargs { - #todo? - dict for {set setinfo} $v { - switch -- $set { - received { - foreach {r rpos} $setinfo { - if {$r ni $goodargs} { - lappend goodargs $r - } - } - } - } - } set parsedargs $v } + -parsestatus { + set parsestatus $v + } -aserror { if {![string is boolean -strict $v]} { set arg_error_isrunning 0 @@ -4844,13 +4849,15 @@ tcl::namespace::eval punk::args { } default { set arg_error_isrunning 0 - error "arg_error invalid option $k. Known_options: -badarg -parsedargs -aserror -scheme -return -form" + error "arg_error invalid option $k. Known_options: -badarg -parsedargs -parsestatus -aserror -scheme -return -form" } } } #todo - scheme - use config and iterm toml definitions etc switch -- $scheme { - "" - -nocolor - -nocolour { + "" - nocolor - nocolour - -nocolor - -nocolour { + #the documented choice value is 'nocolour' - dash spellings and 'nocolor' + #accepted for backward compatibility set scheme nocolour } info - error {} @@ -4883,7 +4890,7 @@ tcl::namespace::eval punk::args { #hack some basics for now. #for coloured schemes - use bold as well as brightcolour in case colour off. - #CLR has been defaulted to arg_error_CLR for its defaults + #CLR is a proc-local copy of arg_error_CLR (see above) #The nocolour,info,error arrays have overrides for some keys. switch -- $scheme { nocolour { @@ -4902,6 +4909,18 @@ tcl::namespace::eval punk::args { } } + #parse-status structure the renderers derive goodarg/badarg row marking and choice + #value-in-effect highlighting from (see ::punk::args::parse_status for the shape). + #An explicit -parsestatus takes precedence; otherwise build one for the displayed + #form from the -parsedargs/-badarg primitives. + if {$parsestatus ne ""} { + set PSTAT $parsestatus + } else { + set PSTAT [private::parse_status_build $spec_dict [lindex $selected_forms 0] -badarg $badarg -parsedargs $parsedargs] + } + set argstatusd [dict get $PSTAT argstatus] + set receivednames [dict get $PSTAT receivednames] + #set RST [a] set RST "\x1b\[0m" @@ -5192,17 +5211,9 @@ tcl::namespace::eval punk::args { #goodargs } } - set full_goodargs [list] - #goodargs may have simplified entries for received opts of form -alias1|-alias2|-realname - #map -realname to full argname - foreach g $goodargs { - if {[string match -* $g] && [dict exists $lookup_optset $g]} { - lappend full_goodargs [dict get $lookup_optset $g] - } else { - lappend full_goodargs $g - } - } - set goodargs $full_goodargs + #received-name normalization to optionset names (formerly done here on the + #transient goodargs local) lives in private::parse_status_build - the + #renderers below consume $argstatusd/$receivednames from the structure if {![catch {package require punk::trie}]} { #todo - reservelist for future options - or just to affect the prefix calculation # (similar to -choiceprefixreservelist) @@ -5304,18 +5315,25 @@ tcl::namespace::eval punk::args { set leading_val_names_hints {} set trailing_val_names_display $trailing_val_names set trailing_val_names_hints {} - #puts "--> parsedargs: $parsedargs" - set parsed_leaders [Dict_getdef $parsedargs leaders {}] - set parsed_opts [Dict_getdef $parsedargs opts {}] - set parsed_values [Dict_getdef $parsedargs values {}] #display options first then values - foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names $parsed_leaders] [list opts $opt_names_display $opt_names_hints $opt_names $parsed_opts] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names $parsed_values]] { - lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames parsedvalues + foreach argumentclassinfo [list [list leaders $leading_val_names_display $leading_val_names_hints $leading_val_names] [list opts $opt_names_display $opt_names_hints $opt_names] [list values $trailing_val_names_display $trailing_val_names_hints $trailing_val_names]] { + lassign $argumentclassinfo argumentclass argnames_display argnames_hints argnames set lastgroup "" set lastgroup_parsekey "" foreach argshow $argnames_display hint $argnames_hints arg $argnames { set arginfo [dict get $form_dict ARG_INFO $arg] + #this argument's entry in the parse-status structure: row marking from + #its status, choice value-in-effect highlighting from hasvalue/value + if {[dict exists $argstatusd $arg]} { + set arg_status [dict get $argstatusd $arg status] + set arg_hasvalue [dict get $argstatusd $arg hasvalue] + set arg_value [dict get $argstatusd $arg value] + } else { + set arg_status unparsed + set arg_hasvalue 0 + set arg_value "" + } if {$argumentclass eq "opts"} { set thisgroup [dict get $arginfo -group] @@ -5354,9 +5372,9 @@ tcl::namespace::eval punk::args { } if {$use_table} { $t add_row [list " $thisgroup" $groupinfo "" "" $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs || $thisgroup_parsekey in $goodargs} { + } elseif {$arg_status eq "ok" || $thisgroup_parsekey in $receivednames} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5364,9 +5382,9 @@ tcl::namespace::eval punk::args { #set arghelp "[a+ bold] $thisgroup$RST $groupinfo" set arghelp [textblock::join -- "[a+ bold] $thisgroup$RST" " " $groupinfo] append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -5527,9 +5545,9 @@ tcl::namespace::eval punk::args { } append cdisplay \n $ctext } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5547,9 +5565,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5613,9 +5631,9 @@ tcl::namespace::eval punk::args { append cdisplay \n $ctext } #puts "-- parsed:$parsedvalues arg:$arg c:$c" - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5640,7 +5658,7 @@ tcl::namespace::eval punk::args { append cdisplay \n [dict get $choicelabeldict $c] } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5657,9 +5675,9 @@ tcl::namespace::eval punk::args { } else { set cdisplay $c } - if {[dict exists $parsedvalues $arg] && [dict get $parsedvalues $arg] eq $c} { + if {$arg_hasvalue && $arg_value eq $c} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] - } elseif {[dict exists $parsedvalues $arg] && $is_multiple && $c in [dict get $parsedvalues $arg]} { + } elseif {$arg_hasvalue && $is_multiple && $c in $arg_value} { dict lappend formattedchoices $groupname [punk::ansi::ansiwrap reverse $cdisplay] } else { dict lappend formattedchoices $groupname $cdisplay @@ -5809,9 +5827,9 @@ tcl::namespace::eval punk::args { set col1 $argshow } $t add_row [list $col1 $typeshow $default $multiple $help] - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_BADARG - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { $t configure_row [expr {[$t row_count]-1}] -ansibase $A_GOODARG } } else { @@ -5823,9 +5841,9 @@ tcl::namespace::eval punk::args { set arghelp "[a+ bold]$argshow$RST $linetail" } append arghelp \n - if {$arg eq $badarg} { + if {$arg_status eq "bad"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_BADARG $arghelp] - } elseif {$arg in $goodargs} { + } elseif {$arg_status eq "ok"} { set arghelp [punk::ansi::ansiwrap -rawansi $A_GOODARG $arghelp] } foreach ln [split $help \n] { @@ -6116,6 +6134,13 @@ tcl::namespace::eval punk::args { #default to enhanced errorstyle despite slow 'catch' (unhappy path) performance #todo - configurable per interp/namespace -errorstyle -type string -default enhanced -choices {enhanced standard basic minimal} + -caller -type string -default "" -help\ + "Caller attribution for validation error messages. + When non-empty, this string replaces the %caller% placeholder in + validation failure messages instead of the automatic call-frame + walk - use it when parsing on behalf of another command (e.g a + usage/help display naming the queried command rather than the + internal parse call site)." -cache -type boolean -default 0 -help\ {Use sparingly. This caches the entire parse result or formatted validation error for the @@ -6200,6 +6225,7 @@ tcl::namespace::eval punk::args { -form {*} -errorstyle standard -cache 0 + -caller {} }] #todo - load override_errorstyle from configuration @@ -6208,7 +6234,7 @@ tcl::namespace::eval punk::args { set opts [dict merge $defaultopts $opts] dict for {k v} $opts { switch -- $k { - -form - -errorstyle - -cache { + -form - -errorstyle - -cache - -caller { } default { #punk::args::usage $args withid ::punk::args::parse ?? @@ -6246,7 +6272,7 @@ tcl::namespace::eval punk::args { set result [punk::args::get_dict $deflist $parseargs -form [dict get $opts -form]] } else { variable parse_cache - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] if {[dict exists $parse_cache $key]} { set cached [dict get $parse_cache $key] if {[dict get $cached type] eq "result"} { @@ -6272,7 +6298,13 @@ tcl::namespace::eval punk::args { set ecode [dict get $erroropts -errorcode] #punk ecode is of form PUNKARGS VALIDATION {description..} -key val ... - set msg [string map [list %caller% [Get_caller]] $msg] + if {[dict get $opts -caller] ne ""} { + #explicit caller attribution supplied (e.g punk::ns::cmdhelp usage display + #naming the queried command) - frame walking would name internal call sites. + set msg [string map [list %caller% [dict get $opts -caller]] $msg] + } else { + set msg [string map [list %caller% [Get_caller]] $msg] + } switch -- $matched_errorstyle { minimal { #return -options [list -code error -errorcode $ecode] $msg @@ -6337,7 +6369,7 @@ tcl::namespace::eval punk::args { } } - set key [list $parseargs $deflist [dict get $opts -form]] + set key [list $parseargs $deflist [dict get $opts -form] [dict get $opts -caller]] dict set parse_cache $key [dict create type "error" value $elist] return {*}$elist } trap {PUNKARGS} {msg erropts} { @@ -6352,6 +6384,312 @@ tcl::namespace::eval punk::args { return $result } + #classify a PUNKARGS VALIDATION failure class-word into the parse-status overall status + #payload = elements of the errorcode description after the class word + proc private::parse_status_classify {failureclass payload} { + switch -- $failureclass { + missingrequiredleader - missingrequiredvalue - leadermissing - optionmissing - valuemissing - missingoptionvalue { + return incomplete + } + leadingvaluecount - trailingvaluecount { + #payload: min max + set num [lindex $payload 0] + set min [Dict_getdef [lrange $payload 1 end] min ""] + if {[string is integer -strict $num] && [string is integer -strict $min] && $num < $min} { + return incomplete + } + return invalid + } + default { + return invalid + } + } + } + + #build the parse-status structure documented in the ::punk::args::parse_status definition. + #fid selects the form whose argument set the per-argument statuses cover (the form usage + #displays render). Options are unvalidated internal inputs - see parse_status/arg_error. + proc private::parse_status_build {spec_dict fid args} { + set defaults [dict create {*}{ + -ok "" + -status "" + -scheme "" + -message "" + -errorcode "" + -failureclass "" + -badarg "" + -parsedargs {} + }] + set opts [dict merge $defaults $args] + set parsedargs [dict get $opts -parsedargs] + set badarg [dict get $opts -badarg] + set form_dict [dict get $spec_dict FORMS $fid] + + #map each member of an -alias1|-alias2|-realname optionset to the optionset (the ARG_INFO key) + set lookup_optset [dict create] + foreach optionset [dict get $form_dict OPT_NAMES] { + foreach o [split $optionset |] { + dict set lookup_optset $o $optionset + } + } + #received argument names normalized to definition argument names. + #received is dict-like but may repeat keys for -multiple arguments; values are clause ordinals. + set receivednames [list] + set positions [dict create] ;#argname -> list of clause ordinals + foreach {r rpos} [Dict_getdef $parsedargs received {}] { + if {[string match -* $r] && [dict exists $lookup_optset $r]} { + set r [dict get $lookup_optset $r] + } + if {$r ni $receivednames} { + lappend receivednames $r + } + dict lappend positions $r $rpos + } + if {$badarg ne "" && [string match -* $badarg] && [dict exists $lookup_optset $badarg]} { + set badarg [dict get $lookup_optset $badarg] + } + set parsed_leaders [Dict_getdef $parsedargs leaders {}] + set parsed_opts [Dict_getdef $parsedargs opts {}] + set parsed_values [Dict_getdef $parsedargs values {}] + + set argstatus [dict create] + foreach {names class parsedvalues} [list {*}{ + } [dict get $form_dict LEADER_NAMES] leader {*}{ + } $parsed_leaders {*}{ + } [dict get $form_dict OPT_NAMES] option {*}{ + } $parsed_opts {*}{ + } [dict get $form_dict VAL_NAMES] value {*}{ + } $parsed_values {*}{ + }] { + foreach arg $names { + set argpositions [Dict_getdef $positions $arg {}] + set received [llength $argpositions] + if {$arg eq $badarg} { + set status bad + set argfailclass [dict get $opts -failureclass] + } elseif {$received > 0} { + set status ok + set argfailclass "" + } else { + set status unparsed + set argfailclass "" + } + #value-in-effect (includes values the parse filled from -default). + #Direct lookup by definition argument name, matching the display renderers: + #aliased optionsets store under their canonical name and are not resolved here. + if {[dict exists $parsedvalues $arg]} { + set hasvalue 1 + set value [dict get $parsedvalues $arg] + } else { + set hasvalue 0 + set value "" + } + dict set argstatus $arg [dict create class $class status $status received $received positions $argpositions hasvalue $hasvalue value $value failureclass $argfailclass] + } + } + return [dict create {*}{ + } ok [dict get $opts -ok] {*}{ + } status [dict get $opts -status] {*}{ + } scheme [dict get $opts -scheme] {*}{ + } message [dict get $opts -message] {*}{ + } errorcode [dict get $opts -errorcode] {*}{ + } failureclass [dict get $opts -failureclass] {*}{ + } badarg $badarg {*}{ + } id [Dict_getdef $spec_dict id ""] {*}{ + } form $fid {*}{ + } receivednames $receivednames {*}{ + } argstatus $argstatus {*}{ + }] + } + + #resolve the form name whose argument set parse-status per-argument entries cover + #(first form matching the -form selection - mirrors arg_error's display-form resolution) + proc private::parse_status_formname {spec_dict formselection} { + set formnames [dict get $spec_dict form_names] + set first [lindex $formselection 0] + if {$formselection eq "*" || $formselection eq ""} { + return [lindex $formnames 0] + } + if {[string is integer -strict $first]} { + set fid [lindex $formnames $first] + if {$fid ne ""} { + return $fid + } + return [lindex $formnames 0] + } + set fmatch [tcl::prefix::match -error "" $formnames $first] + if {$fmatch ne ""} { + return $fmatch + } + return [lindex $formnames 0] + } + + lappend PUNKARGS [list { + @id -id ::punk::args::parse_status + @cmd -name punk::args::parse_status\ + -summary\ + "Parse-status structure from a parse attempt (validation failures reported, not raised)."\ + -help\ + "Run a parse attempt of arglist against a punk::args definition and + return a parse-status dict describing the outcome - for success AND + for validation failure (a validation failure is reported in the + returned structure instead of being raised as an error; other errors + such as an unknown id still raise). + + This is the machine-parsable form of the information behind the + usage-display argument marking (punk::ns::cmdhelp / 'i '): + which supplied arguments validated, which argument failed and why, + and which display scheme applies. punk::args::arg_error accepts the + structure via its -parsestatus option and derives its goodarg/badarg + row marking and choice value-in-effect highlighting from it. + + Overall keys: + ok 1 parse succeeded, 0 validation failure + status valid | invalid | incomplete + invalid - a supplied word failed validation + incomplete - required arguments missing (a count or + allocation shortfall). Note a supplied word + failing its -type check can also surface as + an allocation shortfall (missingrequiredvalue) + - badarg and the per-argument statuses carry + the specifics in both situations. + scheme suggested display scheme: info (ok) | error (failure) + message empty | the validation failure message + errorcode empty | the -errorcode of the validation failure with + the bulky -argspecs payload removed + failureclass empty | first word of the errorcode description + (e.g choiceviolation, typemismatch, missingrequiredvalue) + badarg empty | name of the offending/unfillable argument + id the definition id + form form name the per-argument statuses were built for + (the first form matching -form - the form usage + displays render) + receivednames received argument names normalized to definition + argument names (opt aliases folded to their + -alias|-name optionset) + argstatus dict keyed by definition argument name, each value a + dict with keys: + class leader | option | value + status ok (received and validated) | + bad (the offending argument) | + unparsed (not received) + received count of received clauses (0 if unreceived) + positions ordinal positions of received clauses + hasvalue 1 if a value-in-effect is known + value value-in-effect - includes values the + parse filled from -default; empty when + hasvalue is 0. No values are reported + for a failed parse. + " + @form -form {withid withdef} + @leaders -min 1 -max 1 + arglist -type list -optional 0 -help\ + "Arguments to parse - supplied as a single list" + @opts -prefix 0 + -form -type list -default * -help\ + "Restrict parsing to the set of forms listed (see punk::args::parse). + The per-argument statuses are built for the first matching form." + -caller -type string -default "" -help\ + "Caller attribution for the validation failure message + (see punk::args::parse -caller). When empty, defaults to the + definition's @cmd -name (or its id) rather than the call-frame + walk parse would perform." + @values -min 2 + + @form -form withid -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withid $id" + @values -max 2 + withid -type literal(withid) -help\ + "The literal value 'withid'" + id -type string -help\ + "id of punk::args definition for a command" + + @form -form withdef -synopsis "parse_status arglist ?-form {int|...}? ?-caller ? withdef $def ?$def?" + withdef -type literal(withdef) -help\ + "The literal value 'withdef'" + def -type string -multiple 1 -optional 0 -help\ + "Each remaining argument is a block of text + defining argument definitions." + }] + proc parse_status {args} { + if {[llength $args] < 3} { + punk::args::parse $args withid ::punk::args::parse_status + } + set opts_and_vals $args + set arglist [lpop opts_and_vals 0] + set opts [dict create {*}{ + -form {*} + -caller {} + }] + while {[llength $opts_and_vals] && [string match -* [lindex $opts_and_vals 0]]} { + set k [lpop opts_and_vals 0] + switch -- $k { + -form - -caller { + if {![llength $opts_and_vals]} { + punk::args::parse $args withid ::punk::args::parse_status + } + dict set opts $k [lpop opts_and_vals 0] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + } + set tailtype [lpop opts_and_vals 0] + switch -- $tailtype { + withid { + if {[llength $opts_and_vals] != 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set id [lindex $opts_and_vals 0] + set spec_dict [get_spec $id] + if {$spec_dict eq ""} { + error "punk::args::parse_status - no such id: $id" + } + } + withdef { + if {[llength $opts_and_vals] < 1} { + punk::args::parse $args withid ::punk::args::parse_status + } + set spec_dict [resolve {*}$opts_and_vals] + } + default { + punk::args::parse $args withid ::punk::args::parse_status + } + } + set fid [private::parse_status_formname $spec_dict [dict get $opts -form]] + set caller [dict get $opts -caller] + if {$caller eq ""} { + #the frame walk parse would do by default names parse_status's own internal + #parse call - the definition's command name is the meaningful attribution here + set caller [Dict_getdef $spec_dict cmd_info -name [Dict_getdef $spec_dict id ""]] + } + if {[catch { + punk::args::parse $arglist -form [dict get $opts -form] -errorstyle minimal -caller $caller $tailtype {*}$opts_and_vals + } r ropts]} { + set ecode [Dict_getdef $ropts -errorcode ""] + if {[lrange $ecode 0 1] ne [list PUNKARGS VALIDATION]} { + #not an argument validation failure (e.g definition error) - propagate + return -options $ropts $r + } + set classinfo [lindex $ecode 2] + set failureclass [lindex $classinfo 0] + set payload [lrange $classinfo 1 end] + set customdict [lrange $ecode 3 end] + set badarg [Dict_getdef $customdict -badarg ""] + set status [private::parse_status_classify $failureclass $payload] + #the -argspecs payload (the whole resolved spec) is display machinery - too bulky + #for a status structure; keep the classification and the small custom keys + set ecode_slim [lrange $ecode 0 2] + foreach {k v} $customdict { + if {$k ne "-argspecs"} { + lappend ecode_slim $k $v + } + } + return [private::parse_status_build $spec_dict $fid -ok 0 -status $status -scheme error -message $r -errorcode $ecode_slim -failureclass $failureclass -badarg $badarg] + } + return [private::parse_status_build $spec_dict $fid -ok 1 -status valid -scheme info -parsedargs $r] + } + #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 @@ -10202,7 +10540,7 @@ tcl::namespace::eval punk::args { if {$leadername ni $leadernames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredleader $leadername ] -badarg $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]] "private::get_dict_can_assign_value consumed 0 unexpected 2?" @@ -10413,7 +10751,7 @@ tcl::namespace::eval punk::args { if {$valname ni $valnames_received} { #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 + return -options [list -code error -errorcode [list PUNKARGS VALIDATION [list missingrequiredvalue $valname ] -badarg $valname -argspecs $argspecs]] $msg } else { #error 444 puts stderr "get_dict cannot assign val:$val to valname:$valname (444)" @@ -13332,7 +13670,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.4.2 + set version 0.5.0 }] return diff --git a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.2.0.tm similarity index 98% rename from src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm rename to src/vfs/_vfscommon.vfs/modules/punk/ns-0.2.0.tm index 442d2746..74d4cdcc 100644 --- a/src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punk/ns-0.1.4.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/ns-0.2.0.tm @@ -7,7 +7,7 @@ # (C) 2023 # # @@ Meta Begin -# Application punk::ns 0.1.4 +# Application punk::ns 0.2.0 # Meta platform tcl # Meta license # @@ Meta End @@ -5503,7 +5503,20 @@ y" {return quirkykeyscript} usage information as this is inherently risky. (could create a file, exit the interp etc) " - -return -type string -default table -choices {string table tableobject} + -return -type string -default table -choices {string table tableobject dict} -choicelabels { + string "no table layout" + table "full table layout" + tableobject "table object cmd" + dict "machine-parsable resolution + parse-status" + } -help\ + "Return form of the usage information. + 'dict' returns a dict with keys origin, docid, cmdtype, + args_remaining and parsestatus - where parsestatus is the + parse-status structure of the supplied argument words against + the resolved definition (see punk::args::parse_status for the + documented structure; empty if the command is undocumented). + Its per-argument statuses distinguish a fully-valid, an invalid + and an incomplete argument set machine-parsably." } {${[punk::args::resolved_def -types opts ::punk::args::arg_error -scheme]}} { @@ -5564,17 +5577,6 @@ y" {return quirkykeyscript} set opt_grepstr [dict get $opts -grepstr] set opt_form [dict get $opts -form] set opt_return [dict get $opts -return] - switch -- $opt_return { - string { - set estyle "basic" - } - tableobject { - set estyle "minimal" - } - default { - set estyle "standard" - } - } set nextopts [dict remove $opts -grepstr] #JJJ @@ -5597,35 +5599,37 @@ y" {return quirkykeyscript} if {[llength $testresolved] == 1} { #only the command itself is in the args_resolved list - so we can't resolve to a deeper subcommand ledit queryargs -1 -1 {*}$args_remaining ;#prepend - #advisory goodargs parse (marking of supplied words). With NO supplied args - #a failure only reflects missing required leaders/values - nothing to mark and - #nothing wrong with the user's (absent) input, so we show plain usage in the - #info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the - #full usage table INSIDE the raised error only for it to be discarded here - - #doubling display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $queryargs]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $queryargs -form $opt_form -errorstyle $advisory_estyle withid $rootdoc} parseresult]} { - if {![llength $queryargs]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally + #-errorstyle minimal so no usage table is built inside a discarded error). + #-caller attributes any failure message to the queried command rather than + #an internal parse call site. + set pstatus [punk::args::parse_status $queryargs -form $opt_form -caller $querycommand withid $rootdoc] + #With NO supplied args a failure only reflects missing required + #leaders/values - nothing to mark and nothing wrong with the user's (absent) + #input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $queryargs]} { + dict set pstatus scheme info } - } else { + return [dict create origin $rootorigin docid $rootdoc cmdtype $rootorigintype args_remaining $queryargs parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $queryargs]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $rootdoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5695,36 +5699,44 @@ y" {return quirkykeyscript} } } if {$origindoc ne ""} { - #important not to use "-cache 1" for this parse - need to reflect dynamically updated ensembles etc - #advisory goodargs parse (marking of supplied words). With NO supplied trailing args - #a failure only reflects missing required leaders/values (e.g 'i string is') - nothing - #to mark and nothing wrong with the user's (absent) input, so we show plain usage in - #the info scheme instead of the internal-looking parse error (G-046 item 5). - #Use -errorstyle minimal for that case: the $estyle rendering would build the full - #usage table INSIDE the raised error only for it to be discarded here - doubling - #display time for large argdocs (e.g 'i punk::args::define'). - if {![llength $args_remaining]} { - set advisory_estyle minimal - } else { - set advisory_estyle $estyle - } - if {[catch {punk::args::parse $args_remaining -cache 0 -form $opt_form -errorstyle $advisory_estyle withid $origindoc} parseresult]} { - if {![llength $args_remaining]} { - if {!$scheme_received} { - dict set nextopts -scheme info - } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } elseif {$opt_return eq "tableobject"} { - set result [punk::args::arg_error "$parseresult" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] - } else { - set result $parseresult - } + #caller attribution for any validation failure message: the queried command plus + #the subcommand words consumed by resolution (not the trailing argument words) + if {[llength $args_remaining]} { + set consumedwords [lrange $queryargs 0 end-[llength $args_remaining]] } else { + set consumedwords $queryargs + } + set caller_display [string trim "$querycommand [join $consumedwords { }]"] + #advisory goodargs parse of the supplied words via parse_status (validation + #failures are reported in the structure, not raised; internally uncached and + #-errorstyle minimal so no usage table is built inside a discarded error and + #dynamically updated ensembles are reflected). + set pstatus [punk::args::parse_status $args_remaining -form $opt_form -caller $caller_display withid $origindoc] + #With NO supplied trailing args a failure only reflects missing required + #leaders/values (e.g 'i string is') - nothing to mark and nothing wrong with the + #user's (absent) input, so we show plain usage in the info scheme instead of the + #internal-looking parse error (G-046 item 5). + if {$opt_return eq "dict"} { + if {$scheme_received} { + dict set pstatus scheme [dict get $opts -scheme] + } elseif {![dict get $pstatus ok] && ![llength $args_remaining]} { + dict set pstatus scheme info + } + return [dict create origin $origin docid $origindoc cmdtype $origintype args_remaining $args_remaining parsestatus $pstatus] + } + if {[dict get $pstatus ok]} { #show usage - with goodargs marked if {!$scheme_received} { dict set nextopts -scheme info } - set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsedargs $parseresult] + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] + } elseif {![llength $args_remaining]} { + if {!$scheme_received} { + dict set nextopts -scheme info + } + set result [punk::args::arg_error "" [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0] + } else { + set result [punk::args::arg_error [dict get $pstatus message] [punk::args::get_spec $origindoc] {*}$nextopts -aserror 0 -parsestatus $pstatus] } if {$opt_grepstr ne ""} { if {[llength $opt_grepstr] == 1} { @@ -5735,6 +5747,9 @@ y" {return quirkykeyscript} } return $result } else { + if {$opt_return eq "dict"} { + return [dict create origin $origin docid "" cmdtype $origintype args_remaining $args_remaining parsestatus {}] + } return "Undocumented command $origin. Type: $origintype" } @@ -7642,6 +7657,6 @@ namespace eval ::punk::args::register { ## Ready package provide punk::ns [tcl::namespace::eval punk::ns { variable version - set version 0.1.4 + set version 0.2.0 }] return \ No newline at end of file